rust (formally known as rust_core) is a pure Dart implementation of patterns found in the Rust programming language, bringing the power of Rust to Dart!
Types include Result, Option, Cell, Slice, Array, Iterator, Channels, Mutex, and more.
See the 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/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().isNone():
answer.push(index + 1);
}
}
expect(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>
Imports
rust follows the same library structure and naming as Rust's core library.
To that extent, each library can be imported individually
import 'package:rust/result.dart';
or all libraries
import 'package:rust/rust.dart';
General Notes
All of rust'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 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 Type | Dart Equivalent | rust | Description |
---|---|---|---|
[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> | - | Mutex | Exclusion primitive useful for protecting critical sections |
RwLock<T> | - | RwLock | Exclusion 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 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 Type | Dart Equivalent | rust | Description |
---|---|---|---|
[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> | - | Mutex | Exclusion primitive useful for protecting critical sections |
RwLock<T> | - | RwLock | Exclusion 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 ofT?
. - 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 (andOnceCell
/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 growableList
s everywhere even when you do not need to, which is less performant. So we addedArr
(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 introducingchannel
for typed bi-directional isolate communication. - Dart's iteration methods are lacking for
Iterable
andIterator
(there are none! justmoveNext()
andcurrent
), while Rust has an abundance of useful methods. So we introduced Rust'sIterator
.
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 types (cells) - pub.dev.
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 Cell
s 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 OnceCell
s is NullableOnceCell
.
LazyCell
A value which is initialized on the first access.
int callCount = 0;
LazyCell<int> lazyCell = LazyCell(() {
callCount++;
return 20;
});
LazyCell<int> firstCall = lazyCell();
expect(callCount, equals(1));
expect(firstCall, equals(20));
LazyCell<int> secondCall = lazyCell();
expect(callCount, equals(1));
expect(secondCall, equals(20));
The base type for all LazyCell
s is NullableLazyCell
.
LazyCellAsync
A value which is asynchronously initialized on the first access.
int callCount = 0;
LazyCellAsync<int> lazyCell = LazyCellAsync(() async {
callCount++;
return 20;
});
LazyCellAsync<int> firstCall = await lazyCell.force();
expect(callCount, equals(1));
expect(firstCall, equals(20));
LazyCellAsync<int> secondCall = lazyCell(); // Could also call `await lazyCell.force()` again.
expect(callCount, equals(1));
expect(secondCall, equals(20));
The base type for all LazyCellAsync
s 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
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/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.
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.
rust support nullable and Option
implementations of classes and methods for ergonomic convenience where possible, but you
can easily switch between the two with no runtime cost.
Option<int> option = None;
int? nullable = option.v;
option = Option.from(nullable);
nullable = option as int?; // or
option = nullable as Option<int>; // or
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
If Dart already supports nullable types, why use an option type? Nullable types may required an
uncomfortable level of null checking and nesting. Even so, one may also still need to write a null
assertion !
for some edge cases where the compiler is not smart enough.
The Option
type provides an alternative solution with methods and early return.
Methods:
final profile;
final preferences;
switch (fetchUserProfile()
.map((e) => "${e.name} - profile")
.andThen((e) => Some(e).zip(fetchUserPreferences()))) {
case Some(:final v):
(profile, preferences) = v;
default:
return;
}
print('Profile: $profile, Preferences: $preferences');
Early Return Notation:
final (profile, preferences) = fetchUserProfile()
.map((e) => "${e.name} - profile")
.andThen((e) => Some(e).zip(fetchUserPreferences()))[$];
print('Profile: $profile, Preferences: $preferences');
Traditional Null-Based Approach:
final profile = fetchUserProfile();
if (profile == null) {
return;
} else {
profile = profile.name + " - profile";
}
final preferences = fetchUserPreferences();
if (preferences == null) {
return;
}
print('Profile: $profile, Preferences: $preferences');
Drawbacks
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 = ...;
int x;
switch(xOpt) {
Some(:final v):
x = v;
default:
return;
}
// use `int` x
vs
int? x = ...;
if(x == null){
return;
}
// use `int` x
Conclusion
If you can't decide between the two, it is recommended to use the Option
type as the return type, since it allows
early return, chaining operations, and easy conversion to a nullable type with .v
. But the choice is up to the developer.
You can easily use this package and never use Option
.
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(: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 Future
s 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 Result
s 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 Result
s 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/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]
}
Binary Search
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 like channel, Mutex, and RwLock.
channel
rust 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/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/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/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/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/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:
RangeBounds
can be used to get aSlice
of anArr
,Slice
, orList
.
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
.
RangeBounds
that also implementIterableRangeBounds
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 Type | Equivalent in Rust |
---|---|
Exception | Error |
Error | Panic |
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();
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/rust.dart'
instead it is included included in 'package:rust/vec.dart'
.
Usage
import 'package:rust/rust.dart';
import 'package:rust/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
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":