option_result Test status codecov

option_result is a lightweight library with the goal of bringing Rust's Option and Result types to Dart.

This library aims to provide as close to a 1:1 experience in Dart as possible to Rust's implementation of these types, carrying over all of the methods for composing Option and Result values (and_then(), or_else(), map(), etc.) and allowing the use of Dart 3's new exhaustive pattern matching to provide a familiar experience while working with Option and Result type values.

This package is a work-in-progress.

Overview

Option

Option types represent the presence (Some) or absence (None) of a value.

Dart handles this pretty well on its own via null and a focus on null-safety built in to the compiler and analyzer, but we can do better.

The advantage of Option types over nullable types lies in their composability. Option type values have many methods that allow composing many Option-returning operations together and helpers for propagating None values in larger operations without the need for repetitive null-checking.

This supports writing clean, concise, and most importantly, safe code.

Option<int> multiplyBy5(int i) => Some(i * 5);
Option<int> divideBy2(int i) => switch (i) {
  0 => None(),
  _ => Some(i ~/ 2)
};

Option<int> a = Some(10);
Option<int> b = None();

Option<int> c = a.andThen(divideBy2).andThen(multiplyBy5); // Some(25)
Option<int> d = b.andThen(divideBy2).andThen(multiplyBy5); // None()

For nice, ergonomic safety, operations culminating in an Option that make use of other Option values in their logic where the outcome is dependent on those Option values can benefit from None value propagation:

// If user or email is None when unwrapped, the function will exit early, returning None
Option<String> getUserEmailLowerCase(int id) => ~() {
  Option<User> user = getUser(id);
  // Unwrap user here using ~. Can also be written as:
  // Option<String> email = user.unwrap().email;
  Option<String> email = (~user).email;

  return Some((~email).toLowerCase());
};

Option<String> email = getUserEmailLowerCase(12345);

switch (email) {
  case Some(value: String value): print('User email: $value');
  case None(): print('User does not have a valid email');
}

Result

Result types represent the result of some operation, either success (Ok), or failure (Err), and both variants can hold data.

This promotes safe handling of error values without the need for try/catch blocks while also providing composability like Option via methods for composing Result-returning operations together and helpers for propagating Err values within larger operations without the need for repetitive error catching, checking, and rethrowing.

Again, like Option, this helps promote clean, concise, and safe code.

Result<int, String> multiplyBy5(int i) => Ok(i * 5);
Result<int, String> divideBy2(int i) => switch (i) {
  0 => Err('divided by 0'),
  _ => Ok(i ~/ 2)
};

Result<int, String> a = Ok(10);
Result<int, String> b = Ok(0);
Result<int, String> c = Err('foo');

Result<int, String> d = a.andThen(divideBy2).andThen(multiplyBy5); // Ok(25)
Result<int, String> e = b.andThen(divideBy2).andThen(multiplyBy5); // Err('divided by 0')
Result<int, String> f = c.andThen(divideBy2).andThen(multiplyBy5); // Err('foo')

And, you guessed it, like Option, Result types can benefit from safe propagation of their Err values by making use of the same ergonomic syntax:

Result<String, String> getUserEmailLowerCase(int id) => ~() {
  Result<User, String> user = getUser(id);
  // Unwrap user here using ~. Can also be written as:
  // Result<String, String> email = user.unwrap().getEmail();
  Result<String, String> email = (~user).getEmail();

  return Ok((~email).toLowerCase());
};

Result<String, String> email = getUserEmailLowerCase(12345);

switch (email) {
  case Ok(value: String value): print('User email: $value');
  case Err(value: String err): print('Error fetching email: $err');
}

But Result doesn't always have to concern data. A Result can be used strictly for error handling, where an Ok simply means there was no error and you can safely continue. In Rust this is typically done by returning the unit type () as Result<(), E> and the same can be done in Dart with an empty Record via ().

