Pub Version Dart Package Docs License: MIT Build Status

rust (formally known as rust_core) is a pure Dart implementation of patterns found in the Rust programming language, bringing powerful tools previously only available to Rust developers to Dart developers!

New types include Result, Option, Cell, Slice, Array, Iterator, Channels, Mutex, Path and more.

See the Documentation Book 📖 for a deeper dive!

Example: Rust Language vs rust Package


Goal: Get the index of every "!" in a string not followed by a "?"

Rust:

use std::iter::Peekable;

fn main() {
  let string = "kl!sd!?!";
  let mut answer: Vec<usize> = Vec::new();
  let mut iter: Peekable<_> = string
      .chars()
      .map_windows(|w: &[char; 2]| *w)
      .enumerate()
      .peekable();

  while let Some((index, window)) = iter.next() {
      match window {
          ['!', '?'] => continue,
          ['!', _] => answer.push(index),
          [_, '!'] if iter.peek().is_none() => answer.push(index + 1),
          _ => continue,
      }
  }
  println!("{:?}", answer); // [2, 7]
}

Dart:

import 'package:rust/rust.dart';

void main() {
  final string = "kl!sd!?!";
  Vec<int> answer = [];
  Peekable<(int, Arr<String>)> iter = string
      .chars()
      .mapWindows(2, identity)
      .enumerate()
      .peekable();

  while (iter.moveNext()) {
    final (index, window) = iter.current;
    switch (window) {
      case ["!", "?"]:
        break;
      case ["!", _]:
        answer.push(index);
      case [_, "!"] when iter.peek() == null: // or `iter.peekOpt().isNone()`
        answer.push(index + 1);
    }
  }
  print(answer); // [2, 7]
}

Project Goals

rust's primary goal is to give Dart developers access to powerful tools previously only available to Rust developers.

To accomplish this, Rust's functionalities are carefully adapted to Dart's paradigms, focusing on a smooth idiomatic language-compatible integration. The result is developers now have a whole new toolset to tackle problems in Dart.

True to the Rust philosophy, rust strives to bring reliability and performance in every feature. Every feature is robustly tested. Over 500 meaningful test suites and counting.

Quickstart

Setup


Install

rust can be installed like any other Dart package.

Dart:

dart pub add rust

Flutter:

flutter pub add rust

or add directly to your pubspec.yaml:

dependencies:
  rust: <version>

General Notes


All of rust's classes and methods are well documented in the docs, but being an implementation of Rust, you can also refer to Rust docs if anything is unclear. The functionally is the same.

The Basics


Result and Option

Result<T, E> is a sum type used for returning and propagating errors - Ok and Err.

Option<T> represents a value that can be either some value of type T (Some<T>) or None. It is used "in place" of T? (implemented as a zero cost extension type of T?).

These types can be easily chained with other operations or pattern matched.

The Rust ? Operator and Early Return Key Notion

Result<T,E> and Option<T> both support early return key notation, which has the same function as the rust ? operator. It returns from the scope if an Err or None is encountered, otherwise it retrieves the inner value.

Result example:

Result<double, String> divideBy(int num, int divisor) => divisor == 0 ? Err("Divide by zero error") : Ok(num / divisor); 
Result<double, String> func(int x) => Result(($) { // Early Return Key
   // The function will return here
   int val = divideBy(x, 0)[$] + 10;
   return Ok(val);
 });

 void main(){
    assert(func(5) == Err("Divide by zero error"));
 }

List and Arr

Arr (array) is a compliment to List, representing a fixed sized List. Having a separate Arr type fixes runtime exceptions for trying to grow a non-growable List. It also has zero runtime cost, as it is an extension type of List and is more efficient than a growable List. With Arr, type intent is clear for maintainers and developers are able think about code performance more critically.

var array = Arr(null, 10);
array = Arr.constant(const [1,2,3,4,5]);
array = Arr.generate(10, (i) => i);
for(final entry in array){
    // do something
}
var (slice1, slice2) = array.splitSlice(3);

Iter

rust implements the entirety of Rust's stable and unstable Iterator methods. There are a lot of methods here that many Dart developers may not be familiar with. Definitely worth a look - docs

List<int> list = [1, 2, 3, 4, 5];
Iter<int> filtered = list.iter().filterMap((e) {
  if (e % 2 == 0) {
    return Some(e * 2);
  }
  return None;
});
expect(filtered, [4, 8]);

Iter functions the same as a Rust Iterator. For Dart developers, you can think of it as the union of Dart's Iterator and Iterable. check here for more info.

Slice

A Slice is a contiguous sequence of elements in a List or Arr. Slices are a view into a list without allocating and copying to a new list, thus slices are more efficient than creating a new List with .sublist() e.g. list.sublist(x,y).

var list = [1, 2, 3, 4, 5];
var slice = Slice(list, 1, 4); // or `list.slice(1,4)`
expect(slice, [2, 3, 4]);
var taken = slice.takeLast();
expect(taken, 4);
expect(slice, [2, 3]);
slice[1] = 10;
expect(list, [1, 2, 10, 4, 5]);

Slice also has a lot of efficient methods for in-place mutation within and between slices - docs

Whats Next?


Checkout any of the other sections in the book for more details and enjoy!

New To Rust


Welcome to Rust!

Maybe you have heard of Rust and want to see what all the hype is about, maybe you know a little Rust and want to improve your Rust while writing Dart, for whatever the reason, rust is here to help. Rust has a solid reputation for writing safe, maintainable, and performant code. rust brings the same tools and philosophy to Dart! rust is also a great start to learn and improve your Rust semantics/knowledge. You will write Dart and learn Rust along the way. With rust you can expect all the usual types you see in Rust. Here is a quick matrix comparing Rust, Dart, and Dart with rust:

