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?