Option<T> or T?? What is the point of having the Option type when dart has null safety (T?)?
Their purpose is the same: prevent access to a value that may be missing (null).
Why not removing Option from fpdart?
That's a legitimate question. Nonetheless, after much thinking (and writing, which is better to clarify ideas), I conclude that Option<T> and T? are not mutually exclusive.
This post highlights my thinking about this topic:
- Advantages of
T? - Advantages of
Option<T> - Comparison between
T?andOption<T> - How it would be possible to merge them together
- Why better to keep them separate
Introduction - Why we ended up with both Option and T??
Let's look back in history.
The Option type in fpdart was introduced on 10 June 2021.
Fun fact: originally
Optionwas calledMaybe(from Haskell), then based on feedback from the community it was renamed toOption
At the time Null safety was in its early stages. It was introduced in beta on 19 November 2020, and released in stable on 3 March 2021.
In the initial months after its release, null safety was not ubiquitous as it is today. Many packages did not support it, and many apps were in the process of migrating to it.
At the time, Option was an excellent alternative. What about now?
What makes null safety great
By default, T? is practically the same as T. What the ? adds is a check on possible null values:
int? nullable() => Random().nextBool() ? 10 : null;
...
int noNull = 10;
int? canBeNull = nullable();
final noNullIsEven = noNull.isEven; /// `bool`
// final canBeNullIsEven = canBeNull.isEven; βοΈ
final canBeNullIsEven = canBeNull?.isEven; /// `bool?`int can be used even when a function takes a int?. That's because int contains all the same values as int?. int? only counts one more value: null:
String takesNullable(int? nullInt) => "$nullInt";
...
int noNull = 10;
int? canBeNull = nullable();
takesNullable(canBeNull); /// βοΈ
takesNullable(noNull); /// βοΈBecause of this, it is possible to access the full API of int even from int?. You just need to add a ?. check when calling a function:
noNull.abs(); /// βοΈ
canBeNull?.abs(); /// βοΈ
?.is equivalent tocanBeNull != null ? canBeNull.abs() : null
Furthermore, there are also some other features that make working with nullable values easy:
/// Null-aware cascade
receiver?..method();
/// Null-aware index operator
receiver?[index];
/// Null-aware function call (Allowed with or without null safety)
function?.call(arg1, arg2);
/// Using null safety `!`
String toString() {
if (code == 200) return 'OK';
return 'ERROR $code ${error!.toUpperCase()}';
}
/// Using null safety `late`
class Coffee {
late String _temperature;
void heat() { _temperature = 'hot'; }
void chill() { _temperature = 'iced'; }
String serve() => _temperature + ' coffee';
}All of these operators makes working with int? extremely convenient.
Compare that with Option:
mapmethod to access the not-null value (instead of?.)
Option<int> optionInt = Option.of(10);
int? nullInt = nullable();
nullInt?.abs();
optionInt.map((t) => t.abs());
nullInt?.isEven;
optionInt.map((t) => t.isEven);- A function that takes
int?does not acceptOption<int>
String takesNullable(int? nullInt) => "$nullInt";
...
takesNullable(nullInt);
/// takesNullable(optionInt); βοΈ
takesNullable(optionInt.toNullable());- The type is not aware of checks calling
isSomeorisNone
String? strNullable = Random().nextBool() ? "string" : null;
Option<String> optionNullable = some("string");
if (optionNullable.isSome()) {
optionIntNullable; /// Still type `Option<int>`, not `Some<int>` π
}
if (strNullable != null) {
strNullable; /// This is now `String` π€
}Therefore, Option<T> and T? have a similar functionality, but T? has some language features specifically designed to make it less verbose.
You can read more about all the features designed for null safety in the official documentation
Advantages of the Option type
The main reason that makes Option preferable is chaining methods.
T? is simply a way to declare: "This value can be null".
Option instead is a full class containing a powerful API to compose functions together.
Option(orMaybe, orOptional) is a functional programming type. Functional programming languages useOptioninstead ofnull.Optionpromotes composability
Option gives you a declarative API to easily manipulate its value, regardless if the value is present or not (it's called Monad π»):
int doSomething(String str) => str.length + 10 * 2;
int doSomethingElse(int number) => number + 10 * 2;
...
/// Option has methods that makes it more powerful (chain methods) β
String? strNullable = Random().nextBool() ? "string" : null;
Option<String> optionNullable = some("string");
/// Declarative API: more readable and composable π
Option<double> optionIntNullable = optionNullable
.map(doSomething)
.alt(() => some(20))
.map(doSomethingElse)
.flatMap((t) => some(t / 2));
/// Not really clear what is going on here π€
double? intNullable = (strNullable != null
? doSomethingElse(doSomething(strNullable))
: doSomethingElse(20)) / 2;These convenience cannot be overlooked. Chaining method like this makes your code more readable, easy to maintain, and type-safe at the same time.
Extension methods: joining Option and T?
Why not adding the same powerful API of Option to T??
Dart has a feature called Extension methods.
This feature allows to add methods to any type, without declaring them inside their class.
This means we could add all the methods of Option to all nullable types. This would look like the following π:
/// `fpdart` extension to chain methods on nullable values `T?`
extension FpdartOnNullable<T> on T? {
B? map<B>(B Function(T t) f) {
final value = this;
return value == null ? null : f(value);
}
B match<B>(B Function() onNull, B Function(T t) onNotNull) {
final value = this;
return value == null ? onNull() : onNotNull(value);
}
...
}Now all nullable types have the power of the Option API!
Why extension methods don't work
Wait, there is a problem here π€. What is the result of this code below?
List<int>? list = Random().nextBool() ? [1, 2, 3, 4] : null;
list.map((e) => /** What type is `e`? π */ );List already has its own map method. Is the map in the example the List's map or the T? map?
You can see where this is going. A type T? can have any API.
T? is too generic! Furthermore, since T (not-nullable) is a subtype of T?, now also T has the full API of Option π€―.
Magically all the type in your codebase became
Optionπ§ββοΈ
Not ideal I would say: I think you would agree with me that this is not a good choice ππΌββοΈ.
Solution: bring T? inside Option
There may be a solution to this.
Instead of brining the full API of Option inside T?, why not bringing T? inside Option?
What I mean is making easier to move from T? to Option when necessary, while keeping T? in all other cases.
The proposal would be to extend the API to add more methods to jump back and forth from
T?toOptionand vice versa.
Something like the example below π:
/// `fpdart` extension to chain methods on nullable values `T?`
extension FpdartOnNullable<T> on T? {
Option<T> toOption() => Option.fromNullable(this);
Either<L, T> toEither<L>(L Function(T?) onNull) =>
Either.fromNullable(this, onNull);This extension allows to convert T? to Option when you need to chain methods. Then you can just as easily come back to T? from Option using toNullable.
Currently this is still an open discussion. Feel free to jump in on Twitter or on the fpdart repository to share any feedback, idea, or suggestion π―.
