Optionals in Cadence? Not Optional!
EDIT Oct 2024: The Cadence code in the article uses Cadence 0.42 which is incompatible with the current version of Cadence on all Flow networks. The themes and advice are all mostly still the same though but the code examples will need to be updated to the latest Cadence version.
Hello everyone! Welcome to my bi-weekly blog about Cadence, the new smart contract programming language for the Flow Blockchain. If you are a newcomer, check out my introductory blog post to get started!
Today, I’m going to talk about an aspect of Cadence that many people have a lot of trouble fully wrapping their heads around: Optionals!
Optionals are like the Schrödinger’s Cat of programming languages.
Or are they?
Sorry, bad analogy, but you’ll understand where I am coming from when you’re done reading.
What are Optionals?
Optionals in Cadence are a safe and powerful way to use variables in programming languages. They can be difficult to get familiar with though!
Optionals say either “there is a value, and it is x” or “there isn’t a value at all”. So the value either has been set or, more importantly, has not been set. Optionals default to nil
(which technically is a value) when they don’t have a value of their specific type.
Any type can be an optional. You declare an optional type by using a ?
after the regular variable declaration, like so:
// Declare a String Optional
var name: String?
When assigning to an optional, you can assign to it as if the type was non-optional:
name = "Josh"
but you can also assign the value nil
to the optional.
name = nil
Why would I want to say something is nil
?
Optionals allow software to be able to handle error cases and similar situations more gracefully.
The main goal is to make the unavailabe/empty/nothing state of a variable explicit, and even more so, make it explicit when something cannot ever be unavailabe/empty/nothing.
In languages that allow any variable to be null, it’s often not clear if the variable could be null at the specific location of the code, which leads to two problems:
- Either the null case isn’t handled by the programmer, and then there is a null value, causing a null-dereference, i.e. crash;
- Or the null case is considered by the programmer, and they add defensive checks to handle the supposed null-case, though it actually never occurs, causing unnessary overhead.
Here is an example:
Lets say we are using the field that I defined above in a contract, but not as an optional:
pub contract NameHolder {
pub var name: String
}
If the value hasn’t been set yet, but someone wants to read it, how do we communicate that it hasn’t been set? We could just return an empty string, but that is effectively the same as returning any non-empty string, so the reader might not know that an empty string means that it hasn’t been set yet and make a mistake assuming that everything is ok.
If we make the field optional:
pub contract NameHolder {
pub var name: String?
}
Then it becomes very clear to the reader. If they try to read name
and they get nil
back, then they can easily handle the error and do whatever they need to do in that case. There is no confusion there. nil
means nil.
In Cadence, fields in composite types are required to be initialized, so this exact problem won’t come up very often, but you will often see similar cases that are slightly more complex.
For example, the values in dictionaries are all optionals. You can initialize a dictionary to be empty like this:
let lastNameDictionary: {String: String} = {"Josh": "Hannan"}
But since there are an infinite possible number of key-value pairs, if you try to access any one of them, it will return an optional, even if the value exists for the key you provide!
let lastNameDictionary: {String: String} = {"Josh": "Hannan"}// Both of these cause an error:
// Invalid: mismatched types. expected `String`, got `String?
let existingName: String = lastNameDictionary["Josh"]
let newName: String = lastNameDictionary["Kayla"]
How do I handle Optionals?
If left to their own devices, optionals can be a handful!
Values of optional variables are still considered to be wrapped in the optional. “Wrapped” is a term that means that we still don’t know whether or not the value is nil
or not. If you try to perform a normal operation that you could usually perform with the type you are using, it will fail the Cadence type checker:
let x: Int? = 2// Invalid: cannot apply binary operation + to types: `Int?`, `Int`
let y = x + 2
Even though both arguments specify some form of Int
, x
is still wrapped, so the program doesn’t actually know if it is nil
or not. It won’t let us perform a mathematical operation with it because you can’t add nil
and a number! That would explode the universe!!!!
You need to unwrap the value from its optional first, then perform the operation.
This also applies to composite types. If you have a composite type that is optional, you need to unwrap the optional before you can call any of its methods or access any of its fields.
Here is a simple example:
pub contract HelloWorld {
pub resource OptionalHello {
pub fun hello(): String {
return "hello"
}
} access(self) let helloResource: @OptionalHello? init() {
self.helloResource <- create OptionalHello()
} // Public function that returns our friendly greeting!
pub fun hello(): String {
return self.helloResource.hello()
}
}
So if you see an error like this:
value of type `HelloWorld.OptionalHello?` has no member `hello`. unknown member
Look for the ?
at the end of the type name. The object is often still a wrapped optional and it needs to be unwrapped before calling the hello
method. (better error messages are also coming in the future) 😃
Unwrapping Optionals
There are multiple different ways to unwrap optionals, and they all have different behavior and are used for different use-cases.
Nil-Coalescing Operator
The nil-coalescing operator ??
returns the value inside an optional if it contains a value, or returns a specified alternative value if the optional has no value, i.e., the optional value is nil
.
If the left-hand side is non-nil, the right-hand side is not evaluated.
See the Cadence docs for more info about the nil-coalescing operator.
You probably will see this used a lot when borrowing references to capabilities and resources. It is possible to put any code on the right side of the ??
so if a borrow fails, it returns an optional that is nil
, but in many cases, if a borrow fails, then the transaction should fail. The nil-coalescing operator allows us to panic and print an error message so the state is reverted and the caller knows exactly what went wrong.
For example, when we borrow a reference to an account’s FlowToken
receiver, we usually use this:
// Get a reference to the recipient's Receiver
let receiverCap = getAccount(to).getCapability(/public/flowTokenReceiver)
let receiverRef = receiverCap.borrow<&{FungibleToken.Receiver}>()
?? panic("Could not borrow receiver reference")
If borrow
succeeds, it returns a valid reference. If it fails, it returns nil
. Therefore, the type of the return value is &{FungibleToken.Receiver}?
and we need to unwrap it before we can use it.
Force-Unwrap
Another more succinct way of unwrapping an optional value is to use the force-unwrap operator (!
). The force unwrap will get the value of the optional if it exists, or it will panic and abort if it doesn’t.
let a: Int? = 3
let b: Int = a! // Will succeed and return 3let c: Int? = nil
let d: Int = c! // Will panic and abort at runtime
See the Cadence docs for more information and examples of force-unwrap.
If a force-unwrap fails, it only prints a generic error message about failing when trying to force unwrap. Because of this, you should be very careful about where you use force-unwrap, because if it fails, it will be much harder for you to determine where the error is coming from.
You should only use the force-unwrap operator in places where you are absolutely sure that the value isn’t going to be nil
. For example, if you have already checked that it isn’t nil in a pre-condition, then you know that when you force-unwrap, you’ll be safe.
Optional Binding
Another powerful way to unwrap and handle optionals is via optional binding, a method that allows you to include the unwrapping in a conditional that executes different blocks of code depending on how the unwrapping went.
Optional Biding is a variant of the if-statement that conditionally executes a body of code if an optional is nil
or not.
See the cadence docs for examples of optional binding
Optional binding is a thorough way to handle different cases for optionals, because it gives you the most flexibility with how your program can behave.
If nil
is an expected potential value for an optional in your program, and you have many operations you need to perform in that case, then optional binding is the way to go.
Optional Chaining
Last but not least, we have optional chaining. Sometimes, there are composite types that are optional, because they are stored in a dictionary, or are returned as optionals from a function that retrieves them. If you want to call a function on an optional composite type, you can use optional chaining to “optionally” call that function. If the object you are calling is nil
, it will just not execute any function and move on and return nil
.
See the Cadence docs for more info and examples.
Conclusion
Optionals are an important feature of many languages and learning to use them properly is important for writing clean and secure code so make sure you understand this well!
I hope this post has been informative and useful!
Flow Discord: https://discord.gg/flow
Flow Forum: https://forum.onflow.org
Flow Github: https://github.com/onflow/flow
See you next week! 👋
Thank you to the cats of Axoim Zen for starring in this post!
And thank you to Bastian and Supun for reviewing this, plus Alyssa for the amazing graphics!