Nested if statements with assignments?

I often find myself needing to nest if statements with assignments. This is usually done in a script where exploding because something is lacking is bad, the return value for these scripts is usually an optional struct.

In general I am not needing the capability past the point of know if it exists or not. I mostly need access to the information inside a resource if its there otherwise i want the value for everything to be nil. The question is can this be done in a nicer way?

import BarCollection from 0x___

pub fun main(addr: Address, id: UFix64): BarCollection.ReadOnly? {
  let collectionCapability = getAccount(address)
    .getCapability<&BarCollection.Collection{BarCollection.CollectionPublic}>(BarCollection.publicPath)!
  
  if let collection = collectionCap.borrow() {
    if let nft = collection.borrowNFT(id: id) {
      return nft.asReadOnly()
    }
  }

  return nil
}

We need to do these additional checks because there is no optional equivalent to the above collection.borrowNFT in the NFT spec. (Line 130: https://flow-view-source.com/testnet/account/0x631e88ae7f1d7c20/contract/NonFungibleToken)

In the above case it doesn’t seem that bad, but there are situations, like in a market place, where we may want to receive both the listing info as well as the the NFTs info. In which case the above gets fairly more complex/messy… Once again, if the nft or the listing isnt a thing I am wanting it to return nil

import BarCollection from 0x___
import BarMarket from 0x___

pub struct ReadOnly {
  let listing: BarMarket.ReadOnly
  let nft: BarCollection.ReadOnly

  init(listing: BarMarket.ReadOnly, nft: BarCollection.ReadOnly) {
    self.listing = listing
    self.nft = nft
  }
}

pub fun main(addr: Address, id: UFix64): ReadOnly? {
  let collectionCapability = getAccount(address).getCapability<&BarCollection.Collection{BarCollection.CollectionPublic}>(BarCollection.publicPath)!
  let marketCapability     = getAccount(address).getCapability<&BarMarket.Collection{BarMarket.CollectionPublic}>(BarMarket.publicPath)!
 
  if let market = marketCapability.borrow() {
    if let listing = market.borrowListing(id: id) {
      if let collection = collectionCap.borrow() {
        if let nft = collection.borrowNFT(id: id) {
          return ReadOnly(listing: listing.asReadOnly(), nft: nft.asReadOnly())
        }
      }
    }
  }

  return nil
}

Maybe I am completely missing something? Maybe some sort of optional chaining?Is there a nicer way of doing the above?

If there isn’t currently a nicer way of doing this, I would like to offer up some inspiration from elixirs with statement (https://elixir-lang.org/getting-started/mix-otp/docs-tests-and-with.html#with).

Very roughly translated the above code in elixir could looks something like this:

def main(addr, id) do
  with acct <- getAccount(addr),
       {:ok, nft} <- acct.getCapability<___>(___)!.borrow()!.borrowNFT(id: id),
       {:ok, listing} <- acct.getCapability<___>(___)!.borrow()!.borrowListing(id: id) do
    %{nft: nft, listing: listing}
  else
    _ -> nil
  end     
end

Great point that if-let statements are not ideal for working with multiple optionals, as they lead to deep nesting.

Here are some ideas on how this could be improved:

  • Swift has guard statements, see https://docs.swift.org/swift-book/ReferenceManual/Statements.html#ID524, which are a bit like the inverse of the if: when the condition is false or the binding fails (due to the value being nil), the block is executed and it must somehow stop execution of the function at the end, e.g. by using a return statement.

    With such a feature your example would look something like this:

    pub fun main(addr: Address, id: UFix64): BarCollection.ReadOnly? {
      let collectionCapability = getAccount(address)
        .getCapability<&BarCollection.Collection{BarCollection.CollectionPublic}>(BarCollection.publicPath)!
    
      guard let collection = collectionCap.borrow() else {
          return nil
      }
      guard let nft = collection.borrowNFT(id: id) else {
          return nil
      }
      return nft.asReadOnly()
    }
    
  • In addition, Swift allows multiple optional bindings in if-statements and guard-statements, which reduces the nesting.

    With such a feature your example would look something like this:

    pub fun main(addr: Address, id: UFix64): BarCollection.ReadOnly? {
      let collectionCapability = getAccount(address)
        .getCapability<&BarCollection.Collection{BarCollection.CollectionPublic}>(BarCollection.publicPath)!
    
      if let collection = collectionCap.borrow(),
         let nft = collection.borrowNFT(id: id) 
      {
          return nft.asReadOnly()
      }
      return nil
    }
    
  • In Kotlin, return can be used in expressions, and it is common to used with nil coalescing operator.

    With such a feature your example would look something like this:

    pub fun main(addr: Address, id: UFix64): BarCollection.ReadOnly? {
      let collectionCapability = getAccount(address)
        .getCapability<&BarCollection.Collection{BarCollection.CollectionPublic}>(BarCollection.publicPath)!
  
      let collection = collectionCap.borrow() ?? return nil
      let nft = collection.borrowNFT(id: id) ?? return nil
      return nft.asReadOnly()
    }
    ```
1 Like

Okay, so I think what I am seeing is that it isn’t currently possible to make that nicer?

I dont really have any issues about the stuff you are suggesting there from the other languages. A couple points I wouldn’t mind highlighting though.

  • Returning from a nullish coalescing operator like in kotlin might be confusing in cadence given how return currently works.
  • Multiple lets per if statement would probably work just fine
  • I like the guard idea, sort of like an early return in javascript

A try catch sort of thing could also work for this i think. Write the code in a very explicit way like you are expecting it to be there, and then if if throws an error catch and return nil, but can also see some issues with that like partial state reverting in transactions which could lead to all sorts of really bad things :confused: don’t know if thats a thing we really want in cadence without really really thinking it through.

pub fun main(address: Address, id: UInt64) {
  try {
    let listing = getAccount(address).getCapability<__>(__)!.borrow()!.borrowListing(id: id).asReadOnly()
    let nft = getAccount(address).getCapability<__>(__)!.borrow()!.borrowNFT(id: id).asReadOnly()
    return ReadOnly(listing: listing, nft: nft)
  } catch _e {
    return nil
  }
}

Was looking for guard myself when doing nested ifs.

1 Like