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::*;
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_core::hlist::*; ```
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); ```
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 ```
You can also traverse HLists using .pop()
rust
let h = hlist![true, "hello", Some(41)];
// h has a static type of: HCons<bool, HCons<&str, HCons<Option<{integer}>, HNil>>>
let (h1, tail1) = h.pop();
assert_eq!(h1, true);
assert_eq!(tail1, hlist!["hello", Some(41)]);
You can reverse, map, and fold over them too:
```rust // Reverse let h1 = hlist![true, "hi"]; asserteq!(h1.intoreverse(), hlist!["hi", true]);
// Fold 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]);
```
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; use frunk::*; // for the Generic trait and HList
struct Person<'a> { firstname: &'a str, lastname: &'a str, age: usize, }
let h = hlist!("Joe", "Blow", 30); let p: Person = 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 = ApiPersion { firstname: "Joe", lastname: "Blow", age: 30, }; let dperson: DomainPersion = convertfrom(aperson); // done ```
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_core::hlist::; use frunk::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::*;
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::*;
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)) ```
Before a 1.0 release, would be best to revisit the design of the interfaces and do some general code (and test cleanup).
Benchmarks are available in ./benches
and can be run with:
$ rustup run nightly cargo bench
It would be nice to use something like bench-cmp to compare before and after, but for some reason, there is no output. Should investigate why.
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 ;)