Mixed Type Types?

In a Cadence contract I have a function that has the ability to return the desired optional struct given an address and a UInt64.

import C from 0x___

pub fun main(address: Address, id: UInt64): C.ReadOnly? {
  return C.fetc(address: address, id: id)
}

From javascript (where CODE is the above) I can call the above like so:

await fcl.send([
  fcl.script(CODE),
  fcl.args([
    fcl.arg("0x____", t.Address),
    fcl.arg(72, t.UInt64)
  ])
]).then(fcl.decode)

I am trying to turn the above into a batch fetching, where given a list of address, id pairs I can return a list of the above results. Which is where I run into my issue that I need help with.
An important additional constraint to keep in mind is there is no guarantee that the address and id are unique. Can the smart people here think of a nice way of doing this?

The following cadence is invalid (mixed-type array) but should be a way to convey what I am trying to do

const WANT = [
  { key: "a", value: [addr1, id1] },
  { key: "b", value: [addr2, id2] },
  { key: "c", value: [addr2, id2] },
  { key: "d", value: [addr3, id3] },
]

const CODE = fcl.cdc`
  import C from 0x____

  pub fun main(want: {String: [Address, UInt64]}): {String: C.ReadOnly?} {
    let result: {String: C.ReadOnly?} = {}
    for let w in want.keys {
      result[w] = C.fetch(address: w[0], id: w[1])
    }
    return result
  }
`

await fcl.send([
  fcl.script(CODE),
  fcl.args([
    fcl.arg(WANT, t.Dictionary({ key: t.String, value: t.Array([t.Address, t.Unit64]) }))
  ])
]).then(fcl.decode) // Would return something like { a: thing, b: thing, c: null, d: thing }

If you want to have values of various types in an array, you have to use a type that all values conform to. In your case, both Address and UInt64 are value types (as opposed to resources), so they conform to AnyStruct.

A gotcha in Cadence is the type inference right now: An array is inferred to have the type of the first element, so you have to cast the first element to AnyStruct:

let address: Address = 0x1
let number: UInt64 = 1
let values: [AnyStruct] = [address as AnyStruct, number]
3 Likes

Ohh. Yes the type inference was tripping me up. Nice.

Thereโ€™s an issue to improve the type inference for array literals, dictionary literals, and the ternary operator here: https://github.com/onflow/cadence/issues/61

1 Like

Me, personally, would start with something like this:

const CODE = fcl.cdc`
  import C from 0x____

  pub fun main(keys: [String], addresses: [Address], ids: [UInt64]} {
    let result: {String: C.ReadOnly?} = {}

    // This should be really in the JS code, but just for the sake of user we will check it here
    assert(keys.length == addresses.length && addresses.length == ids.length, "Arrays have different number of elements")

    var i = 0
    while i < keys.length {
        let key = keys[i]
        let address = addresses[i]
        let id = ids[i]
        result[key] = C.fetch(address: address, id: id)  // we can make names optional if we are allowed to edit contract
        
        i = i + 1
    }

    return result
  }
`

const WANT = [
  { key: "a", value: [addr1, id1] },
  { key: "b", value: [addr2, id2] },
  { key: "c", value: [addr2, id2] },
  { key: "d", value: [addr3, id3] },
]

const { keys, addresses, ids } = WANT.reduce((acc, item) => {
    acc.keys.push(item.key);
    acc.addresses.push(item.value[0])
    acc.ids.push(item.value[1])
   
    return acc
   }, { keys: [], addresses: [], ids: [] })

await fcl.send([
  fcl.script(CODE),
  fcl.args([
    fcl.arg(keys, t.Array(String)),
    fcl.arg(keys, t.Array(Address)),
    fcl.arg(ids, t.Array(UInt64))
  ])
]).then(fcl.decode) // Would return something like { a: thing, b: thing, c: null, d: thing }

While itโ€™s possible to create typecasting solution for mixed type array/dictionary, I think itโ€™s overkill for a case with this constraints :slight_smile:

For my own reference and as an update I was able to get this to work with this:

const result = await fcl.send([
  fcl.script`
    pub struct Foo {
      pub let addr: String
      pub let id: UInt64

      init(addr: String, id: UInt64) {
        self.addr = addr
        self.id = id
      }
    }
    
    pub fun main(args: {String: [AnyStruct]}): {String: Foo} {
      let r: {String: Foo} = {}
      
      for key in args.keys {
        let addr: String = args[key]![0] as! String
        let id: UInt64 = args[key]![1] as! UInt64
        
        r[key] = Foo(addr: addr, id: id)
      }
      return r
    }
  `,
  fcl.args([
    fcl.arg([
      {key: "foo", value: ["rawr", 3]},
      {key: "bar", value: ["moo", 9]},
    ], t.Dictionary({ key: t.String, value: t.Array([t.String, t.UInt64]) }))
  ])
]).then(fcl.decode)

 assert(result, {
  "foo": {
    "addr": "rawr",
    "id": 3
  },
  "bar": {
    "addr": "moo",
    "id": 9
  }
})

Any hints or any thoughts on ways of making it cleaner? This code will be in the exemplar app?
cc: @bastian @robmyers @MaxStarka

1 Like

In my answer I mostly tried to focus on having values of different types in an array.

As to your concrete problem/code: I donโ€™t think itโ€™s a good idea to put this type-unsafe code in the exemplar app, what @MaxStarka showed is type-safer. Maybe replace while i < 5 with the length of the addresses, and check the length of the ids matches the length of the addresses.

2 Likes

That was a silly mistake, thanks for catching that one up, @bastian :sweat_smile:
Iโ€™ve replaced 5 with actual length of array and added assert to check that lengths of arrays match.

1 Like

Yeah, I came up with a similar solution to @MaxStarka in my head already and was already probably going to go in that direction, was hoping there would be a nicer way of doing it though. Itโ€™s not really a solution I was (or still am) particularly excited/happy with. While splitting things up into three arrays and then using the script as a zip function works there is a heap of additional incidental complexity involved in both the expansion of the data in javascript and zipping of the data in cadence. Both solutions feel a little hacky, dirty and off, more of a โ€œit will get the job doneโ€ kind of vibe and wouldnโ€™t feel very proud if I were to recommend either of them as a solution to others, which the exemplar app is supposed to beโ€ฆ :confused:

From a web app perspective we need batch fetching, from most of the contracts I have seen itโ€™s most likely going to be an address of the resource owner, plus other things that act as some sort of cursor for lookup.

Iโ€™m in no way saying we need to hastily find a solution, or add some sort of new type to Cadence, more asking if we can have a think about this on our own, or keep it in the back of our minds so that when it comes up again (no way I will be the last one haha) we might have some answers ready or even a solution we can be proud of and recommend without hesitation.

Can any of us think of any other possible solutions to this batch problem? I worry that the initial code example might have added a bias to, or skewed the solutions a little. From a cadence perspective, @bastian are you able to say or give an example of a nice way of writing cadence to achieve a similar thing where the data remains grouped together? How should I go about thinking about how best to do this in cadence?