Combinators

What is a combinator?

  • One meaning of “combinator” is a more informal sense referring to the combinator pattern, a style of organizing libraries centered around the idea of combining things. Usually there is some type T, some functions for constructing “primitive” values of type T, and some “combinators” which can combine values of type T in various ways to build up more complex values of type T. The other definition is “function with no free variables”. __ wiki.haskell.org

  • A combinator is a function which builds program fragments from program fragments; in a sense the programmer using combinators constructs much of the desired program automatically, rather that writing every detail by hand. __ John Hughes—Generalizing Monads to Arrows via Functional Programming Concepts

The exact definition of “combinators” in Rust ecosystem is bit unclear. 

  • or(), and(), or_else(), and_then()

    • Combine two values of type T and return same type T.
  • xor() for Option types

    • Combine two values of type T and return same type T, only if exactly one value is T
  • filter() for Option types

    • Filter type T by using a closure as a conditional function
    • Return same type T
  • map(), map_err()

    • Convert type T by applying a closure.
    • The data type of the value inside T can be changed. ex. Some<&str> can be converted to Some<usize> or Err<&str> to Err<isize> and etc.
  • map_or(), map_or_else()

    • Transform type T by applying a closure & return the value inside type T.
    • For None and Err, a default value or another closure is applied.
  • ok_or(), ok_or_else() for Option types

    • Transform Option type into a Result type.
  • as_ref(), as_mut()

    • Transform type T into a reference or a mutable reference.

or() and and()

While combining two expressions, which return either Option/ Result

  • or(): If either one got Some or Ok, that value returns immediately.
  • and(): If both got Some or Ok, the value in the second expression returns. If either one got None or Err that value returns immediately.

With Option

let s1 = Some("some1");
let s2 = Some("some2");
let n: Option<&str> = None;

assert_eq!(s1.or(s2), s1); // Some1 or Some2 = Some1
assert_eq!(s1.or(n), s1);  // Some or None = Some
assert_eq!(n.or(s1), s1);  // None or Some = Some
assert_eq!(n.or(n), n);    // None1 or None2 = None2

assert_eq!(s1.and(s2), s2); // Some1 and Some2 = Some2
assert_eq!(s1.and(n), n);   // Some and None = None
assert_eq!(n.and(s1), n);   // None and Some = None
assert_eq!(n.and(n), n);    // None1 and None2 = None1

With Result

let o1: Result<&str, &str> = Ok("ok1");
let o2: Result<&str, &str> = Ok("ok2");
let e1: Result<&str, &str> = Err("error1");
let e2: Result<&str, &str> = Err("error2");

assert_eq!(o1.or(o2), o1); // Ok1 or Ok2 = Ok1
assert_eq!(o1.or(e1), o1); // Ok or Err = Ok
assert_eq!(e1.or(o1), o1); // Err or Ok = Ok
assert_eq!(e1.or(e2), e2); // Err1 or Err2 = Err2

assert_eq!(o1.and(o2), o2); // Ok1 and Ok2 = Ok2
assert_eq!(o1.and(e1), e1); // Ok and Err = Err
assert_eq!(e1.and(o1), e1); // Err and Ok = Err
assert_eq!(e1.and(e2), e1); // Err1 and Err2 = Err1

xor()

While combining two Options, which return either Option, only if exactly one option is T.

The same Some type is returned, only if we pass only one Some value. None is returned, if both Some or None type. Rust support xor() only for Option types.

let s1 = Some("some1");
let s2 = Some("some2");
let n: Option<&str> = None;

assert_eq!(s1.xor(s2), n); // Some1 xor Some2 = None
assert_eq!(s1.xor(n), s1); // Some xor None = Some
assert_eq!(n.xor(s1), s1); // None xor Some = Some
assert_eq!(n.xor(n), n); // None1 xor None2 = None2

or_else()

Similar to or(). The only difference is, the second expression should be a closure which returns same type T.

With Option

let s1 = Some("some1");
let s2 = Some("some2");
let fn_some = || Some("some2"); // similar to: let fn_some = || -> Option<&str> { Some("some2") };

let n: Option<&str> = None;
let fn_none = || None;

assert_eq!(s1.or_else(fn_some), s1);  // Some1 or_else Some2 = Some1
assert_eq!(s1.or_else(fn_none), s1);  // Some or_else None = Some
assert_eq!(n.or_else(fn_some), s2);   // None or_else Some = Some
assert_eq!(n.or_else(fn_none), None); // None1 or_else None2 = None2