Rust TypeDart EquivalentrustDescription
[T; N]const [...]/List<T>(growable: false)Arr<T>Fixed size array or list
Iterator<T>Iterator<T>/Iterable<T>Iter<T>Consumable iteration
Option<T>T?Option<T>A type that may hold a value or none
Result<T, E>-Result<T, E>Type used for returning and propagating errors
[T]-Slice<T>View into an array or list
Cell<T>-Cell<T>Value wrapper, useful for primitives
channel<T>-channel<T>Communication between produces and consumers
Mutex<T>-MutexExclusion primitive useful for protecting critical sections
RwLock<T>-RwLockExclusion primitive allowing multiple read operations and exclusive write operations
Path-PathType for file system path manipulation and interaction
Vec<T>List<T>Vec<T>Dynamic/Growable array

To learn more about the Rust programming language, checkout the Rust Book!

New To Dart


Welcome to Dart!

Dart is a great language choice for fast cross platform development and scripting. You'll find that rust is great start to learn Dart's semantics as you will feel like you are writing native rust. rust will introduce you to a few new types you may find useful as a Dart developer:

Rust TypeDart EquivalentrustDescription
[T; N]const [...]/List<T>(growable: false)Arr<T>Fixed size array or list
Iterator<T>Iterator<T>/Iterable<T>Iter<T>Consumable iteration
Option<T>T?Option<T>A type that may hold a value or none
Result<T, E>-Result<T, E>Type used for returning and propagating errors
[T]-Slice<T>View into an array or list
Cell<T>-Cell<T>Value wrapper, useful for primitives
channel<T>-channel<T>Communication between produces and consumers
Mutex<T>-MutexExclusion primitive useful for protecting critical sections
RwLock<T>-RwLockExclusion primitive allowing multiple read operations and exclusive write operations
Path-PathType for file system path manipulation and interaction
Vec<T>List<T>Vec<T>Dynamic/Growable array

To learn more about the Dart programming language, checkout dart.dev!

FAQ

Why Use The Rust Package Even If I Don't Know Rust Language?


From a language perspective we believe Dart is sadly lacking in a few areas, of which this package solves:

  • Dart utilizes unchecked try/catch exceptions. Handling errors as values is preferred for maintainability, thus the Result type.
  • Dart has nullable types but you cannot do null or non-null specific operations without a bunch of if statements and bugs may be introduced due to T?? not being possible. Option<T> fixes this with no runtime cost and you can easily switch back and forth.
  • Dart is missing the functionality of Rust's early return operator (?) operator, so we implemented it in Dart.
  • Dart is missing a built in Cell type or equivalent (and OnceCell/LazyCell).
  • Dart's List type is an array/vector union (it's growable or non-growable). This is not viewable at the type layer, which may lead to runtime exceptions and encourages using growable Lists everywhere even when you do not need to, which is less performant. So we added Arr (array).
  • Dart has no concept of a slice type, so allocating sub-lists is the only method, which is not that efficient. So we added Slice<T>.
  • Dart's between isolate communication is by ports (ReceivePort/SendPort), which is untyped and horrible, we standardized this with introducing channel for typed bi-directional isolate communication.
  • Dart's iteration methods are lacking for Iterable and Iterator (there are none! just moveNext() and current), while Rust has an abundance of useful methods. So we introduced Rust's Iterator as Iter.
  • Dart does not have built in path manipulation. So, we added Path, a zero cost extension type of String for path manipulation.
  • Dart's File/Directory/Link abstraction was the wrong choice and prone to exceptions. We believe Fs and Path are a stronger and safer alternative.
  • No built in cross-platform environment introspect - Platform does not work on web. So we added Env which is cross-platform.

I Know Rust, Can This Package Benefit My Team and I?


Absolutely! In fact, our team both programs in and love Dart and Rust. From a team and user perspective, having one common api across two different languages greatly increases our development velocity in a few ways:

  • Context switching is minimized, the api's across the two languages are the same.
  • Shrinking the knowledge gap between Rust and Dart developers.

Array


Arr (Array) is a zero cost extension type of List, where the List is treated as non-growable. This is useful for correctly handling lists where growable is false and const lists - as these types of lists are treated the same in the regular Dart type system, which may lead to errors. With Arr, type intent is clear for maintainers and developers are able think about code performance more critically.

Arr<int?> array = Arr(null, 10);

Arr's allocation will be more efficient than compared to a List since it does not reserve additional capacity and allocates the full amount eagerly. Which is important since allocations account for most of the cost of the runtime costs of a List.

Examples

Creating Arrays

You can create Arr instances in several ways:

From a Default Value

var array = Arr(null, 10); // Creates an array of 10 null values

From a Constant List

const array = Arr.constant(const [1, 2, 3, 4, 5]); // Creates an array from a constant list

Using a Generator Function

var array = Arr.generate(10, (i) => i); // Generates an array with values from 0 to 9

Creating a Range

var array = Arr.range(0, 10, step: 2); // Creates an array with values [0, 2, 4, 6, 8]

Accessing Elements

You can access and modify elements just like a normal list:

var entry = array[2]; // Accesses the element at index 2
array[2] = 10; // Sets the element at index 2 to 10

Iterating Over Elements

You can use a for-in loop to iterate over the elements:

for (final entry in array) {
  print(entry);
}

Converting to List

You can convert Arr back to a regular list (this will be erased at compile time so there is no cost):

var list = array.asList();

Splitting Arrays

You can split an array into two slices:

var (slice1, slice2) = array.splitSlice(3);

Cell


Cell is library of useful wrappers types (cells).

Cell - A wrapper with interior mutability.

OnceCell - A cell which can be written to only once.

LazyCell - A value which is initialized on the first access.

LazyCellAsync - A value which is asynchronously initialized on the first access.

Cell

A wrapper with interior mutability. Useful for primitives and an escape hatch for working with immutable data patterns. Cell<T> can be thought of as a List<T> with a single object.

void main() {
  Cell<int> cell = Cell(1);
  mutate(cell);
}

void mutate(Cell<int> cell){
  cell.set(2);
}

In a reactive context, such as with flutter, the conceptual equivalent is ValueNotifier.

Extensions exist for primitives. e.g. Cell<int> can be used similar to a normal int.

