Pub Version Dart Package Docs License: MIT Build Status

rust_core is a pure Dart implementation of patterns found in Rust's core library, bringing the power of Rust to Dart!

Types include Result, Option, Cell, Slice, Array, Iterator, Channels, Mutex, and more.

See the Rust Core Book 📖

Example

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,
      }
  }
  assert_eq!(answer, [2, 7]);
}

Dart:

import 'package:rust_core/rust_core.dart';

void main() {
  String string = "kl!sd!?!";
  List<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.add(index);
      case [_, "!"] when iter.peek().isNone():
        answer.add(index + 1);
    }
  }
  expect(answer, [2, 7]);
}

Project Goals

rust_core'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_core 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_core can be installed like any other Dart package.

Dart:

dart pub add rust_core

Flutter:

flutter pub add rust_core

or add directly to your pubspec.yaml:

dependencies:
  rust_core: <version>

Imports

rust_core follows the same library structure and naming as Rust's core library.

To that extent, each library can be imported individually

import 'package:rust_core/result.dart';

or all libraries

import 'package:rust_core/rust_core.dart';

General Notes


All of rust_core's classes and methods are well documented in the docs, but being an implementation of Rust's core library, you can also refer to Rust core 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_core 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_core is here to help. Rust has a solid reputation for writing safe, maintainable, and performant code. rust_core brings the same tools and philosophy to Dart! rust_core 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_core you can expect all the usual types you see in Rust. Here is a quick matrix comparing Rust, Dart, and Dart with rust_core:

Rust TypeDart Equivalentrust_coreDescription
[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-Path*Type for file system path manipulation and interaction
Vec<T>List<T>Vec<T>Dynamic/Growable array

*: Implemented through additional packages found here

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_core is great start to learn Dart's semantics as you will feel like you are writing native rust. rust_core will introduce you to a few new types you may find useful as a Dart developer:

Rust TypeDart Equivalentrust_coreDescription
[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-Path*Type for file system path manipulation and interaction
Vec<T>List<T>Vec<T>Dynamic/Growable array

*: Implemented through additional packages found here

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

FAQ

Why Use Rust Core Even If I Don't Know Rust?


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. Option<T> fixes this with no runtime cost and you can easily switch back and forth to nullable types since it is just a zero cost extension type of T?.
  • Dart is missing the functionality of Rust's ? 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.

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

var 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); // Do something with each 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.list;

Splitting Arrays

You can split an array into two slices:

var (slice1, slice2) = array.splitSlice(3); // Splits the array at index 3

Cell


Cell is library of useful wrappers of values (cells) - pub.dev.

Cell - A wrapper around a mutable value.

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 around a mutable value. Useful for mimicking references and wrapping primitives. Extensions exist for primitives. e.g. Cell<int> can be used similar to a normal int.

final cell = Cell<int>(10);
expect(cell.get(), 10);
cell.add(2);
expect(cell.get(), 12);
final anotherCell = Cell<int>(10);
final 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.

final cell = OnceCell<int>();
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 NullableOnceCell.

LazyCell

A value which is initialized on the first access.

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

The base type for all LazyCells is NullableLazyCell.

LazyCellAsync

A value which is asynchronously initialized on the first access.

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

The base type for all LazyCellAsyncs is NullableLazyCellAsync.

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_core 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 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]);
Option<int> next = iter.next();
expect(next, Some(6));
collect.add(next.unwrap());
next = iter.next();
collect.add(next.unwrap());
expect(next, Some(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_core/rust_core.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.

Option


Option represents the union of two types - Some<T> and None. An Option<T> is an extension type of T?. Therefore, Option has zero runtime cost and has one big advantage over T?, you can chain null specific operations!

rust_core support nullable and Option implementations of classes and methods for ergonomic convenience where possible, but you can easily switch between the two with toOption and toNullable (or you can use .v directly).

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
  default:
    // 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 {
  ...
});

To Option or Not To Option

As mentioned Option<T> is an extension type of T? so they can be used interchangeably with no runtime cost.