With Result

let o1: Result<&str, &str> = Ok("ok1");
let o2: Result<&str, &str> = Ok("ok2");
let fn_ok = |_| Ok("ok2"); // similar to: let fn_ok = |_| -> Result<&str, &str> { Ok("ok2") };

let e1: Result<&str, &str> = Err("error1");
let e2: Result<&str, &str> = Err("error2");
let fn_err = |_| Err("error2");

assert_eq!(o1.or_else(fn_ok), o1);  // Ok1 or_else Ok2 = Ok1
assert_eq!(o1.or_else(fn_err), o1); // Ok or_else Err = Ok
assert_eq!(e1.or_else(fn_ok), o2);  // Err or_else Ok = Ok
assert_eq!(e1.or_else(fn_err), e2); // Err1 or_else Err2 = Err2

and_then()

Similar to and(). The only difference is, the second expression should be a closure which returns same type T.

With Option

let s1 = Some("some1");
let s2 = Some("some2");
let fn_some = |_| Some("some2"); // similar to: let fn_some = |_| -> Option<&str> { Some("some2") };

let n: Option<&str> = None;
let fn_none = |_| None;

assert_eq!(s1.and_then(fn_some), s2); // Some1 and_then Some2 = Some2
assert_eq!(s1.and_then(fn_none), n);  // Some and_then None = None
assert_eq!(n.and_then(fn_some), n);   // None and_then Some = None
assert_eq!(n.and_then(fn_none), n);   // None1 and_then None2 = None1

With Result

let o1: Result<&str, &str> = Ok("ok1");
let o2: Result<&str, &str> = Ok("ok2");
let fn_ok = |_| Ok("ok2"); // similar to: let fn_ok = |_| -> Result<&str, &str> { Ok("ok2") };

let e1: Result<&str, &str> = Err("error1");
let e2: Result<&str, &str> = Err("error2");
let fn_err = |_| Err("error2");

assert_eq!(o1.and_then(fn_ok), o2);  // Ok1 and_then Ok2 = Ok2
assert_eq!(o1.and_then(fn_err), e2); // Ok and_then Err = Err
assert_eq!(e1.and_then(fn_ok), e1);  // Err and_then Ok = Err
assert_eq!(e1.and_then(fn_err), e1); // Err1 and_then Err2 = Err1

filter()

💡 Usually in programming languages filter functions are used with arrays or iterators to create a new array/ iterator by filtering own elements via a function/ closure. Rust also provides filter() as an iterator adaptor to apply a closure on each element of an iterator to transform it into another iterator. However in here we are talking about the functionality of filter() with Option types.

The same Some type is returned, only if we pass a Some value and the given closure returned true for it. None is returned, if None type passed or the closure returned false. The closure uses the value inside Some as an argument. Still Rust support filter() only for Option types.

let s1 = Some(3);
let s2 = Some(6);
let n = None;

let fn_is_even = |x: &i8| x % 2 == 0;

assert_eq!(s1.filter(fn_is_even), n);  // Some(3) -> 3 is not even -> None
assert_eq!(s2.filter(fn_is_even), s2); // Some(6) -> 6 is even -> Some(6)
assert_eq!(n.filter(fn_is_even), n);   // None -> no value -> None

map() and map_err()

💡 Usually in programming languages map() functions are used with arrays or iterators, to apply a closure on each element of the array or iterator. Rust also provides map() as an iterator adaptor to apply a closure on each element of an iterator to transform it into another iterator. However in here we are talking about the functionality of map() with Option and Result types.

  • map() : Convert type T by applying a closure. The data type of Some or Ok blocks can be changed according to the return type of the closure. Convert Option<T> to Option<U>, Result<T, E> to Result<U, E>

⭐ Via map(), only Some and Ok values are getting changed. No affect to the values inside Err (None doesn’t contain any value at all).

With Option

let s1 = Some("abcde");
let s2 = Some(5);

let n1: Option<&str> = None;
let n2: Option<usize> = None;

let fn_character_count = |s: &str| s.chars().count();

assert_eq!(s1.map(fn_character_count), s2); // Some1 map = Some2
assert_eq!(n1.map(fn_character_count), n2); // None1 map = None2