Cell<int> cell = Cell(10);
expect(cell.get(), 10);
cell.add(2);
expect(cell.get(), 12);
Cell<int> anotherCell = Cell(10);
Cell<int> newCell = cell + anotherCell;
expect(newCell, 22);
expect(cell, 12);
expect(antherCell, 10);

The base type for all Cells is ConstCell.

OnceCell

A cell which can be written to only once. Similar to late final <variable>, but will never throw an error.

OnceCell<int> cell = OnceCell();
var result = cell.set(10);
expect(result, const Ok(()));
result = cell.set(20);
expect(result, const Err(20));

The base type for all OnceCells is OnceCellNullable.

LazyCell

A value which is initialized on the first access.

int callCount = 0;
LazyCell<int> lazyCell = LazyCell(() {
  callCount++;
  return 20;
});
int firstCall = lazyCell();
expect(callCount, equals(1));
expect(firstCall, equals(20));
int secondCall = lazyCell();
expect(callCount, equals(1));
expect(secondCall, equals(20));

The base type for all LazyCells is LazyCellNullable.

LazyCellAsync

A value which is asynchronously initialized on the first access.

int callCount = 0;
LazyCellAsync<int> lazyCell = LazyCellAsync(() async {
  callCount++;
  return 20;
});
int firstCall = await lazyCell.force();
expect(callCount, equals(1));
expect(firstCall, equals(20));
int secondCall = lazyCell(); // Could also call `await lazyCell.force()` again.
expect(callCount, equals(1));
expect(secondCall, equals(20));

The base type for all LazyCellAsyncs is LazyCellNullableAsync.

Env

Env introduces Env for handling the environment. It works like Platform, except it is cross-platform (also works on web), since it is independent of dart:io, and has additional methods.

void main(){
    if(Env.isWeb) {
        print("On web, doing nothing.");
    }
    else if(Env.isLinux || Env.isMacOs) {
        Env.currentDirectory = "/";
        print("Moved current directory to root");
    }
    ...
}

Fs

Fs introduces Fs and OpenOptions. Fs is a container of static methods for working with the file system in a safe manner. Fs combines many of the functionalities in File/Directory/Link/FileStat/FileSystemEntity into one location and will never throw an exception. Instead of using instances of the previous entities, Fs works only on paths.

Result<(), IoError> = await Fs.createDir("path/to/dir".asPath());
// handle

rather than

try {
    await Directory("path/to/dir").create();
}
catch (e) {
// handle
}
// handle

OpenOptions is a more powerful builder pattern for opening files in place of File(..).open(mode)

OpenOptions options = OpenOptions()
    ..append(true)
    ..create(true)
    ..createNew(true)
    ..read(true)
    ..truncate(true)
    ..write(true)
RandomAccessFile randomAccessFile = options.openSync("path/to/file".asPath()).unwrap();

Iter


A Rust Iterator is analogous to the union of a Dart Iterable and Iterator. Since Dart already has an Iterator class, to avoid confusion, the Dart implementation of the Rust iterator is Iter. Iter makes working with collections of rust types and regular Dart types a breeze. e.g.

List<int> list = [1, 2, 3, 4, 5];
Iter<int> filtered = list.iter().filterMap((e) {
  if (e % 2 == 0) {
    return e * 2;
  }
  return null;
});
expect(filtered, [4, 8]);
// or
filtered = list.iter().filterMapOpt((e) {
  if (e % 2 == 0) {
    return Some(e * 2);
  }
  return None;
});
expect(filtered, [4, 8]);

Iter can be retrieved by calling iter() on an Iterable or an Iterator. Iter can be iterated like an Iterable or Iterator, and is consumed like an Iterator.

List<int> list = [1, 2, 3, 4, 5, 6, 7, 8, 9];
Iter<int> iter = list.iter();
List<int> collect = [];
for (final e in iter.take(5).map((e) => e * e)) {
  if (e.isEven) {
    collect.add(e);
  }
}
expect(collect, [4, 16]);
int? next = iter.next();
expect(next, 6);
collect.add(next.unwrap());
next = iter.next();
collect.add(next.unwrap());
expect(next, 7);
while(iter.moveNext()){
  collect.add(iter.current * iter.current);
}
expect(collect, [4, 16, 6, 7, 64, 81]);
expect(iter,[]);

Iter contains many more useful methods than the base Dart Iterable class and works in all places you would reach for an Iterator - pub.dev

Dart vs Rust Example

Goal: Get the index of every "!" in a string not followed by a "?"

import 'package:rust/rust.dart';

void main() {
  List<int> answer = [];
  String string = "kl!sd!?!";
  Peekable<(int index, Arr<String> window)> iter = string
      .chars()
      .mapWindows(2, identity)
      .enumerate()
      .peekable();
  while (iter.moveNext()) {
    final (int index, Arr<String> window) = iter.current;
    switch (window) {
      case ["!", "?"]:
        break;
      case ["!", _]:
        answer.add(iter.current.$1);
      case [_, "!"] when iter.peek().isNone():
        answer.add(index + 1);
    }
  }
  expect(answer, [2, 7]);
}

Rust equivalent

use std::iter::Peekable;

fn main() {
  let mut answer: Vec<usize> = Vec::new();
  let string = "kl!sd!?!";
  let mut iter: Peekable<_> = string
      .chars()
      .map_windows(|w: &[char; 2]| *w)
      .enumerate()
      .peekable();

  while let Some((index, window)) = iter.next() {
      match window {
          ['!', '?'] => continue,
          ['!', _] => answer.push(index),
          [_, '!'] if iter.peek().is_none() => answer.push(index + 1),
          _ => continue,
      }
  }
  assert_eq!(answer, [2, 7]);
}

Additional Examples

    /// Extract strings that are 3 long inside brackets '{' '}' and are not apart of other strings
    String string = "jfsdjf{abcdefgh}sda;fj";
    Iter<String> strings = string.runes
        .iter()
        .skipWhile((e) => e != "{".codeUnitAt(0))
        .skip(1)
        .arrayChunks(3)
        .takeWhile((e) => e[2] != "}".codeUnitAt(0))
        .map((e) => String.fromCharCodes(e));
    expect(strings, ["abc", "def"]);

Misc