Option<int> intNone() => None;
Option<int> option = intNone();
int? nullable = option.v;
nullable = option.toNullable(); // or
nullable = option as int?; // or
option = Option.from(nullable);
option = nullable as Option<int>; // or

If Dart already supports nullable types, why use an option type? - with null, chaining null specific operations is not possible and the only alternate solution is a bunch of if statements and implicit and explicit type promotion. The Option type solves these issues.

final x;
final y;
switch(optionFunc1().map((e) => e + " added string").zip(optionFunc2())){
  case Some(:final v):
    (x, y) = v;
  default:
    return
}
// use x and y

or if using early return notation.

final (x,y) = optionFunc1().map((e) => e + " added string").zip(optionFunc2())[$];
// use x and y

vs

final x = optionFunc1();
if (x == null) {
  return;
}
else {
  x = x + " added string";
}
final y = optionFunc2();
if (y == null) {
  return;
}
// use x and y

With Option you will also never get another null assertion error again.

As in the previous example, it is recommended to use the Option type as the return type, since it allows early return and chaining operations. But the choice is up to the developer.

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_core/result.dart';

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

Result<String, String> processOrder(String user, int orderNumber) {
  final result = makeFood(orderNumber);
  if(result case Ok(:final ok)) {
    final paymentResult = processPayment(user);
    if(paymentResult case Ok(ok:final paymentOk)) {
      return Ok("Order of $ok is complete for $user. Payment: $paymentOk");
    }
    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(:final err)){
    return err;
}

and vice versa

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

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 ok):
   print(ok);
 case Err(:final err):
   print(err);
}

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

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 ok):
    a = ok;
  case Err():
    return boolResult;
}
final intResult = intOk();
switch(intResult){
  case Ok(:final ok):
    b = ok;
  case Err():
      return intResult;
}
final doubleResult = doubleOk();
switch(doubleResult){
    case Ok(:final ok):
        c = ok;
    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 ok):
      (a, b, c) = ok;
   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 err):
      return Err(err);
    case Ok(:final ok):
      x = ok.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_core/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_core/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_core/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_core/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_core/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_core/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_core/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_core/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_core/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 like channel, Mutex, and RwLock.

channel

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

Local Channels


channel is used for communication between produces and consumers on the same isolate. channel is similar to StreamController except it buffers data until read and will never throw. In more detail, channel 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

In this example, a single sender sends data to a single receiver. The receiver retrieves the data and processes it.

import 'package:rust_core/sync.dart';