Result<(), String> failableOperation() => Ok(());
Result<(), String> err = failableOperation();

if (err case Err(value: String error)) {
  print(error);
  return;
}

// No error, continue...

Note that just like how unit has one value in Rust, empty Record values in Dart are optimized to the same runtime constant reference so there is no performance or memory overhead when using () as a unit type.

Key differences from Rust

  • Option and Result types provided by this library are immutable. All composition methods either return new instances or the same instance unmodified if applicable, and methods for inserting/replacing values are not provided.

    The benefits of immutability speak for themselves, but this also allows compile-time const Option and Result values which can help improve application performance.

  • This library lacks all of the methods Rust's Option and Result types have that are related to ref, deref, mut, pin, clone, and copy due to not being applicable to Dart as a higher-level language.

  • The Option.filter() method has been renamed where() to be more Dart-idiomatic.

  • The Option and Result methods mapOr, mapOrElse return Option<U> and Result<U, E> respectively to aid composition of Option and Result values. The encapsulated values of these types should never leave the context of Option or Result unless explicitly unwrapped via the designated methods (unwrap(), expect(), etc.).

  • None()/Err() propagation is not supported at the language-level in Dart since there's no concept of it so it's not quite as ergonomic as Rust, but is still quite comfy and easily managed via the provided helpers.

Getting started

Add the dependency to your pubspec.yaml file in your Dart/Flutter project:

dependencies:
  option_result: ^1.0.0

Or via git:

dependencies:
  option_result:
    git: https://github.com/zajrik/option_result.git

Then run dart pub get or flutter pub get and import the library:

import 'package:option_result/option_result.dart';
// or import the separate types individually:
import 'package:option_result/option.dart';
import 'package:option_result/result.dart';

Basic Usage

// Assume getUser() returns some sort of User object
Result<User, String> user = await getUser(id: 12345);

if (user case Err(value: String error)) {
  print('Error retrieving user: $error');
  return;
}

// Assume the User object has an email field of type Option<String>
Option<String> email = user.unwrap().email;

if (email case Some(value: String address)) {
  print('User email: $address');
} else {
  print('User has no email set.');
}

// Alternative to the above using a switch expression for pattern matching
print(switch (email) {
  Some(value: String address) => 'User email: $address',
  None() => 'User has no email set.'
});

// Pattern matching with switch is exhaustive for Option and Result, so the compiler
// will give you warnings/errors to make sure you're providing cases for all potential
// values for Some()/Ok(), either directly or via a default case, and for None()/Err(),
// again either directly or via a default case

Similar packages

I started writing this library because there are many options (pun-intended) out there that accomplished similar goals but none of them stuck out to me at a glance as something that fit my needs. Pretty much all of them provided faux-pattern-matching via higher-order functions which I didn't care for, and I wanted to be able to make use of Dart 3's new exhaustive pattern matching which none of the libraries that I could find provided at the time of starting this project.

  • oxidized - Provides Option and Result types and is smiliarly close to a 1:1 representation of Rust's implementation as this library but with a much cooler name.

    • Supports Dart 3's exhaustive pattern matching as of v6.0.0. This feature was not available at the time of starting this project and probably would have prevented me from wanting to start it at all had it been 🤣

  • ruqe - Provides Option and Result types, as well as an Either type, which is like a Result type with extra steps.

  • either_option - Provides Option and Either types.

  • crab - Provides Option and Result types. Has a cool name.

  • dartz - The quintessential Dart functional programming library. Provides Option, Either, and so many other monadic types. Definitely worth taking a peek at if you like functional programming practices.

    With the addition of proper pattern matching and tuples in the form of Record in Dart 3, I foresee a major overhaul to Dartz in the near future. It might just get even cooler.

    Also has a cool name.

Libraries

option
This is the option library, containing only the Option type and its helpers.
option_result
This is the base option_result library, containing both Option and Result types and their helpers.
result
This is the result library, containing only the Result type and its helpers.