Clone

Another a big advantage of Iter over Iterable<T> and Iterator<T> is that Iter<T> is clonable. This means the iterator can be cloned without cloning the underlying data.

    var list = [1, 2, 3, 4, 5];
    var iter1 = list.iter();
    iter1.moveNext();
    var iter2 = iter1.clone();
    iter2.moveNext();
    var iter3 = iter2.clone();
    iter3.moveNext();
    var iter4 = iter1.clone();
    expect(iter1.collectList(), [2, 3, 4, 5]);
    expect(iter2.collectList(), [3, 4, 5]);
    expect(iter3.collectList(), [4, 5]);
    expect(iter4.collectList(), [2, 3, 4, 5]);

This allows for situations where you want to work ahead and not lose your iterator position, or pass the Iter to another function without needing to call e.g. collectList(), collectArr(), etc.

Ops

Range

RangeBounds

RangeBounds works the same as Rust's RangeBounds (usually seen as syntactic sugar e.g. 1..=3) They have two uses:

  1. RangeBounds can be used to get a Slice of an Arr, Slice, or List.
void func(RangeBounds bounds) {
    List<int> list = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9];
    Slice<int> slice = list(bounds);
    expect(slice, equals([4, 5, 6, 7, 8, 9]));
}

func(RangeFrom(4));

The variants are Range, RangeFrom, RangeFull, RangeInclusive, RangeTo, and RangeToInclusive.

  1. RangeBounds that also implement IterableRangeBounds can be iterated over as a generator.
for(int x in Range(5, 10)){ // 5, 6, 7, 8, 9
    // code
}

All RangeBounds can be const.

range Function

range is a convenience function that works the same as the python range function. As opposed to the RangeBounds classes, the range function can have negative ranges and varying step sizes.

range(end); // == range(0,end);
range(start, end);
range(start, end, step);

for(final x in range(0, 10, 1)){}
// equivalent to
for(final x in range(10)){}

Note Arr.range(..) also exists as a more efficient method for when it is known collecting the range is needed.

Option


Option represents the union of two types - Some<T> and None.

Option is easy to declare and translate back and forth between nullable types.

Option<int> option = None;
Option<int> option = Some(1);

int? nullable = option.toNullable();
option = Option.of(nullable);

Usage

The Option type and features work very similar to Result. We are able to chain operations in a safe way without needing a bunch of if statements to check if a value is null.

Option<int> intOptionFunc() => None;
double halfVal(int val) => val/2;
Option<double> val = intOptionFunc()
    .map(halfVal);
expect(val.unwrapOr(2), 2);

See the docs for all methods and extensions.

You can also use Option in pattern matching

switch(Some(2)){
  case Some(:final v):
    // do something
  case _:
    // do something
}

or

final x = switch(Some(2)){
  Some(:final v) => "some"
  _ => "none"
}

Early Return Key Notation

Option also supports "Early Return Key Notation" (ERKN), which is a derivative of "Do Notation". It allows a function to return early if the value is None, and otherwise safely access the inner value directly without needing to unwrap or type check.

Option<int> intNone() => None;
Option<double> earlyReturn(int val) => Option(($) { // Early Return Key
  // Returns here
  double x = intNone()[$].toDouble();
  return Some(val + x);
});
expect(earlyReturn(2), None);

This is a powerful concept and make you code much more concise without losing any safety.

For async, use Option.async e.g.

FutureOption<double> earlyReturn() => Option.async(($) async {
  ...
});

Discussion

If Dart Has Nullable Types Why Ever Use Option?

Option is wrapper around a value that may or may be set. A nullable type is a type that may or may not be set. This small distinction leads to some useful differences:

  • Any extension method on T? also exists for T. So null specific extensions cannot be added. Also since T is all types, there would be a lot of clashes with existing types if you tried to do so - e.g. map on Iterable. While Option plays well for a pipeline style of programming.

  • T?? is not possible, while Option<Option> or Option<T?> is. This may be useful, e.g.

    No value at all (None): The configuration value isn't defined at all.

    A known absence of a value (Some(None)): The configuration value is explicitly disabled.

    A present value (Some(Some(value))): The configuration value is explicitly set to value.

    With nullable types, a separate field or enum/sealed class would be needed to keep track of this.

  • Correctness of code and reducing bugs. As to why, e.g. consider nth which returns the nth index of an iterable or null if the iterable does not have an nth index. If the iterable is Iterable<T?>, then a null value from calling nth means the nth element is either null or the iterable does not have n elements. While if nth rather returned Option, if the nth index is null it returns Some(null) and if it does not have n elements it returns None. One might accidentally mishandle the nullable case and assume the nth index does not actually exist, when it is rather just null. While the second case with Option one is force to handle both cases. This holds true for a lot of operations that might have unintended effects e.g. filterMap - since null can be a valid state that should not be filtered.

These issues are not insurmountable, and in fact, most of the time nullable types are probably more concise and easier to deal with. Therefore, for every method in this library that uses T? there is also an Option version, usually suffixed with ..Opt.

In some languages (like Rust, not Dart) Option can be passed around like a reference and values can be taken in and out of (transmutation). Thus visible to all with reference to the Option, unlike null. Implementing such an equivalence in Dart would remove pattern matching and const-ness.

Why Not To Use Option

  • Null chaining operations with ? is not possible with Option

  • Currently in Dart, one cannot rebind variables and Option does not support type promotion like nullable types. This makes using Option less ergonomic in some scenarios.

Option<int> xOpt = optionFunc();
int x;
switch(xOpt) {
  case Some(:final v):
    x = v;
  case _:
    return;
}
// use `int` x

vs

int? x = nullableFunc();
if(x == null){
  return;
}
// use `int` x

Fortunately, it can be converted back and forth.

int? x = optionFunc().toNullable();
if(x == null){
  return;
}
// use `int` x

Conclusion

The choice to use Option is up to the developer. You can easily use this package and never use Option.

Path

This library introduces path types for working with file paths in a structured and type-safe manner, supporting Unix (POSIX) and Windows file systems. All Path types are extension types of string, so they are zero runtime cost.