With Result

let o1: Result<&str, &str> = Ok("abcde");
let o2: Result<usize, &str> = Ok(5);

let e1: Result<&str, &str> = Err("abcde");
let e2: Result<usize, &str> = Err("abcde");

let fn_character_count = |s: &str| s.chars().count();

assert_eq!(o1.map(fn_character_count), o2); // Ok1 map = Ok2
assert_eq!(e1.map(fn_character_count), e2); // Err1 map = Err2
  • map_err() for Result types : The data type of Err blocks can be changed according to the return type of the closure. Convert Result<T, E> to Result<T, F>.

⭐ Via map_err(), only Err values are getting changed. No affect to the values inside Ok.

let o1: Result<&str, &str> = Ok("abcde");
let o2: Result<&str, isize> = Ok("abcde");

let e1: Result<&str, &str> = Err("404");
let e2: Result<&str, isize> = Err(404);

let fn_character_count = |s: &str| -> isize { s.parse().unwrap() }; // convert str to isize

assert_eq!(o1.map_err(fn_character_count), o2); // Ok1 map = Ok2
assert_eq!(e1.map_err(fn_character_count), e2); // Err1 map = Err2

map_or() and map_or_else()

Hope you remember the functionality of unwrap_or() and unwrap_or_else() functions. These functions also bit similar to them. But map_or() and map_or_else() apply a closure on Some and Ok values and return the value inside type T.

  • map_or() : Support only for Option types (not supporting Result). Apply the closure to the value inside Some and return the output according to the closure. The given default value is returned for None types.
const V_DEFAULT: i8 = 1;

let s = Some(10);
let n: Option<i8> = None;
let fn_closure = |v: i8| v + 2;

assert_eq!(s.map_or(V_DEFAULT, fn_closure), 12);
assert_eq!(n.map_or(V_DEFAULT, fn_closure), V_DEFAULT);
  • map_or_else() : Support for both Option and Result types. Similar to map_or() but should provide another closure instead a default value for the first parameter.

None types doesn’t contain any value. So no need to pass anything to the closure as input with Option types. But Err types contain some value inside it. So default closure should able to read it as an input, while using this with Result types.

let s = Some(10);
let n: Option<i8> = None;

let fn_closure = |v: i8| v + 2;
let fn_default = || 1; // None doesn't contain any value. So no need to pass anything to closure as input.

assert_eq!(s.map_or_else(fn_default, fn_closure), 12);
assert_eq!(n.map_or_else(fn_default, fn_closure), 1);
let o = Ok(10);
let e = Err(5);

let fn_closure = |v: i8| v + 2;
let fn_default_for_result = |v: i8| v + 1; // Err contain some value inside it. So default closure should able to read it as input

assert_eq!(o.map_or_else(fn_default_for_result, fn_closure), 12);
assert_eq!(e.map_or_else(fn_default_for_result, fn_closure), 6);

ok_or() and ok_or_else()

As mentioned earlier, ok_or(), ok_or_else() transform Option type into Result type. Some to Ok and None to Err.

  • ok_or() : A default Err message should pass as argument.
const ERR_DEFAULT: &str = "error message";

let s = Some("abcde");
let n: Option<&str> = None;

let o: Result<&str, &str> = Ok("abcde");
let e: Result<&str, &str> = Err(ERR_DEFAULT);

assert_eq!(s.ok_or(ERR_DEFAULT), o); // Some(T) -> Ok(T)
assert_eq!(n.ok_or(ERR_DEFAULT), e); // None -> Err(default)
  • ok_or_else() : Similar to ok_or(). A closure should be passed as the argument.
let s = Some("abcde");
let n: Option<&str> = None;
let fn_err_message = || "error message";

let o: Result<&str, &str> = Ok("abcde");
let e: Result<&str, &str> = Err("error message");

assert_eq!(s.ok_or_else(fn_err_message), o); // Some(T) -> Ok(T)
assert_eq!(n.ok_or_else(fn_err_message), e); // None -> Err(default)

as_ref() and as_mut()

🔎 As mentioned earlier, these functions are used to borrow type T as a reference or as a mutable reference.

  • as_ref() : Convert Option<T> to Option<&T> and Result<T, E> to Result<&T, &E>
  • as_mut() : Converts Option<T> to Option<&mut T> and Result<T, E> to Result<&mut T, &mut E>