- Notifications
You must be signed in to change notification settings - Fork66
A library for functional programming in Rust
License
JasonShin/fp-core.rs
Folders and files
Name | Name | Last commit message | Last commit date | |
---|---|---|---|---|
Repository files navigation
The project is a library for functional programming in Rust.
A library for functional programming in Rust.
It contains purely functional data structures to supplement the functional programming needs alongsidewith the Rust Standard Library.
Add below line to your Cargo.toml
fp-core ="0.1.9"
If you haveCargo Edit you may simply
$ cargo add fp-core
Functional programming (FP) provides many advantages, and its popularity has been increasing as a result.However, each programming paradigm comes with its own unique jargon and FP is no exception. By providing a glossary,we hope to make learning FP easier.
Where applicable, this document uses terms defined in theFantasy Land specand Rust programming language to give code examples.
The content of this section was drawn fromFunctional Programming Jargon in Javascript andwe sincerely appreciate them for providing the initial baseline.
Table of Contents
- Arity
- Higher-Order Functions (HOF)
- Closure
- Partial Application
- Currying
- Auto Currying
- Referential Transparency
- Lambda
- Lambda Calculus
- Purity
- Side effects
- Idempotent
- Function Composition
- Continuation
- Point-Free Style
- Predicate
- Contracts
- Category
- Value
- Constant
- Variance
- Higher Kinded Type
- Functor
- Pointed Functor
- Lifting
- Equational Reasoning
- Monoid
- Monad
- Comonad
- Applicative
- Morphism
- Setoid
- Ord
- Semigroup
- Foldable
- Lens
- Type Signature
- Algebraic data type
- Option
- Functional Programming References
- Function Programming development in Rust Language
- Inspiration
The number of arguments a function takes. From words like unary, binary, ternary, etc.This word has the distinction of being composed of two suffixes, "-ary" and "-ity."Addition, for example, takes two arguments, and so it is defined as a binary function or a function with an arity of two.Such a function may sometimes be called "dyadic" by people who prefer Greek roots to Latin.Likewise, a function that takes a variable number of arguments is called "variadic,"whereas a binary function must be given two and only two arguments, currying and partial application notwithstanding (see below).
let sum = |a:i32,b:i32|{ a + b};// The arity of sum is 2
A function which takes a function as an argument and/or returns a function.
let filter = |predicate:fn(&i32) ->bool,xs:Vec<i32> |{ xs.into_iter().filter(predicate).collect::<Vec<i32>>()};
let is_even = |x:&i32|{ x %2 ==0};
filter(is_even,vec![1,2,3,4,5,6]);
A closure is a scope which retains variables available to a function when it's created. This is important forpartial application to work.
let add_to = |x:i32|move |y:i32| x + y;
We can calladd_to
with a number and get back a function with a baked-inx
. Notice that we also need to move the ownership of the x to the internal lambda.
let add_to_five =add_to(5);
In this case thex
is retained inadd_to_five
's closure with the value5
. We can then calladd_to_five
with they
and get back the desired number.
add_to_five(3);// => 8
Closures are commonly used in event handlers so that they still have access to variables defined in their parents when theyare eventually called.
Further reading
Partially applying a function means creating a new function by pre-filling some of the arguments to the original function.
To achieve this easily, we will be using apartial application crate
#[macro_use]externcrate partial_application;fnfoo(a:i32,b:i32,c:i32,d:i32,mul:i32,off:i32) ->i32{(a + b*b + c.pow(3) + d.pow(4))* mul - off}let bar =partial!( foo(_, _,10,42,10,10));assert_eq!( foo(15,15,10,42,10,10), bar(15,15));// passes
Partial application helps create simpler functions from more complex ones by baking in data when you have it.Curried functions are automatically partially applied.
Further reading
The process of converting a function that takes multiple arguments into a function that takes them one at a time.
Each time the function is called it only accepts one argument and returns a function that takes one argument until all arguments are passed.
fnadd(x:i32) ->implFn(i32)->i32{move |y| x + y}let add5 =add(5);add5(10);// 15
Further reading
Transforming a function that takes multiple arguments into one that if given less than itscorrect number of arguments returns a function that takes the rest. When the function gets the correct number ofarguments it is then evaluated.
Although Auto Currying is not possible in Rust right now, there is a debate on this issue on the Rust forum:https://internals.rust-lang.org/t/auto-currying-in-rust/149/22
An expression that can be replaced with its value without changing the behavior of the program is said to be referentially transparent.
Say we have function greet:
let greet = ||"Hello World!";
Any invocation ofgreet()
can be replaced withHello World!
hence greet is referentially transparent.
An anonymous function that can be treated like a value.
fnincrement(i:i32) ->i32{ i +1}let closure_annotated = |i:i32|{ i +1};let closure_inferred = |i| i +1;
Lambdas are often passed as arguments to Higher-Order functions.You can assign a lambda to a variable, as shown above.
A branch of mathematics that uses functions to create auniversal model of computation.
This is in contrast to aTuring machine, an equivalent model.
Lambda calculus has three key components: variables, abstraction, and application. A variable is just somesymbol, sayx
. An abstraction is sort of a function: it binds variables into "formulae". Applicationsare function calls. This is meaningless without examples.
The identity function (|x| x
in rust) looks like\ x. x
in most literature (\
is a Lambda where Latexor Unicode make it available). It is an abstraction. If1
were a value we could use,(\ x. x) 1
wouldbe an application (and evaluating it gives you1
).
But there's more...
Computation in Pure Lambda Calculus
Let's invent booleans.\ x y. x
can be true and\ x y. y
can be false.If so,\ b1 b2. b1(b2,(\x y. y))
isand
. Let's evaluate it to show how:
b1 | b2 | Theirand |
---|---|---|
\ x y. x | \x y. x | \x y. x |
\ x y. x | \x y. y | \x y. y |
\ x y. y | \x y. y | \x y. y |
\ x y. y | \x y. x | \x y. y |
I'll leaveor
as an exercise. Furthermore,if
can now be implemented:\c t e. c(t, e)
wherec
is the condition,t
the consequent (then
) ande
the else clause.
SICP leaves numbers as an exercise.They define 0 as\f . \x. x
and adding one as\n. \f. \x. f(n(f)(x))
.That isn't even ASCII art, so let's add:0 + 1
:
(\n. \f. \x. f(n(f)(x)))(\f. \x. x) = \f. \x. f((\x'. x')(x)) = \f. \x. f(x)
Basically, the number off
s in the expression is the number. I'll leave figuring out larger numbers as a exercise.With patience, you can show that\f. \x. f(f(x))
is two. This will help with addition:\n m. \f. \x. n(m(f)(x))
should add two numbers. Let's make 4:
(\n m. \f. \x. n(f)(m(f)(x)))(\f. x. f(f(x)), \f. \x. f(f(x))) = \f. \x. (\f'. \x'. f'(f'(x')))(f)((\f'. \x'. f'(f'(x')))(f)(x)) = \f. \x. (\x'. f(f(x')))(f(f(x'))) = \f. \x. f(f(f(f(x))))
Multiplication is harder and there's betterexposition on Wikipedia.Another good reference ison stackoverflow.
A function is pure if the return value is only determined by its input values, and does not produce side effects.
let greet = |name:&str|{format!("Hi! {}", name)};greet("Jason");// Hi! Jason
As opposed to each of the following:
let name ="Jason";let greet = || ->String{format!("Hi! {}", name)};greet();// String = "Hi! Jason"
The above example's output is based on data stored outside of the function...
letmut greeting:String ="".to_string();letmut greet = |name:&str|{ greeting =format!("Hi! {}", name);};greet("Jason");assert_eq!("Hi! Jason", greeting);// Passes
... and this one modifies state outside of the function.
A function or expression is said to have a side effect if apart from returning a value,it interacts with (reads from or writes to) external mutable state.
use std::time::SystemTime;let now =SystemTime::now();
println!("IO is a side effect!");// IO is a side effect!
A function is idempotent if reapplying it to its result does not produce a different result.
// Custom immutable sort methodlet sort = |x:Vec<i32>| ->Vec<i32>{letmut x = x; x.sort(); x};
Then we can use the sort method like
let x =vec![2,1];let sorted_x =sort(sort(x.clone()));let expected =vec![1,2];assert_eq!(sorted_x, expected);// passes
let abs = |x:i32 | ->i32{ x.abs()};let x:i32 =10;let result =abs(abs(x));assert_eq!(result, x);// passes
The act of putting two functions together to form a third function where the output of one function is the input of the other.Below is an example of compose function is Rust.
macro_rules! compose{( $last:expr) =>{ $last};( $head:expr, $($tail:expr), +) =>{ compose_two($head, compose!($($tail),+))};}fncompose_two<A,B,C,G,F>(f:F,g:G) ->implFn(A) ->CwhereF:Fn(A) ->B,G:Fn(B) ->C,{move |x|g(f(x))}
Then we can use it like
let add = |x:i32 | x +2;let multiply = |x:i32 | x*2;let divide = |x:i32 | x /2;let intermediate =compose!(add, multiply, divide);let subtract = |x:i32 | x -1;let finally =compose!(intermediate, subtract);let expected =11;let result =finally(10);assert_eq!(result, expected);// passes
At any given point in a program, the part of the code that's yet to be executed is known as a continuation.
let print_as_string = |num:i32|println!("Given {}", num);let add_one_and_continue = |num:i32,cc:fn(i32)|{let result = num +1;cc(result)};add_one_and_continue(1, print_as_string);// Given 2
Continuations are often seen in asynchronous programming when the program needs to wait to receive data before it can continue.The response is often passed off to the rest of the program, which is the continuation, once it's been received.
Writing functions where the definition does not explicitly identify the arguments used.This style usually requires currying or other Higher-Order functions. A.K.A Tacit programming.
A predicate is a function that returns true or false for a given value.A common use of a predicate is as the callback for array filter.
let predicate = |a:&i32 | a.clone() >2;let result =(vec![1,2,3,4]).into_iter().filter(predicate).collect::<Vec<i32>>();assert_eq!(result, vec![3,4]);// passes
A contract specifies the obligations and guarantees of the behavior from a function or expression at runtime.This acts as a set of rules that are expected from the input and output of a function or expression,and errors are generally reported whenever a contract is violated.
let contract = |x:&i32 | ->bool{ x >&10};let add_one = |x:&i32 | ->Result<i32,String>{ifcontract(x){returnOk(x +1);}Err("Cannot add one".to_string())};
Then you can useadd_one
like
let expected =12;matchadd_one(&11){Ok(x) =>assert_eq!(x, expected), _ =>panic!("Failed!")}
A category in category theory is a collection of objects and morphisms between them. In programming, typically typesact as the objects and functions as morphisms.
To be a valid category 3 rules must be met:
- There must be an identity morphism that maps an object to itself.Where
a
is an object in some category,there must be a function froma -> a
. - Morphisms must compose.Where
a
,b
, andc
are objects in some category,andf
is a morphism froma -> b
, andg
is a morphism fromb -> c
;g(f(x))
must be equivalent to(g • f)(x)
. - Composition must be associative
f • (g • h)
is the same as(f • g) • h
Since these rules govern composition at very abstract level, category theory is great at uncovering new ways of composing things.In particular, many see this as another foundation of mathematics (so, everything would be a category from this view of math).Various definitions in this guide are related to category theory since this approach applies elegantly to functional programming.
Examples of categories
When one specifies a category, the objects and morphisms are essential. Additionally, showing that the rules are met is nicethough usually left to the reader as an exercise.
- Any type and pure functions on the type. (Note, we require purity since side-effects affect associativity (the third rule).)
These examples come up in mathematics:
- 1: the category with 1 object and its identity morphism.
- Monoidal categories:monoids are defined later but any monoid is a category with 1 object and many morphismsfrom the object to itself. (Yes, there is also a category of monoids -- this is not that -- this example is that any monoid is its owncategory.)
Further reading
- Category Theory for Programmers
- Awodey's introduction for those who like math.
Anything that can be assigned to a variable.
let a =5;let b =vec![1,2,3];let c ="test";
A variable that cannot be reassigned once defined.
let a =5;a =3;// error!
Constants arereferentially transparent.That is, they can be replaced with the values that they represent without affecting the result.
Variance in functional programming refers to subtyping between more complex types related to subtyping betweentheir components.
Unlike other usage of variance inObject Oriented Programming like Typescript or C#orfunctional programming language like Scala or Haskell
Variance in Rust is used during the type checking against type and lifetime parameters. Here are examples:
- By default, all lifetimes are co-variant except for
'static
because it outlives all others 'static
is always contra-variant to others regardless of where it appears or used- It is
in-variant
if you useCell<T>
orUnsafeCell<T>
inPhatomData
Further Reading
- https://github.com/rust-lang/rustc-guide/blob/master/src/variance.md
- https://nearprotocol.com/blog/understanding-rust-lifetimes/
Rust does not support Higher Kinded Typesyet. First of all, HKT is atype with a "hole" in it, so you can declare a type signature such astrait Functor<F<A>>
.
Although Rust lacks in a native support for HKT, we always have a walk around calledLightweight Higher Kinded Type
An implementation example of above theory in Rust would look like below:
pubtraitHKT<A,B>{typeURI;typeTarget;}// Lifted Optionimpl<A,B>HKT<A,B>forOption<A>{typeURI =Self;typeTarget =Option<B>;}
Higher Kinded Type is crucial for functional programming in general.
Further Reading
- https://gist.github.com/CMCDragonkai/a5638f50c87d49f815b8
- https://www.youtube.com/watch?v=ERM0mBPNLHc
An object that implements a map function which,while running over each value in the object to produce a new functor of the same type, adheres to two rules:
Preserves identity
object.map(x => x) ≍ object
Composable
object.map(compose(f, g)) ≍ object.map(g).map(f)
(f
,g
are arbitrary functions)
For example, below can be considered as a functor-like operation
let v:Vec<i32> =vec![1,2,3].into_iter().map(| x | x +1).collect();assert_eq!(v, vec![2,3,4]);// passes while mapping the original vector and returns a new vector
While leveraging theHKT implementation, You can define a trait that represents Functor like below
pubtraitFunctor<A,B>:HKT<A,B>{fnfmap<F>(self,f:F) -> <SelfasHKT<A,B>>::TargetwhereF:FnOnce(A) ->B;}
Then use it against a type such as Option like
impl<A,B>Functor<A,B>forOption<A>{fnfmap<F>(self,f:F) ->Self::TargetwhereF:FnOnce(A) ->B{self.map(f)}}// This conflicts with the above, so isn't tested// below and is commented out. TODO: add a careful// implementation that makes sure Optional and this// don't conflict.impl<A,B,T>HKT<A,B>forTwhereT:Sized +Iterator<Item =A>,U:Sized +Iterator<Item =B>,{typeURI =Self;typeTarget =U;}impl<A,B,T>Functor<A,B>forTwhereT:Iterator<Item =A>,{fnfmap<F>(self,f:F) ->Self::TargetwhereF:FnOnce(A) ->B,A:Sized,B:Sized,{self.map(f)}}#[test]fntest_functor(){let z =Option::fmap(Some(1), |x| x +1).fmap(|x| x +1);// Return Option<B>assert_eq!(z,Some(3));// passes}
The Underlying Math
The confusing fact is that functors are morphisms in the category of categories. Really, this means thata functor from categoryC
intoD
preserves properties of the category, so that the data is somewhatpreserved.
Technically, every category has a functor into the simplest (non-empty) category (1): since the category1
justhas one object and one function, map all the objects and functions in whatever category you start from into thething in1
. So, data isn't quite preserved in a "nice" sense. Such functors are called forgetful sometimes asthey drop structure.
However, less forgetful examples provide more insight and empower useful statements about types.Unfortunately, these are rather heavy-handed in the mathematics they evoke.
An object with an of function that puts any single value into it.
#[derive(Debug,PartialEq,Eq)]enumMaybe<T>{Nothing,Just(T),}impl<T>Maybe<T>{fnof(x:T) ->Self{Maybe::Just(x)}}
Then use it like
let pointed_functor =Maybe::of(1);assert_eq!(pointed_functor,Maybe::Just(1));
Lifting in functional programming typically means to lift a function into a context (a Functor or Monad).For example, give a functiona -> b
and lift it into aList
then the signature wouldlook likeList[a] -> List[b]
.
Further Reading
- https://wiki.haskell.org/Lifting
- https://stackoverflow.com/questions/43482772/difference-between-lifting-and-higher-order-functions
- https://stackoverflow.com/questions/2395697/what-is-lifting-in-haskell/2395956
When an application is composed of expressions and devoid of side effects, truths about the system can be derived from the parts.
An set with a binary function that "combines" pairs from that set into another element of the set.
One simple monoid is the addition of numbers:
1 +1// i32: 2
In this case numbers are the set and+
is the function.
An "identity" value must also exist that when combined with a value doesn't change it.
The identity value for addition is0
.
1 +0// i32: 1
It's also required that the grouping of operations will not affect the result (associativity):
1 +(2 +3) ==(1 +2) +3// bool: true
Array concatenation also forms a monoid:
[vec![1,2,3],vec![4,5,6]].concat();// Vec<i32>: vec![1, 2, 3, 4, 5, 6]
The identity value is empty array[]
[vec![1,2],vec![]].concat();// Vec<i32>: vec![1, 2]
If identity and compose functions are provided, functions themselves form a monoid:
fnidentity<A>(a:A) ->A{ a}
foo
is any function that takes one argument.
compose(foo, identity) ≍ compose(identity, foo) ≍ foo
We can express Monoid as a Rust trait and the type signature would look like below
usecrate::applicative_example::Applicative;traitEmpty<A>{fnempty() ->A;}traitMonoid<A,F,B>:Empty<A> +Applicative<A,F,B>whereF:FnOnce(A) ->B,{}
According to Fantasy Land Specification, Monoid should implementEmpty
andApplicative
.
AMonad is a trait that implementsApplicative
andChain
specifications.chain
islikemap
except it un-nests the resulting nested object.
First,Chain
type can be implemented like below:
pubtraitChain<A,B>:HKT<A,B>{fnchain<F>(self,f:F) -> <SelfasHKT<A,B>>::TargetwhereF:FnOnce(A) -> <SelfasHKT<A,B>>::Target;}impl<A,B>Chain<A,B>forOption<A>{fnchain<F>(self,f:F) ->Self::TargetwhereF:FnOnce(A) -> <SelfasHKT<A,B>>::Target{self.and_then(f)}}
ThenMonad
itself can simply deriveChain
andApplicative
pubtraitMonad<A,F,B>:Chain<A,B> +Applicative<A,F,B>whereF:FnOnce(A) ->B{}impl<A,F,B>Monad<A,F,B>forOption<A>whereF:FnOnce(A) ->B{}#[test]fnmonad_example(){let x =Option::of(Some(1)).chain(|x|Some(x +1));assert_eq!(x,Some(2));// passes}
pure
is also known asreturn
in other functional languages.flat_map
is also known asbind
in other languages.
Importantly, it is worth noting that monads are a rather advanced topic in category theory. In fact, they are calledtriples by some as they involve adjoint functors and their unit -- both of which are rare to see in functional programming.The meme is to think of a monad as a burrito with "pure" being the act of taking a tortilla (the empty burrito) andadding ingredients using "chain".
The purely mathematical presentation of monads does not look anything like this, but there is an equivalence.
An object that hasextract
andextend
functions.
traitExtend<A,B>:Functor<A,B> +Sized{fnextend<W>(self,f:W) -> <SelfasHKT<A,B>>::TargetwhereW:FnOnce(Self) ->B;}traitExtract<A>{fnextract(self) ->A;}traitComonad<A,B>:Extend<A,B> +Extract<A>{}
Then we can implement these types for Option
impl<A,B>Extend<A,B>forOption<A>{fnextend<W>(self,f:W) ->Self::TargetwhereW:FnOnce(Self) ->B,{self.map(|x|f(Some(x)))}}impl<A>Extract<A>forOption<A>{fnextract(self) ->A{self.unwrap()// is there a better way to achieve this?}}
Extract takes a value out of a comonad.
Some(1).extract();// 1
Extend runs a function on the Comonad.
Some(1).extend(|co| co.extract() +1);// Some(2)
This can be thought of as the reverse of a monad. In fact, this is calledthe "dual" in category theory. (Basically, if you know whatA
is, acoA
is everything inA
's definition with the arrows reversed.)
An applicative functor is an object with anap
function.ap
applies a function in the object to a value in anotherobject of the same type. Given a pure programg: (b: A) -> B
, we must lift it tog: (fb: F<A>) -> F<B>
. In order to achievethis, we will introduce anotherhigher kinded type, calledHKT3
that is capable of doing this.
For this example, we will use Option datatype.
traitHKT3<A,B,C>{typeTarget2;}impl<A,B,C>HKT3<A,B,C>forOption<A>{typeTarget2 =Option<B>;}
Since Applicative implements Apply forap
andPure
forof
according toFantasy Land specificationwe must implement the types like below:
// ApplytraitApply<A,F,B>:Functor<A,B> +HKT3<A,F,B>whereF:FnOnce(A) ->B,{fnap(self,f: <SelfasHKT3<A,F,B>>::Target2) -> <SelfasHKT<A,B>>::Target;}impl<A,F,B>Apply<A,F,B>forOption<A>whereF:FnOnce(A) ->B,{fnap(self,f:Self::Target2) ->Self::Target{self.and_then(|v| f.map(|z|z(v)))}}// PuretraitPure<A>:HKT<A,A>{fnof(self) -> <SelfasHKT<A,A>>::Target;}impl<A>Pure<A>forOption<A>{fnof(self) ->Self::Target{self}}// ApplicativetraitApplicative<A,F,B>:Apply<A,F,B> +Pure<A>whereF:FnOnce(A) ->B,{}// Simply derives Apply and Pureimpl<A,F,B>Applicative<A,F,B>forOption<A>whereF:FnOnce(A) ->B,{}
Then we can use Option Applicative like this:
let x =Option::of(Some(1)).ap(Some(|x| x +1));assert_eq!(x,Some(2));
A function that preserves the structure of its domain.Seethe category definition from more.
The first few (endomorphism, homomorphism, and isomorphism) are easier tounderstand than the rest. The rest require the notion of an F-algebra.The simpler Haskell declarations are listed inthe wikipedia on paramorphismsbut the notions have yet to be extended to more general category theory.Briefly, the view of F-algebras is to take the set-theoretic definition of algebraicobjects and redefined them on a purely category theoretic footing: to move the ideasaway from sets containing elements to collections of objects with morphisms.However, some of the ideas here have yet to be generalised into this movement.
A function where the input type is same as the output.
// uppercase :: &str -> Stringlet uppercase = |x:&str| x.to_uppercase();// decrement :: i32 -> i32let decrement = |x:i32| x -1;
A pair of transformations between 2 types of objects that is invertible.
For example, 2D coordinates could be stored as a i32 vector [2,3] or a struct {x: 2, y: 3}.
#[derive(PartialEq,Debug)]structCoords{x:i32,y:i32,}let pair_to_coords = |pair:(i32,i32) |Coords{x: pair.0,y: pair.1};let coords_to_pair = |coords:Coords |(coords.x, coords.y);assert_eq!( pair_to_coords((1,2)),Coords{ x:1, y:2},);// passesassert_eq!( coords_to_pair(Coords{ x:1, y:2}),(1,2),);// passes
Isomorphisms are critical in making structures identical. Since we know that the struct above isidentical to a pair, all the functions that exist on the pair can exist on the struct. Iff
is the isomorphism andg
and endomorphism on the codomain:f^{-1} g f
would extendg
to apply on the domain.
A homomorphism is just a structure preserving map. It is the older term of morphism.In fact, a functor is just a homomorphism between categories as it preserves the original category's structure under the mapping.
assert_eq!(A::of(f).ap(A::of(x)),A::of(f(x)));// passesassert_eq!(Either::of(|x:&str| x.to_uppercase(x)).ap(Either::of("oreos")),Either::of("oreos".to_uppercase),);// passes
AreduceRight
function that applies a function against an accumulator and each value of the array (from right-to-left)to reduce it to a single value.
let sum = |xs:Vec<i32>| xs.iter().fold(0, |mut sum,&val|{ sum += val; sum});assert_eq!(sum(vec![1,2,3,4,5]),15);
Anunfold
function. Anunfold
is the opposite offold
(reduce
). It generates a list from a single value.
let count_down =unfold((8_u32,1_u32), |state|{let(refmut x1,refmut x2) =*state;if*x1 ==0{returnNone;}let next =*x1 -*x2;let ret =*x1;*x1 = next;Some(ret)});assert_eq!( count_down.collect::<Vec<u32>>(), vec![8,7,6,5,4,3,2,1],);
The combination of anamorphism and catamorphism.
It's the opposite of paramorphism, just as anamorphism is the opposite of catamorphism.Whereas with paramorphism, you combine with access to the accumulator and what has been accumulated,apomorphism lets you unfold with the potential to return early.
This is a set with an equivalence relation.
An object that has anequals
function which can be used to compare other objects of the same type.
It must obey following rules to beSetoid
a.equals(a) == true
(reflexivity)a.equals(b) == b.equals(a)
(symmetry)a.equals(b)
andb.equals(c)
thena.equals(c)
(transitivity)
Make a Vector a setoid:
Note that I am treatingSelf
/self
likea
.
traitSetoid{fnequals(&self,other:&Self) ->bool;}implSetoidforVec<i32>{fnequals(&self,other:&Self) ->bool{self.len() == other.len()}}assert_eq!(vec![1,2].equals(&vec![1,2]),true);// passes
In Rust standard library, it already providesEq, whichresembles Setoid that was discussed in this section. AlsoEqhasequals
implementations that covers a range of data structures that already exist in Rust.
An object or value that implements Ord specification, also implementsSetoid specification.
The Ord object or value must satisfy below rules for alla
,b
orc
:
- totality:
a <= b
orb <= a
- antisymmetric:
a <= b
andb <= a
, thena == b
- transivity:
a <= b
andb <= c
, thena <= c
Rust documentation for Ord can be found hereOrd
An object that has acombine
function that combines it with another object of the same type.
It must obey following rules to beSemigroup
a.add(b).add(c)
is equivalent toa.add(b.add(c))
(associativity)
use std::ops::Add;pubtraitSemigroup<M>:Add<M>{}assert_eq!( vec![1,2].add(&vec![3,4]), vec![1,2,3,4],);// passesassert_eq!( a.add(&b).add(&c), a.add(&b.add(&c)),);// passes
An object that has afoldr/l
function that can transform that object into some other type.
fold_right
is equivalent to Fantasy Land Foldable'sreduce
, which goes like:
fantasy-land/reduce :: Foldable f => f a ~> ((b, a) -> b, b) -> b
use fp_core::foldable::*;let k =vec![1,2,3];let result = k.reduce(0, |i, acc| i + acc);assert_eq!(result,6);
If you were to implementFoldable
manually, the trait of it would look like below
usecrate::hkt::HKT;usecrate::monoid::Monoid;pubtraitFoldable<A,B>:HKT<A,B>{fnreduce<F>(b:B,ba:F) -> <SelfasHKT<A,B>>::TargetwhereF:FnOnce(B,A) ->(B,B);fnfold_map<M,N,F>(m:M,fa:F) ->MwhereM:Monoid<N>,F:FnOnce(<SelfasHKT<A,B>>::URI) ->M;fnreduce_right<F>(b:B,f:F) -> <SelfasHKT<A,B>>::TargetwhereF:FnOnce(A,B) ->(B,B);}
A lens is a type that pairs a getter and a non-mutating setter for some other data structure.
traitLens<S,A>{fnover(s:&S,f:&Fn(Option<&A>) ->A) ->S{let result:A =f(Self::get(s));Self::set(result,&s)}fnget(s:&S) ->Option<&A>;fnset(a:A,s:&S) ->S;}#[derive(Debug,PartialEq,Clone)]structPerson{name:String,}#[derive(Debug)]structPersonNameLens;implLens<Person,String>forPersonNameLens{fnget(s:&Person) ->Option<&String>{Some(&s.name)}fnset(a:String,s:&Person) ->Person{Person{name: a,}}}
Having the pair of get and set for a given data structure enables a few key features.
let e1 =Person{name:"Jason".to_string(),};let name =PersonNameLens::get(&e1);let e2 =PersonNameLens::set("John".to_string(),&e1);let expected =Person{name:"John".to_string()};let e3 =PersonNameLens::over(&e1,&|x:Option<&String>|{match x{Some(y) => y.to_uppercase(),None =>panic!("T_T")// lol...}});assert_eq!(*name.unwrap(), e1.name);// passesassert_eq!(e2, expected);// passesassert_eq!(e3,Person{ name:"JASON".to_string()});// passes
Lenses are also composable. This allows easy immutable updates to deeply nested data.
structFirstLens;impl<A>Lens<Vec<A>,A>forFirstLens{fnget(s:&Vec<A>) ->Option<&A>{ s.first()}fnset(a:A,s:&Vec<A>) ->Vec<A>{unimplemented!()// Nothing to set in FirstLens}}let people =vec![Person{ name:"Jason"},Person{ name:"John"}];Lens::over(composeL!(FirstLens,NameLens),&|x:Option<&String>|{match x{Some(y) => y.to_uppercase(),None =>panic!("T_T")}}, people);// vec![Person { name: "JASON" }, Person { name: "John" }];
Further Reading
Every function in Rust will indicate the types of their arguments and return values.
// add :: i32 -> i32 -> i32fnadd(x:i32) ->implFn(i32)->i32{move |y| x + y}// increment :: i32 -> i32fnincrement(x:i32) ->i32{ x +1}
If a function accepts another function as an argument it is wrapped in parentheses.
// call :: (a -> b) -> a -> bfncall<A,B>(f:&Fn(A) ->B) ->implFn(A) ->B +'_{move |x|f(x)}
The lettersa
,b
,c
,d
are used to signify that the argument can be of any type.The following version of map takesa
function that transformsa
value of some typea
into another typeb
,an array of values of typea
, and returns an array of values of typeb
.
// map :: (a -> b) -> [a] -> [b]fnmap<A,B>(f:&Fn(A) ->B) ->implFn(A) ->B +'_{move |x|f(x)}
Further Reading
A composite type made from putting other types together. Two common classes of algebraic types aresum andproduct.
A Sum type is the combination of two types together into another one.It is called sum because the number of possible values in the result type is the sum of the input types.
Rust hasenum
that literally representsum
in ADT.
enumWeakLogicValues{True(bool),False(bool),HalfTrue(bool),}// WeakLogicValues = bool + otherbool + anotherbool
A product type combines types together in a way you're probably more familiar with:
structPoint{x:i32,y:i32,}// Point = i32 x i32
It's called a product because the total possible values of the data structure is the product of the different values.Many languages have a tuple type which is the simplest formulation of a product type.
See alsoSet Theory
Further Reading
Option is asum type with two cases often called Some and None.
Option is useful for composing functions that might not return a value.
letmut cart =HashMap::new();letmut item =HashMap::new();item.insert("price".to_string(),12);cart.insert("item".to_string(), item,);fnget_item(cart:&HashMap<String,HashMap<String,i32>>) ->Option<&HashMap<String,i32>>{ cart.get("item")}fnget_price(item:&HashMap<String,i32>) ->Option<&i32>{ item.get("price")}
Useand_then ormap to sequence functions that return Options
fnget_nested_price(cart:&HashMap<String,HashMap<String,i32>>) ->Option<&i32>{returnget_item(cart).and_then(get_price);}let price =get_nested_price(&cart);match price{Some(v) =>assert_eq!(v,&12),None =>panic!("T_T"),}
Option
is also known asMaybe
.Some
is sometimes calledJust
.None
is sometimes calledNothing
.
As a community, we have chosen our value as "learn by teaching".We want to share our knowledge with the world while we are learning.