Types

Path - A platform dependent path type - uses WindowPath on windows and UnixPath on all other platforms.

UnixPath - A Unix path type.

WindowsPath - A Windows path type.

Basic Operations

Create a path and perform basic operations:

import 'package:rust/rust.dart';

void main() {
  var path = UnixPath('/foo/bar/baz.txt');

  print('File name: ${path.fileName()}'); // Output: baz.txt
  print('Extension: ${path.extension()}'); // Output: txt
  print('Is absolute: ${path.isAbsolute()}'); // Output: true

  var parent = path.parent();
  if (parent != null) {
    print('Parent: $parent'); // Output: /foo/bar
  }

  var newPath = path.withExtension('md');
  print('New path with extension: $newPath'); // Output: /foo/bar/baz.md
}

Extracting Components

Get the components of a path:

void main() {
  var path = UnixPath('/foo/bar/baz.txt');
  var components = path.components().toList();

  for (var component in components) {
    print(component); // Output: /, foo, bar, baz.txt
  }
}

Ancestors

Retrieve all ancestors of a path:

void main() {
  var path = UnixPath('/foo/bar/baz.txt');

  for (var ancestor in path.ancestors()) {
    print(ancestor);
    // Output:
    // /foo/bar/baz.txt
    // /foo/bar
    // /foo
    // /
  }
}

File System Interaction

Check if a path exists and get metadata:

void main() {
  var path = UnixPath('/foo/bar/baz.txt');

  if (path.existsSync()) {
    var metadata = path.metadataSync();
    print('File size: ${metadata.size}');
    print('Last modified: ${metadata.modified}');
  } else {
    print('Path does not exist.');
  }
}

Panic


As with Error in Dart Core, Panic represents a state that should never happen and thus should never be caught. Rust vs Dart Error handling terminology:

Dart Exception TypeEquivalent in Rust
ExceptionError
ErrorPanic
Result x = Err(1);
if (x.isErr()) {
  return x.unwrap(); // this will throw a Panic (should be "unwrapErr()")
}

panic can also be called directly with

throw Panic("Panic message here.");
// or 
panic("Panic message here.");

rust was designed with safety in mind. The only time rust will ever throw is if you unwrap incorrectly (as above), in this case a Panic's can be thrown. But the good news is you can usually avoid using these methods. See How to Never Unwrap Incorrectly section to avoid ever using unwrap.

Unreachable

Unreachable is shorthand for a Panic where the compiler can’t determine that some code is unreachable.

throw Unreachable();
// or
unreachable();

Handling Panic

For the most part, a panic is meant to abort your program. Thus one should only try to handle panics sparingly, likely only at the root. Use panicHandler or panicHandlerAsync if this is desired.

Result


Result<T, E> is the type used for returning and propagating errors. It is an alternative to throwing exceptions. It is a sealed type with the variants, Ok(T), representing success and containing a value, and Err(E), representing error and containing an error value.

To better understand the motivation around the Result type refer to this article.

Example


By using the Result type, there is no web of try/catch statements to maintain and hidden control flow bugs, all control flow is defined.

import 'package:rust/result.dart';

void main() {
  final result = processOrder("Bob", 2);
  switch(result) {
    Ok(v:var okay) => print("Success: $okay"),
    Err(v:var error) => print("Error: $error"),
  };
}

Result<String, String> processOrder(String user, int orderNumber) {
  final result = makeFood(orderNumber);
  if(result case Ok(v:final order)) {
    final paymentResult = processPayment(user);
    if(paymentResult case Ok(v:final payment)) {
      return Ok("Order of $order is complete for $user. Payment: $payment");
    }
    return paymentResult;
  }
  return result;
}

Result<String, String> makeFood(int orderNumber) {
  if (orderNumber == 1) {
    return makeHamburger();
  } else if (orderNumber == 2) {
    return makePizza();
  }
  return Err("Invalid order number.");
}

Result<String,String> makeHamburger() {
  return Err("Failed to make the hamburger.");
}

Result<String,String> makePizza() {
  return Ok("pizza");
}

Result<String,String> processPayment(String user) {
  if (user == "Bob") {
    return Ok("Payment successful.");
  }
  return Err("Payment failed.");
}

Chaining


Effects on a Result can be chained in a safe way without needing to inspect the type.

Result<int,String> result = Ok(4);
Result<String, String> finalResult = initialResult
    .map((okValue) => okValue * 2) // Doubling the `Ok` value if present.
    .andThen((okValue) => okValue != 0 ? Ok(10 ~/ okValue) : Err('Division by zero')) // Potentially failing operation.
    .map((okValue) => 'Result is $okValue') // Transforming the successful result into a string.
    .mapErr((errValue) => 'Error: $errValue'); // Transforming any potential error.

// Handling the final `Result`:
finalResult.match(
  ok: (value) => print('Success: $value'),
  err: (error) => print('Failure: $error'),
);

See the docs for all methods and extensions.

Adding Predictable Control Flow To Legacy Dart Code


At times, you may need to integrate with legacy code that may throw or code outside your project. To handle these situations you can just wrap the code in a helper function like guard

void main() {
  Result<int,Object> result = guard(functionWillThrow).mapErr((e) => "$e but was guarded");
  print(result);
}

int functionWillThrow() {
  throw "this message was thrown";
}

Output:

this message was thrown but was guarded

Dart Equivalent To The Rust "?" Early Return Operator


In Dart, the Rust "?" operator (Early Return Operator) functionality in x?, where x is a Result, can be accomplished in two ways

into()

if (x case Err()) {
  return x.into(); // may not need "into()"
}

into may be needed to change the S type of Result<S,F> for x to that of the functions return type if they are different. into only exits if after the type check, so you will never mishandle a type change since the compiler will stop you. Note: There also exists intoUnchecked that does not require implicit cast of a Result Type.

Early Return Key Notation

"Early Return Key Notation" is a take on "Do Notation" and the "Early Return Key" functions the same way as the "Early Return Operator". The Early Return Key is typically denoted with $ and when passed to a Result it unlocks the inner value, or returns to the surrounding context. e.g.

