frunk frəNGk * Functional programming toolbelt in Rust. * Might seem funky at first, but you'll like it. * Comes from: funktional (German) + Rust → Frunk
The general idea is to make things easier by providing FP tools in Rust to allow for stuff like this:
```rust use frunk::monoid::combine_all;
let v = vec![Some(1), Some(3)]; asserteq!(combineall(&v), Some(4));
// Slightly more magical let t1 = (1, 2.5f32, String::from("hi"), Some(3)); let t2 = (1, 2.5f32, String::from(" world"), None); let t3 = (1, 2.5f32, String::from(", goodbye"), Some(10)); let tuples = vec![t1, t2, t3];
let expected = (3, 7.5f32, String::from("hi world, goodbye"), Some(13)); asserteq!(combineall(&tuples), expected); ```
For a deep dive, RustDocs are available for: * Code on Master * Latest published release
Statically typed heterogeneous lists.
First, let's enable hlist
:
```rust
use frunk::{HNil, HCons}; ```
Some basics:
```rust
let h = hlist![1];
// Type annotations for HList are optional. Here we let the compiler infer it for us
// h has a static type of: HCons
// HLists have a head and tail asserteq!(hlist![1].head, 1); asserteq!(hlist![1].tail, HNil);
// You can convert a tuple to an HList and vice-versa let h2 = hlist![ 42f32, true, "hello" ]; let t: (f32, bool, &str) = h2.into(); assert_eq!(t, (42f32, true, "hello"));
let t3 = (999, false, "world"); let h3: Hlist![ isize, bool, &str ] = t3.into(); assert_eq!(h3, hlist![ 999, false, "world" ]); ```
HLists have a hlist_pat!
macro for pattern matching;
```rust
let h: Hlist!(&str, &str, i32, bool) = hlist!["Joe", "Blow", 30, true];
// We use the Hlist! type macro to make it easier to write
// a type signature for HLists, which is a series of nested HCons
// h has an expanded static type of: HCons<&str, HCons<&str, HCons
let hlistpat!(fname, lname, age, isadmin) = h; asserteq!(fname, "Joe"); asserteq!(lname, "Blow"); asserteq!(age, 30); asserteq!(is_admin, true);
// You can also use into_tuple2() to turn the hlist into a nested pair ```
To traverse or build lists, you can also prepend/or pop elements at the front:
```rust
let list = hlist![true, "hello", Some(41)];
// h has a static type of: HCons
// or using macro sugar: let hlistpat![head2, ...tail2] = list; // equivalent to pop let list2 = hlist![head2, ...tail2]; // equivalent to prepend asserteq!(list, list2); ```
You can reverse, map, and fold over them too:
```rust // Reverse let h1 = hlist![true, "hi"]; asserteq!(h1.intoreverse(), hlist!["hi", true]);
// Fold (foldl and foldr exist) let h2 = hlist![1, false, 42f32]; let folded = h2.foldr( hlist![ |i, acc| i + acc, |, acc| if acc > 42f32 { 9000 } else { 0 }, |f, acc| f + acc ], 1f32 ); asserteq!(folded, 9001)
// Map let h3 = hlist![9000, "joe", 41f32]; let mapped = h3.map(hlist![ |n| n + 1, |s| s, |f| f + 1f32]); assert_eq!(mapped, hlist![9001, "joe", 42f32]); ```
You can pluck a type out of an HList
using pluck()
, which also gives you back the remainder after plucking that type
out. This method is checked at compile-time to make sure that the type you ask for can be extracted.
rust
let h = hlist![1, "hello", true, 42f32];
let (t, remainder): (bool, _) = h.pluck();
assert!(t);
assert_eq!(remainder, hlist![1, "hello", 42f32])
Similarly, you can re-shape, or sculpt, an Hlist
, there is a sculpt()
method, which allows you to re-organise and/or
cull the elements by type. Like pluck()
, sculpt()
gives you back your target with the remainder data in a pair. This
method is also checked at compile time to make sure that it won't fail at runtime (the types in your requested target shape
must be a subset of the types in the original HList
.
rust
let h = hlist![9000, "joe", 41f32, true];
let (reshaped, remainder): (Hlist![f32, i32, &str], _) = h.sculpt();
assert_eq!(reshaped, hlist![41f32, 9000, "joe"]);
assert_eq!(remainder, hlist![true]);
Generic
is a way of representing a type in ... a generic way. By coding around Generic
, you can to write functions
that abstract over types and arity, but still have the ability to recover your original type afterwards. This can be a fairly powerful thing.
Frunk comes out of the box with a nice custom Generic
derivation so that boilerplate is kept to a minimum.
Here are some examples:
```rust
extern crate frunk; extern crate frunk_core;
struct Person<'a> { firstname: &'a str, lastname: &'a str, age: usize, }
let h = hlist!("Joe", "Blow", 30); let p: Person = frunk::fromgeneric(h); asserteq!(p, Person { firstname: "Joe", lastname: "Blow", age: 30, }); ```
This also works the other way too; just pass a struct to into_generic
and get its generic representation.
Sometimes you may have 2 different types that are structurally the same (e.g. different domains but the same data). Use cases include:
Generic comes with a handy convert_from
method that helps make this painless:
```rust // Assume we have all the imports needed
struct ApiPerson<'a> { FirstName: &'a str, LastName: &'a str, Age: usize, }
struct DomainPerson<'a> { firstname: &'a str, lastname: &'a str, age: usize, }
let aperson = ApiPerson { FirstName: "Joe", LastName: "Blow", Age: 30, }; let dperson: DomainPerson = frunk::convertfrom(aperson); // done ```
In addition to Generic
, there is also LabelledGeneric
, which, as the name implies, relies on a generic representation
that is labelled. This means that if two structs derive LabelledGeneric
, you can convert between them only if their
field names match!
Here's an example:
```rust // Suppose that again, we have different User types representing the same data // in different stages in our application logic.
struct NewUser<'a> { firstname: &'a str, lastname: &'a str, age: usize, }
struct SavedUser<'a> { firstname: &'a str, lastname: &'a str, age: usize, }
let nuser = NewUser { firstname: "Joe", last_name: "Blow", age: 30 };
// Convert from a NewUser to a Saved using LabelledGeneric // // This will fail if the fields of the types converted to and from do not // have the same names or do not line up properly :) // // Also note that we're using a helper method to avoid having to use universal // function call syntax let suser: SavedUser = frunk::labelledconvertfrom(nuser);
asserteq!(suser.firstname, "Joe"); asserteq!(suser.lastname, "Blow"); asserteq!(suser.age, 30);
// Uh-oh ! lastname and firstname have been flipped!
struct DeletedUser<'a> { lastname: &'a str, firstname: &'a str, age: usize, }
// This would fail at compile time :) let duser: DeletedUser = frunk::labelledconvertfrom(suser);
// This will, however, work, because we make use of the Sculptor type-class // to type-safely reshape the representations to align/match each other. let duser: DeletedUser = frunk::transformfrom(s_user); ```
Sometimes you need might have one data type that is "similar in shape" to another data type, but it
is similar recursively (e.g. it has fields that are structs that have fields that are a superset of
the fields in the target type, so they are transformable recursively). .transform_from
can't help you
there because it doesn't deal with recursion, but the Transmogrifier
can help if both are LabelledGeneric
by transmogrify()
ing from one to the other.
What is "transmogrifying"? In this context, it means to recursively tranform some data of type A into data of type B, in a typesafe way, as long as A and B are "similarly-shaped". In other words, as long as B's fields and their subfields are subsets of A's fields and their respective subfields, then A can be turned into B.
As usual, the goal with Frunk is to do this:
* Using stable (so no specialisation, which would have been helpful, methinks)
* Typesafe
* No usage of unsafe
Here is an example:
```rust
extern crate frunk_core;
use frunk::labelled::Transmogrifier;
struct InternalPhoneNumber {
emergency: Option
struct InternalAddress<'a> { is_whitelisted: bool, name: &'a str, phone: InternalPhoneNumber, }
struct InternalUser<'a> { name: &'a str, age: usize, address: InternalAddress<'a>, is_banned: bool, }
struct ExternalPhoneNumber { main: usize, }
struct ExternalAddress<'a> { name: &'a str, phone: ExternalPhoneNumber, }
struct ExternalUser<'a> { age: usize, address: ExternalAddress<'a>, name: &'a str, }
let internaluser = InternalUser { name: "John", age: 10, address: InternalAddress { iswhitelisted: true, name: "somewhere out there", phone: InternalPhoneNumber { main: 1234, secondary: None, emergency: Some(5678), }, }, is_banned: true, };
/// Boilerplate-free conversion of a top-level InternalUser into an /// ExternalUser, taking care of subfield conversions as well. let externaluser: ExternalUser = internaluser.transmogrify();
let expectedexternaluser = ExternalUser { name: "John", age: 10, address: ExternalAddress { name: "somewhere out there", phone: ExternalPhoneNumber { main: 1234, }, } };
asserteq!(externaluser, expectedexternaluser); ```
Note that as of writing, there are a couple of known limitations with transmogrify()
,
some of which may be addressed in the future:
LabelledGeneric
,
the compiler will tell you that it can't "infer an index" for transmogrify()
; this
is because impl
s of the Transmogrifier
trait will clash. This may or may not
change in the future (perhaps if we move to a pure procedural macro powered way of doing
things?)transmogfiy()
ing,
using this technique will likely increase your compile time.transform_from
when a transform is deemed
impossible (e.g. missing field), the errors for transmogrify()
are worse to the degree that
recursive transmogrify()
is required for your types.For more information how Generic and Field work, check out their respective Rustdocs: * Generic * Labelled
One of the other things that LabelledGeneric
-deriving structs can do is be generically traversed
using Path
and its companion trait PathTraverser
. In some circles, this functionality is also
called a Lens.
Path
-based traversals are
* Easy to use through the procedural macro path!
(frunk_proc_macros
)
* Traversing multiple levels is familiar; just use dot .
syntax (path!(nested.attribute.value)
)
* Compile-time safe
* Composable (add one to the other using +
)
* Allows you to get by value, get by reference or get by mutable reference, depending on the type
of thing you pass it.
```rust
struct Dog<'a> { name: &'a str, dimensions: Dimensions, }
struct Cat<'a> { name: &'a str, dimensions: Dimensions, }
struct Dimensions { height: usize, width: usize, unit: SizeUnit, }
enum SizeUnit { Cm, Inch, }
let mut dog = Dog { name: "Joe", dimensions: Dimensions { height: 10, width: 5, unit: SizeUnit::Inch, }, };
let cat = Cat { name: "Schmoe", dimensions: Dimensions { height: 7, width: 3, unit: SizeUnit::Cm, }, };
// generic, re-usable, compsable paths let dimensionslens = path!(dimensions); let heightlens = dimensionslens + path!(height); // compose multiple let unitlens = path!(dimensions.unit); // dot syntax to just do the whole thing at once
asserteq!(*heightlens.get(&dog), 10); asserteq!(*heightlens.get(&cat), 7); asserteq!(*unitlens.get(&dog), SizeUnit::Inch); asserteq!(*unitlens.get(&cat), SizeUnit::Cm);
// modify by passing a &mut height_lens.get(&mut dog) = 13; assert_eq!(height_lens.get(&dog), 13); ```
There's also a Path!
type-level macro for declaring shape-constraints. This allows you to write adhoc shape-dependent
functions for LabelledGeneric
types.
rust
// Prints height as long as `A` has the right "shape" (e.g.
// has `dimensions.height: usize` and `dimension.unit: SizeUnit)
fn print_height<'a, A, HeightIdx, UnitIdx>(obj: &'a A) -> ()
where
&'a A: PathTraverser<Path!(dimensions.height), HeightIdx, TargetValue = &'a usize>
+ PathTraverser<Path!(dimensions.unit), UnitIdx, TargetValue = &'a SizeUnit>,
{
println!(
"Height [{} {:?}]",
path!(dimensions.height).get(obj),
path!(dimensions.unit).get(obj)
);
}
See examples/paths.rs
to see how this works.
If you've ever wanted to have an adhoc union / sum type of types that you do not control, you may want
to take a look at Coproduct
. In Rust, thanks to enum
, you could potentially declare one every time you
want a sum type to do this, but there is a light-weight way of doing it through Frunk:
```rust
use frunk::prelude::*; // for the fold method
// Declare the types we want in our Coproduct type I32F32Bool = Coprod!(i32, f32, bool);
let co1 = I32F32Bool::inject(3); let getfrom1a: Option<&i32> = co1.get(); let getfrom1b: Option<&bool> = co1.get();
asserteq!(getfrom1a, Some(&3)); // None because co1 does not contain a bool, it contains an i32 asserteq!(getfrom1b, None);
// This will fail at compile time because i8 is not in our Coproduct type let nopegetfrom1b: Option<&i8> = co1.get(); // <-- will fail // It's also impossible to inject something into a coproduct that is of the wrong type // (not contained in the coproduct type) let nopeco = I32F32Bool::inject(42f64); // <-- will fail
// We can fold our Coproduct into a single value by handling all types in it asserteq!( co1.fold(hlist![|i| format!("int {}", i), |f| format!("float {}", f), |b| (if b { "t" } else { "f" }).tostring()]), "int 3".to_string()); ```
For more information, check out the docs for Coproduct
Validated
is a way of running a bunch of operations that can go wrong (for example,
functions returning Result<T, E>
) and, in the case of one or more things going wrong,
having all the errors returned to you all at once. In the case that everything went well, you get
an HList
of all your results.
Mapping (and otherwise working with plain) Result
s is different because it will
stop at the first error, which can be annoying in the very common case (outlined
best by the Cats project).
To use Validated
, first:
```rust
use frunk::prelude::*; // for Result::into_validated ```
Assuming we have a Person
struct defined
```rust
struct Person { age: i32, name: String, street: String, } ```
Here is an example of how it can be used in the case that everything goes smoothly.
```rust
fn getname() -> Result
// Build up a Validated
by adding in any number of Result
s
let validation = getname().intovalidated() + getage() + getstreet();
// When needed, turn the Validated
back into a Result and map as usual
let tryperson = validation.intoresult()
// Destructure our hlist
.map(|hlist_pat!(name, age, street)| {
Person {
name: name,
age: age,
street: street,
}
});
asserteq!(tryperson.unwrap(), Person { name: "James".toowned(), age: 32, street: "Main".toowned(), })); } ```
If, on the other hand, our Result
s are faulty:
```rust
/// This next pair of functions always return Recover::Err
fn getnamefaulty() -> Result
fn getagefaulty() -> Result
let validation2 = getnamefaulty().intovalidated() + getagefaulty(); let tryperson2 = validation2.intoresult() .map(|| unimplemented!());
// Notice that we have an accumulated list of errors! asserteq!(tryperson2.unwraperr(), vec!["crap name".toowned(), "crap age".to_owned()]); ```
Things that can be combined.
```rust use frunk::Semigroup; use frunk::semigroup::All;
assert_eq!(Some(1).combine(&Some(2)), Some(3));
asserteq!(All(3).combine(&All(5)), All(1)); // bit-wise && asserteq!(All(true).combine(&All(false)), All(false)); ```
Things that can be combined and have an empty/id value.
```rust use frunk::monoid::combine_all;
let t1 = (1, 2.5f32, String::from("hi"), Some(3)); let t2 = (1, 2.5f32, String::from(" world"), None); let t3 = (1, 2.5f32, String::from(", goodbye"), Some(10)); let tuples = vec![t1, t2, t3];
let expected = (3, 7.5f32, String::from("hi world, goodbye"), Some(13)); asserteq!(combineall(&tuples), expected)
let productnums = vec![Product(2), Product(3), Product(4)]; asserteq!(combineall(&productnums), Product(24)) ```
Frunk comes with support for deriving serde serializer/deserializers for its core
data structures. This can be enabled by adding the serde
feature flag.
For example, if you'd like to use just frunk_core
with serde
toml
[dependencies]
frunk_core = { version = "$version", features = ["serde"] }
Or, if you'd like to use frunk
with serde, you need to explicitly include frunk_core
as well
toml
[dependencies]
frunk = { version = "$version", features = ["serde"] }
frunk_core = { version = "$version", features = ["serde"] }
Benchmarks are available in ./benches
and can be run with:
$ rustup run nightly cargo bench
Benchmarks on master
are also auto-generated, uploaded and available online.
Before a 1.0 release, would be best to revisit the design of the interfaces and do some general code (and test cleanup).
Given that Rust has no support for Higher Kinded Types, I'm not sure if these
are even possible to implement. In addition, Rustaceans are used to calling iter()
on collections to get a lazy view, manipulating their elements with map
or and_then
, and then doing a collect()
at the end to keep things
efficient. The usefulness of these following structures maybe limited in that context.
Functor
Monad
Apply
Applicative
Yes please !
The following are considered important, in keeping with the spirit of Rust and functional programming:
Scalaz, Shapeless, Cats, Haskell, the usual suspects ;)
A.k.a. people whom you can bug/tag/@ on Gitter :D