Maybe; try Optionals —
Maybe; try Optionals.
Optionals, what are they? They are being added almost uniformly to newer languages (Rust, Swift) and to older languages in heavy use (C++ 17, Java 8). They can help you write clearer, cleaner code when used correctly. Let’s see what they are about.
Optionals (or Option types or Nullable types) are types that are allowed to have a valid state (Some) and an invalid state (None). They help to clarify that a piece of data is not yet initialized and so its value is not valid. This helps particularly with types that have a default value that could be used but it’s not clear if it is actually set.
For example, an integer or boolean type, all the values in the range could be used. How can you state something is actually initialized without blindly assuming 0 for integers means invalid, or false for booleans means it’s not initialized? Use Optionals.
When explaining optionals, it’s a good idea to start with what they are at their core: Variants.
Variants
Variants or Tagged Unions, or Sum Types, are a simple concept. They are a type that can hold other fixed types. You can think of it as a C union, except that they explicitly keep track of what Type they contain. You can’t mix the types inside the variant. It is one and only one of the things it holds at any point in time.
What do Optionals look like?
In a hypothetical language that supports them, they go like this:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Optional of T { | |
None, //There is nothing here. Empty. Nada. | |
Some(T) //We have an item T | |
} | |
//Examples | |
let myOption = Some(5); // Some(int32) | |
let myOption = Some("Rodents of unusual size. I don't believe they exist"); // Some(String) | |
let myOption = None; // We got nothing. |
So when you have a bit of data that holds an Optional, it either contains a T or None.
You can really think about it like a present. When you unwrap the present and has a toy in it, you have Some(Toy). But when you unwrap your present and there is nothing there, you have None. It’s not more complicated than that.
So why would I even use this?
It seems easier to just pass an int32 * or a struct *Widget to a function and follow the convention that if it’s not nullptr, it’s safe. Everyone knows that an integer with the value -1 is not ready to be used. So we won’t have any issues…
It’s about clarity
Writing a function that receives or takes an optional as a parameter should give you pause. Instead of thinking about just handling an integer or struct you should now be thinking about how to handle this value and what you should do if it’s None.
Without getting all monadic about it, this is Haskell’s Maybe Type. But you don’t need to know type theory to use it. You only need to be thinking about handling both cases and what is the right thing to do each possiblity comes along. Types that imply branching. Neat.
Passing optionals suggests you will have a branch when you go to unwrap them because you need to unwrap them to access their values. This makes it clearer in your code that you are about to use a value you were given, and that you know that the value was initialized before you try to use it.
It’s about safety
Using optional types bakes in the idea of default type construction. But constructing a type that has no initial value means that your variant has nothing in it at all. So uninitialized Optionals default to None.
You don’t have to go back to your function and think what the initial value of present is, it’s None. You also don’t have to try to create a bogus initial value that you are “sure” is safe and everyone understands. It’s coded right into the type itself.
Unwrapping Optionals
The process of inspecting what is inside the optional is called unwrapping. Think of it like a present. You unwrap the optional and you got Some(present) or the box was empty and you got None. In the None case, since you haven’t got a type, you can’t pretend it’s a null pointer to an int32 and try to access it. There isn’t anything there to access.
Example in Java
Java 8 has some nice ways of dealing with optional values.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
public class Magician { | |
public Magician(){} | |
public Magician(String name){ | |
this.name = name; | |
} | |
public String getName() { | |
return name; | |
} | |
String name; | |
} | |
Magician magician = new Magician("Miracle Max"); | |
Optional<Magician> optMagician = Optional.ofNullable(magician); | |
if (optMagician.isPresent()) | |
return optMagician.get(); //returns a Miracle Max Magician | |
else | |
System.out.println("Miracle Max isn't home."); |
https://gist.github.com/BrantRoom3327/424d2ab1a6735db49f513f0a5ddb4fdd#file-optionals_2
The Optional.ofNullable() method takes a type and wraps it up in an optional. You can then see if the optional is set (isPresent()) and pull out the Magician value with the Optional.get() method. If it’s not present then you will just see that “Miracle Max isn’t home”
Example in Rust
Here’s a way to use some optionals in Rust:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
let waiting = Some("I'm Waiting for Vizzini."); | |
//Rust's match statement lets you figure out if you got Some or None | |
match waiting { | |
Some(c) => println!("{}", c), | |
None => println!("You got None"), | |
} | |
Prints "I'm Waiting for Vizzini." |
unwrap_or()
A neat little helper for cleanly unwrapping optionals inline in Rust works like this:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
assert_eq!(Some("Andre").unwrap_or("Giant"), "Andre"); // "Andre" | |
assert_eq!(None.unwrap_or("Vizzini"), "Vizzini"); // "Vizzini" | |
//from above | |
assert_eq!(waiting.unwrap_or("Nope"), "I'm Waiting for Vizzini."); // I'm Waiting for Vizzini. |
assert_eq()! just lets you confirm that the item on the left is the same as the item on the right. The interesting part here is chaining the optionals with unwrap_or() to give a default to an optional you are expecting to be in one state or another.
Wrap up
Optionals are useful in making your intentions clearer and safer to use, reducing confusion and simple programming errors. Give them a try on your next project. You might find it easier to reason about your program and spend less time debugging it.
Brant Rosenberger
Categorised as: Uncategorized
Comments are disabled on this post