Result<int, String> innerErrFn() => Err("message");
Result<int, String> earlyReturn() => Result(($) { // Early Return Key
   int y = 2;
   // The function will return here
   int x = innerErrFn()[$];
   return Ok(x + y);
 });

expect(earlyReturn().unwrapErr(), "message");

Using the Early Return Key Notation reduces the need for pattern matching or checking, in a safe way. This is quite a powerful tool. See here for another example.

For async, use the Result.async e.g.

FutureResult<int, String> earlyReturn() => Result.async(($) async {
  ...
});

Tips and Best Practices

How to Never Unwrap Incorrectly


In Rust, as here, it is possible to unwrap values that should not be unwrapped:

if (x.isErr()) {
  return x.unwrap(); // this will panic (should be "unwrapErr()")
}

There are four ways to never unwrap incorrectly:

Pattern Matching

Simple do a type check with is or case instead of isErr().

if (x case Err(v:final error)){
    return error;
}

and vice versa

if (x case Ok(v:final okay)){
    return okay;
}

The type check does an implicit cast, and we now have access to the immutable error and ok value respectively.

Switch

Similarly, we can mimic Rust's match keyword, with Dart's switch

switch(x){
 case Ok(:final v):
   print(v);
 case Err(:final v):
   print(v);
}

final y = switch(x){
  Ok(:final v) => v,
  Err(:final v) => v,
};

Declaratively

Or declaratively with match or mapOrElse

x.match(
  ok: (ok) => ok,
  err: (err) => err
);

Early Return Key Notation

We can also use the Early Return Key Notation, which is a very powerful idiomatic approach.

Working with Futures


When working with Futures it is easy to make a mistake like this

Future.delayed(Duration(seconds: 1)); // Future not awaited

Where the future is not awaited. With Result's (Or any wrapped type) it is possible to make this mistake

await Ok(1).map((n) async => await Future.delayed(Duration(seconds: n))); // Outer "await" has no effect

The outer "await" has no effect since the value's type is Result<Future<void>> not Future<Result<void>>. To address this use toFutureResult()

await Ok(1).map((n) async => await Future.delayed(Duration(seconds: n))).toFutureResult(); // Works as expected

To avoid these issues all together in regular Dart and with wrapped types like Result, it is recommended to enable these Future linting rules in analysis_options.yaml

linter:
  rules:
    unawaited_futures: true # Future results in async function bodies must be awaited or marked unawaited using dart:async
    await_only_futures: true # "await" should only be used on Futures
    avoid_void_async: true # Avoid async functions that return void. (they should return Future<void>)

analyzer:
  errors:
    unawaited_futures: error
    await_only_futures: error
    avoid_void_async: error

ToResult and ToResultEager


In various circumstances you may have multiple Results and just want a single Result. For these times, think toResult() or in some cases toResultEager(). These extension methods have been added to make life easier.

Iterable Result

One of these situations is when you have a Iterable<Result<S,F>>, which can turn into a Result<List<S>,List<F>>. Also, there is .toResultEager() which can turn into a single Result<List<S>,F>.

var result = [Ok(1), Ok(2), Ok(3)].toResult();
expect(result.unwrap(), [1, 2, 3]);

result = [Ok<int,int>(1), Err<int,int>(2), Ok<int,int>(3)].toResultEager();
expect(result.unwrapErr(), 2);

Multiple Results of Different Success Types

Sometimes you need to call multiple functions that return Results of different types. You could write something like this:

final a, b, c;
final boolResult = boolOk();
switch(boolResult){
  case Ok(:final v):
    a = v;
  case Err():
    return boolResult;
}
final intResult = intOk();
switch(intResult){
  case Ok(:final v):
    b = v;
  case Err():
      return intResult;
}
final doubleResult = doubleOk();
switch(doubleResult){
    case Ok(:final v):
        c = v;
    case Err():
        return doubleResult;
}
/// ... Use a,b,c

That is a little verbose. Fortunately, extensions to the recuse, instead do:

final a, b, c;
final result = (boolOk, intOk, doubleOk).toResultEager();
switch(result){
   case Ok(:final v):
      (a, b, c) = v;
   case Err():
      return result;
}
/// ... Use a,b,c

This also has a toResult() method.

Pattern Matching vs Early Return Key


void main(){
  usingTheEarlyReturnKey();
  usingRegularPatternMatching();
}

Result<int,String> usingTheEarlyReturnKey() => Result(($){ // Early Return Key
  // Will return here with 'Err("error")'
  int x = willAlwaysReturnErr()[$].toInt();
  return Ok(x);
});

Result<int,String> usingRegularPatternMatching(){
  int x;
  switch(willAlwaysReturnErr()){
    case Err(:final v):
      return Err(v);
    case Ok(:final v):
      x = v.toInt();
  }
  return Ok(x);
}

Result<double,String> willAlwaysReturnErr() => Err("error");

Slice


A Slice is a contiguous sequence of elements in a List. Slices are a view into a list without allocating and copying to a new list, thus slices are more efficient than creating a sub-list, but they do not own their own data. That means shrinking the original list can cause the slices range to become invalid, which may cause an exception.

Slice also has a lot of efficient methods for in-place mutation within and between slices. e.g.

    var list = [1, 2, 3, 4, 5];
    var slice = Slice(list, 1, 4);
    slice = list.slice(1,4); // alternative
    expect(slice, [2, 3, 4]);
    var taken = slice.takeLast();
    expect(taken, 4);
    expect(slice, [2, 3]);
    slice[1] = 10;
    expect(list, [1, 2, 10, 4, 5]);

Examples

Array Windows

Create overlapping windows of a specified size from a list:

import 'package:rust/slice.dart';

void main() {
  var list = [1, 2, 3, 4, 5];
  var slice = Slice(list, 0, 5);
  var windows = slice.arrayWindows(2);
  print(windows); // [[1, 2], [2, 3], [3, 4], [4, 5]]
}

Chunking

Split a list into chunks of a specified size:

import 'package:rust/slice.dart';