void main() async {
  final (tx, rx) = channel<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

This example shows how to handle timeouts when receiving data from a channel.

import 'package:rust_core/sync.dart';

void main() async {
  final (tx, rx) = channel<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

In this example, we see how to handle errors that might occur while receiving data from a channel.

import 'package:rust_core/sync.dart';

void main() async {
  final (tx, rx) = channel<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

This example demonstrates how to iterate over the received data using the iter method.

import 'package:rust_core/sync.dart';

void main() async {
  final (tx, rx) = channel<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

In this example, we see how to use the receiver as a stream, allowing for asynchronous data handling.

import 'package:rust_core/sync.dart';

void main() async {
  final (tx, rx) = channel<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 toIsolateCodec and/or a fromIsolateCodec 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

This example demonstrates a simple string message being sent and received between the main isolate and a spawned isolate.

void main() async {
  final (tx1, rx1) = await isolateChannel<String, String>((tx2, rx2) async {
    assert((await rx2.recv()).unwrap() == "hello");
    tx2.send("hi");
  }, toIsolateCodec: const StringCodec(), fromIsolateCodec: const StringCodec());

  tx1.send("hello");
  expect((await rx1.recv()).unwrap(), "hi");
}

Different Codecs for Communication

This example demonstrates using different codecs for communication between the main isolate and a spawned isolate.

void main() async {
  final (tx1, rx1) = await isolateChannel<String, int>((tx2, rx2) async {
    assert((await rx2.recv()).unwrap() == "hello");
    tx2.send(1);
  }, toIsolateCodec: const StringCodec(), fromIsolateCodec: const IntCodec());

  tx1.send("hello");
  expect((await rx1.recv()).unwrap(), 1);
}

No Codecs

This example demonstrates communication without specifying codecs, relying on the default codecs.

void main() async {
  final (tx1, rx1) = await isolateChannel<String, int>((tx2, rx2) async {
    assert((await rx2.recv()).unwrap() == "hello");
    tx2.send(1);
  });

  tx1.send("hello");
  expect((await rx1.recv()).unwrap(), 1);
}

Bi-directional Send and Receive

This example demonstrates a more complex scenario where multiple messages are sent and received in both directions.

void main() async {
  final (tx1, rx1) = await isolateChannel<int, int>((tx2, rx2) async {
    await Future.delayed(Duration(milliseconds: 100));
    tx2.send((await rx2.recv()).unwrap() * 10);
    await Future.delayed(Duration(milliseconds: 100));
    tx2.send((await rx2.recv()).unwrap() * 10);
    await Future.delayed(Duration(milliseconds: 100));
    tx2.send(6);
    await Future.delayed(Duration(milliseconds: 100));
    tx2.send((await rx2.recv()).unwrap() * 10);
    await Future.delayed(Duration(milliseconds: 100));
    tx2.send((await rx2.recv()).unwrap() * 10);
    await Future.delayed(Duration(milliseconds: 100));
    tx2.send(7);
    await Future.delayed(Duration(milliseconds: 100));
    tx2.send((await rx2.recv()).unwrap() * 10);
  }, toIsolateCodec: const IntCodec(), fromIsolateCodec: const IntCodec());

  tx1.send(1);
  expect(await rx1.recv().unwrap(), 10);
  tx1.send(2);
  expect(await rx1.recv().unwrap(), 20);
  tx1.send(3);
  expect(await rx1.recv().unwrap(), 6);
  tx1.send(4);
  expect(await rx1.recv().unwrap(), 30);
  tx1.send(5);
  expect(await rx1.recv().unwrap(), 40);
  expect(await rx1.recv().unwrap(), 7);
  expect(await rx1.recv().unwrap(), 50);
}

RwLock


RwLock is used in critical sections to allow multiple concurrent read operations while ensuring that write operations are exclusive. Dart being single threaded, means it is less common to need a RwLock, but they are still useful e.g. reading and writing to data sources or transactions. RwLock uses a fifo model to prevent starvation.

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. Dart being single threaded, means it is less common to need a Mutex, but they are still useful e.g. reading and writing to data sources or transactions. Mutex uses a fifo model to prevent starvation.

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 Mutex _mutex = Mutex();

  DataManager(this._dataSources);

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

  Future<void> writeToAll(String data) async {
    await _mutex.withLock(() 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.writeToAll('Data1'),
    manager.readFromAll().then((data) => print('Read: $data')),
  ]);

  print('All operations completed');
}

Convert


Infallible

Infallible is the error type for errors that can never happen. This can be useful for generic APIs that use Result and parameterize the error type, to indicate that the result is always Ok. Thus these types expose intoOk and intoErr.


Result<int, Infallible> x = Ok(1);
expect(x.intoOk(), 1);
Result<Infallible, int> w = Err(1);
expect(w.intoErr(), 1);
typedef Infallible = Never;

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) {
    Arr<int> arr = Arr.range(0, 10);
    Slice<int> slice = arr(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.

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_core was designed with safety in mind. The only time rust_core 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();

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. Vec is not included in 'package:rust_core/rust_core.dart' instead it is included included in 'package:rust_core/vec.dart'.

Usage

import 'package:rust_core/rust_core.dart';
import 'package:rust_core/vec.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 Core

Official

LibraryDescription
anyhowIdiomatic error handling capabilities to make your code safer, more maintainable, and errors easier to debug.
path_typeA zero cost path type.
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.
thiserrorA library for concisely defining error types (error enums / sealed classes) and their String representation.

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":