void main() {
  var list = [1, 2, 3, 4, 5];
  var slice = list.slice();
  var (chunks, remainder) = slice.asChunks(2);
  print(chunks); // [[1, 2], [3, 4]]
  print(remainder); // [5]
}

Perform binary search on a sorted list:

import 'package:rust/slice.dart';

void main() {
  Slice<num> s = [0, 1, 1, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55].slice();
  var result = s.binarySearch(13);
  print(result); // Ok(9)
}

Rotations

Rotate elements to the left or right:

import 'package:rust/slice.dart';

void main() {
  var list = ['a', 'b', 'c', 'd', 'e', 'f'];
  var slice = list.asSlice();
  slice.rotateLeft(2);
  print(slice); // ['c', 'd', 'e', 'f', 'a', 'b']

  slice.rotateRight(2);
  print(slice); // ['e', 'f', 'a', 'b', 'c', 'd']
}

Deduplication

Remove duplicate elements based on a custom equality function:

import 'package:rust/slice.dart';

void main() {
  var list = ["foo", "Foo", "BAZ", "Bar", "bar", "baz", "BAZ"];
  var slice = list.asSlice();
  var (dedup, duplicates) = slice.partitionDedupBy((a, b) => a.toLowerCase() == b.toLowerCase());
  print(dedup); // ["foo", "BAZ", "Bar"]
  print(duplicates); // ["Foo", "bar", "baz"]
}

Sorting

Sort elements in-place:

import 'package:rust/slice.dart';

void main() {
  var list = [5, 4, 3, 2, 1];
  var slice = list.asSlice();
  slice.sortUnstable();
  print(slice); // [1, 2, 3, 4, 5]
}

Reversing

Reverse the elements in a list:

import 'package:rust/slice.dart';

void main() {
  var list = [1, 2, 3, 4, 5];
  var slice = list.asSlice();
  slice.reverse();
  print(slice); // [5, 4, 3, 2, 1]
}

Partitioning

Partition elements into those that satisfy a predicate and those that don't:

import 'package:rust/slice.dart';

void main() {
  var list = [1, 2, 2, 3, 3, 2, 1, 1];
  var slice = list.asSlice();
  var (dedup, duplicates) = slice.partitionDedup();
  print(dedup); // [1, 2, 3, 2, 1]
  print(duplicates); // [2, 3, 1]
}

Copying and Moving

Copy elements within a list or to another list:

import 'package:rust/slice.dart';

void main() {
  var srcList = [1, 2, 3, 4, 5];
  var dstList = [6, 7, 8, 9, 10];
  var src = Slice(srcList, 0, 5);
  var dst = Slice(dstList, 0, 5);
  dst.copyFromSlice(src);
  print(dstList); // [1, 2, 3, 4, 5]
}

Sync


sync contains useful synchronization utilities.

Dart being single threaded, means it is less common to need a synchronization construct like Mutex, but they are still certain scenarios where they may be needed.

channel

rust supports two types of channels, "local" channels (same isolate) and "isolate" channels (different isolates).

Local Channels


localChannel is used for communication between produces and consumers on the same isolate. localChannel is similar to StreamController except it buffers data until read and will never throw. In more detail, localChannel returns a Sender and Receiver. Each item T sent by the Sender will only be seen once by the Receiver. Even if the Sender calls close while the Receiver's buffer is not empty, the Receiver will still yield the remaining items in the buffer until empty.

Examples

Single Sender, Single Receiver

import 'package:rust/sync.dart';

void main() async {
  final (tx, rx) = localChannel<int>();

  // Sender sends data
  tx.send(1);
  tx.send(2);
  tx.send(3);

  // Receiver retrieves data
  for (int i = 0; i < 3; i++) {
    print(await rx.recv()); // Outputs: 1, 2, 3
  }
}

Receiver with Timeout

import 'package:rust/sync.dart';

void main() async {
  final (tx, rx) = localChannel<int>();

  // Sender sends data
  tx.send(1);

  // Receiver retrieves data with a timeout
  final result = await rx.recvTimeout(Duration(milliseconds: 100));
  if (result.isOk()) {
    print(result.unwrap()); // Outputs: 1
  } else {
    print("Timeout"); // If timeout occurs
  }
}

Receiver with Error Handling

import 'package:rust/sync.dart';

void main() async {
  final (tx, rx) = localChannel<int>();

  // Sender sends data and then an error
  tx.send(1);
  tx.send(2);
  tx.sendError(Exception("Test error"));

  // Receiver retrieves data and handles errors
  for (int i = 0; i < 3; i++) {
    final result = await rx.recv();
    if (result.isOk()) {
      print(result.unwrap()); // Outputs: 1, 2
    } else {
      print("Error: ${result.unwrapErr()}"); // Handles error
    }
  }
}

Iterating Over Received Data

import 'package:rust/sync.dart';

void main() async {
  final (tx, rx) = localChannel<int>();

  // Sender sends data
  tx.send(1);
  tx.send(2);
  tx.send(3);

  // Receiver iterates over the data
  final iterator = rx.iter();
  for (final value in iterator) {
    print(value); // Outputs: 1, 2, 3
  }
}

Using Receiver as a Stream

import 'package:rust/sync.dart';

void main() async {
  final (tx, rx) = localChannel<int>();

  // Sender sends data
  tx.send(1);
  tx.send(2);
  tx.send(3);

  // Close the sender after some delay
  () async {
    await Future.delayed(Duration(seconds: 1));
    tx.close();
  }();

  // Receiver processes the stream of data
  await for (final value in rx.stream()) {
    print(value); // Outputs: 1, 2, 3
  }
}

Isolate Channels


isolateChannel is used for bi-directional isolate communication. The returned Sender and Receiver can communicate with the spawned isolate and the spawned isolate is passed a Sender and Receiver to communicate with the original isolate. Each item T sent by the Sender will only be seen once by the Receiver. Even if the Sender calls close while the Receiver's buffer is not empty, the Receiver will still yield the remaining items in the buffer until empty. Types that can be sent over a SendPort, as defined here, are allow to be sent between isolates. Otherwise a toSpawnedCodec and/or a fromSpawnedCodec can be passed to encode and decode the messages.

Note: Dart does not support isolates on web. Therefore, if your compilation target is web, you cannot use isolateChannel.

Examples

Simple String Communication

void main() async {
  final (tx, rx) = await isolateChannel<String, String>((tx, rx) async {
    assert((await rx.recv()).unwrap() == "hello");
    tx.send("hi");
  });

  tx.send("hello");
  expect((await rx.recv()).unwrap(), "hi");
}

Explicitly Defined Codecs for Communication

void main() async {
  final (tx, rx) = await isolateChannel<String, int>((tx, rx) async {
    assert((await rx.recv()).unwrap() == "hello");
    tx.send(1);
  }, toSpawnedCodec: const StringCodec(), fromSpawnedCodec: const IntCodec());

  tx.send("hello");
  expect((await rx.recv()).unwrap(), 1);
}

RwLock


RwLock is used in critical sections to allow multiple concurrent read operations while ensuring that write operations are exclusive. RwLock uses a fifo model to prevent starvation. e.g. Reading and writing to data sources or transactions -

import 'dart:async';

class DataSource {
  final String name;
  DataSource(this.name);

  Future<String> read() async {
    print('Reading from $name');
    await Future.delayed(Duration(milliseconds: 100)); // Simulate delay
    return 'Data from $name';
  }

  Future<void> write(String data) async {
    print('Writing to $name: $data');
    await Future.delayed(Duration(milliseconds: 100)); // Simulate delay
  }
}

class DataManager {
  final List<DataSource> _dataSources;
  final RwLock _rwLock = RwLock();

  DataManager(this._dataSources);

  Future<List<String>> readFromAll() async {
    return await _rwLock.withReadLock(() async {
      final results = <String>[];
      for (var dataSource in _dataSources) {
        results.add(await dataSource.read());
      }
      return results;
    });
  }

  Future<void> writeToAll(String data) async {
    await _rwLock.withWriteLock(() async {
      for (var dataSource in _dataSources) {
        await dataSource.write(data);
      }
    });
  }
}

void main() async {
  final dataSources = [DataSource('DB1'), DataSource('DB2'), DataSource('DB3')];
  final manager = DataManager(dataSources);

  await Future.wait([
    manager.readFromAll().then((data) => print('Read: $data')),
    manager.readFromAll().then((data) => print('Read: $data')),
    manager.writeToAll('New Data'),
  ]);

  print('All operations completed');
}

Mutex


Mutex is used to ensure that only one task can perform a critical section of code at a time. Mutex uses a fifo model to prevent starvation. e.g. Transactions -

class Account {
  int balance;

  Account(this.balance);
}

final mutex = Mutex();

Future<Result<(),Exception>> transfer(Account from, Account to, int amount) async {
  await mutex.withLock(() async {
    final fromBalance = from.balance;
    final toBalance = to.balance;

    if (fromBalance < amount) {
      return Err(Exception('Insufficient funds for transfer.'));
    }

    // Simulate some processing delay
    await Future.delayed(Duration(milliseconds: 100));

    from.balance = fromBalance - amount;
    to.balance = toBalance + amount;

    return Ok(());
  });
}

void main() async {
  final account1 = Account(100);
  final account2 = Account(50);

  await Future.wait([
    transfer(account1, account2, 30),
    transfer(account2, account1, 20),
  ]);
}

KeyedMutex


KeyedMutex is used to ensure that only one task can perform a critical section of code at a time, where the critical section of code is associated with an key. KeyedMutex uses a fifo model to prevent starvation. e.g. Async IO operations -

final keyedMutex = KeyedMutex();

Future<void> deleteFileIfExists(File file) async {
  return keyedMutex.withLock(file.absolute, () async {
    if (await file.exists()) {
      await file.delete();
    }
  });
}

The alternative to not using KeyedMutex in the above code would be to use the synchronous version of th IO api's or risk a scenario like below which would otherwise likely cause an exception.

deleteFileIfExists(File("path/to/file"));
deleteFileIfExists(File("path/to/file"));

Vec

Vec adds additional extension methods for List and adds a Vec type - a typedef of List. Vec is used to specifically denote a contiguous growable array, unlike List which is growable or non-growable, which may cause runtime exceptions. Vec being a typedef of List means it can be used completely interchangeably.

Vec<int> vec = [1, 2, 3, 4];
// easily translate back and forth
List<int> list = vec;
vec = list;

Vec is a nice compliment to Arr (array) type.

Usage

import 'package:rust/rust.dart';

void main() {
  Vec<int> vec = [1, 2, 3, 4];

  Vec<int> replaced = vec.splice(1, 3, [5, 6]);
  print(replaced); // [2, 3]
  print(vec); // [1, 5, 6, 4]

  vec.push(5);
  vec.insert(2, 99);

  int removed = vec.removeAt(1);
  print(removed); // 5
  print(vec); // [1, 99, 6, 4, 5]

  vec.resize(10, 0);
  print(vec); // [1, 99, 6, 4, 5, 0, 0, 0, 0, 0]

  Iter<int> iter = vec.extractIf((element) => element % 2 == 0);
  Vec<int> extracted = iter.collectVec();
  print(extracted); // [6, 4, 0, 0, 0, 0, 0]
  print(vec); // [1, 99, 5]

  Slice<int> slice = vec.asSlice();
}

Packages Built On Rust

Official

LibraryDescription
anyhowIdiomatic error handling capabilities to make your code safer, more maintainable, and errors easier to debug.
tapperExtension methods on all types that allow transparent, temporary, inspection/mutation (tapping), transformation (piping), or type conversion.

Community

LibraryDescription
rewindA logging utility that allows you completely customize what is captured and how your logs look at each logging level.

Tips

Null and Unit

In Dart, void is used to indicate that a function doesn't return anything or a type should not be used, as such:

Result<void, void> x = Ok(1); // valid
Result<void, void> y = Err(1); // valid
int z = x.unwrap(); // not valid 

Since stricter types are preferred and Err cannot be null, use () aka "Unit":