Bastet

Bastet is a ReasonML/Ocaml library for category theory and abstract algebra. The entry point of this library is the module: Bastet.

Project Layout

There are also implementations for these builtin Ocaml data types:

Who is this for?

This library is intended for those who want to write highly reusable functional code. Category theory and abstract algebra is a perfect foundation for this because of functional programming's close relationship with pure math — functions in this context are closer to the mathematical sense of the word rather than something like a subroutine.

But unless your entire team is already familiar with these principles, it's recommended that library authors provide a concrete API that doesn't expose this level of abstraction for newcomers.

For example, consider this abstraction that generalizes the notion of something similar to the Bastet.Result.hush function but instead returns a unit type for the second type argument instead of an option type:

# #require "bastet";;
# open Bastet;;

# module Hush (B : Interface.BIFUNCTOR) =
  struct let hush bifunctor = bifunctor |> B.bimap Function.Category.id (Function.const ()) end;;
module Hush :
  functor (B : Bastet.Interface.BIFUNCTOR) ->
    sig val hush : ('a, 'b) B.t -> ('a, unit) B.t end

# module Hush_Result = Hush (Result.Bifunctor)
module Hush_Result : sig val hush : ('a, 'b) result -> ('a, unit) result end

# let hush = Hush_Result.hush
val hush : ('a, 'b) result -> ('a, unit) result = <fun>

In this example, the (abstract) Hush functor would be exposed to other library authors while the (concrete) hush function with some docs would be exposed only to the newcomers.

This way anyone can still take advantage of the derived implementations provided by this abstraction without any of the learning curve — they can just read the type signature of the hush function and ignore the other stuff. And the functor could be reused for some other type that takes two parameters, such as a tuple, map, or record.

A Succinct Introduction

The downside to using such abstractions is that newcomers are often overwhelmed. They might not know how much category theory or abstract algebra they need to formally know (almost none), or whether or not to read monad tutorials (you shouldn't). The best way to learn this stuff is to read the module types and the laws that the types need to satisfy. This succinct and hopeful more useful introduction is ideally all you will need to understand the core concepts and be productive.

You can try all of the examples here with utop.

Functors

Functors generalize the notion of something that's composably mappable. You're probably already familiar with this concept from javascript's Array.map function.

Here's what it looks like in Ocaml:

# Array.map (( * ) 2) [|1; 2; 3|];;
- : int array = [|2; 4; 6|]

# Array.map string_of_int [|1; 2; 3|];;
- : string array = [|"1"; "2"; "3"|]

In order for a type to be a functor, it needs to have a map function that also satisfies two laws. The laws ensure that the map function works well with function composition:

Thsee laws describe what's meant by composably mappable: the map function together with function composition (compose) should be sort of right-distributive and work together in a nice way.

Here are some concerete examples:

Magmas

Magmas are a very simple concept: it's just a set (or type) with a single binary operation, and that binary operation must be closed under that set. That just means it takes two arguments of the same type and returns the same type. You're already familiar with several magmas:

# (+);;
- : int -> int -> int = <fun>
# (-);;
- : int -> int -> int = <fun>
# (+.);;
- : float -> float -> float = <fun>
# (/.);;
- : float -> float -> float = <fun>
# (&&);;
- : bool -> bool -> bool = <fun>
# (||);;
- : bool -> bool -> bool = <fun>

No additional laws or conditions need to be satisfied. This is extremely useful when you want to be able to write abstractions that work for any binary operation that's closed:

# module List_Apply (M: Interface.MAGMA) =
  struct let apply init list = ListLabels.fold_left ~f:M.append ~init list end;;
module List_Apply :
  functor (M : Bastet.Interface.MAGMA) ->
    sig val apply : M.t -> M.t list -> M.t end

# module List_Additive_Int_Apply = List_Apply(Int.Additive.Magma);;
module List_Additive_Int_Apply : sig val apply : int -> int list -> int end

# module List_Subtractive_Int_Apply = List_Apply(Int.Subtractive.Magma);;
module List_Subtractive_Int_Apply : sig val apply : int -> int list -> int end

# let adder = List_Additive_Int_Apply.apply;;
val adder : int -> int list -> int = <fun>

# let subtractor = List_Subtractive_Int_Apply.apply;;
val subtractor : int -> int list -> int = <fun>

# [1; 2; 3] |> adder 0 = (0 + 1 + 2 + 3);;
- : bool = true

# [1; 2; 3] |> subtractor 10 = (10 - 1 - 2 - 3);;
- : bool = true

This example provides a generic functor that works on any magma. Some concrete instances are constructed for Bastet.Int.Additive.Magma and Bastet.Int.Subtractive.Magma.

As you can see it can be pretty useful. One of the benefits of magmas is that it's very general because you don't need to satisfy any additional laws.

You might ask: why not just use monoids? Integer subtraction (-) can't be a monoid because although it has an empty element (0), it's not associative — the evaluation order of the operations matter:

# 10 - (1 - 2) = (10 - 1) - 2;;
- : bool = false

And obviously you'll probably eventually run into a situation where you need to subtract everything in a list or array starting from some initial value, which is where magmas shine. Monoids will be exlpained more in the next section.

Monoidal Categories

- "A monad is just a monoid in the category of endofunctors, what's the problem?"

This quote is often cited in the FP community as a joke. The joke is that the sentence sounds extremely complicated because of the jargon, but it's actually the easiest way of understanding monads.

By the end of this section you'll learn about the different categories of monoids (like monads) and why monads are used for IO. The goal ideally is that you'll leave this section without feeling the need to pick up any other resource for monads because you get it now.

General Overview

This section provides a general overview of monoids. This is one subsection that you might not get immediately. Because of the level of abstraction, it's recommended that you don't spend a lot of time on this section, and instead come back to it after reading the subsequent sections and writing code that uses this stuff to get a feel for it.

The idea of a monoid is to represent something that's appendable or combinable in a general way.

Monoids come up constantly in programming because we're more or less just combining things: a construction worker is a civic developer, and for the most part we're white-collar construction workers (software developers).

You can obviously combine things that are of different types, otherwise we wouldn't be able to build anything really interesting or useful with software. A monoid specifically describes combining something of the same type and returning the same type.

We know that we need a magma because we're taking two things of the same type and returning the same type. But as you saw in the previous section, a magma isn't enough to describe something that's appendable because subtraction is a magma, and subtraction doesn't combine or add two things together, it takes something away.

What's really needed is a magma that satisfies two laws in order to truly describe something that's appendable:

Monoid

After reading the previous section, you might not have understood everything fully and that's ok. It's best to see some concrete examples of functions that you've often used in order to get a wholistic perspective of what's going on.

Plus

Bastet.Interface.PLUS is a monoid in the category of functors.

Unlike a monoid, it's a functor so it's type has a type parameter 'a: instead of having a type t where t is a concerete type (ie: string, int), it instead has a type 'a t where the 'a can be anything (ie: 'a list, 'a option).

It satisfies the monoid laws at the functor level:

Monad

Bastet.Interface.MONAD is a monoid in the category of endofunctors.

Monads are endofunctors: they're functors that have a pure function that can be thought of as a constructor function, and a flatten function that flattens the nested type.

It's common practice to provide a flat_map function directly instead of a flatten function. The flat_map function can be derived from flat_map f = compose flatten (map f), and vice-versa. For the purposes of this section, it's best to think of monads as functors that provide additional pure and flatten functions and satisfies the monoid laws.

Example

Here's an example of Bastet.Option.Monad:

# let compose f g x = f (g x);;
val compose : ('a -> 'b) -> ('c -> 'a) -> 'c -> 'b = <fun>

# let pure a = Some a;;
val pure : 'a -> 'a option = <fun>

# let map f = function
  | Some value -> Some (f value)
  | None -> None;;
val map : ('a -> 'b) -> 'a option -> 'b option = <fun>

# let flatten: 'a option option -> 'a option = function
  | Some value -> value
  | None -> None;;
val flatten : 'a option option -> 'a option = <fun>

# let flat_map f = compose flatten (map f);;
val flat_map : ('a -> 'b option) -> 'a option -> 'b option = <fun>

# let (>>=) a b = flat_map b a;;
val ( >>= ) : 'a option -> ('a -> 'b option) -> 'b option = <fun>


# (* Another style of working with monads instead of flat_map is to use
     kliesli composition: *)
  let kliesli_compose f g a = f a >>= g;;
val kliesli_compose :
  ('a -> 'b option) -> ('b -> 'c option) -> 'a -> 'c option = <fun>

# let (>=>) = kliesli_compose;;
val ( >=> ) : ('a -> 'b option) -> ('b -> 'c option) -> 'a -> 'c option =
  <fun>


# (* Here's an example use case of option types as monads - getting deeply nested
     optional properties: *);;

# type user_details = { phone_number: int option; address: string option };;
type user_details = { phone_number : int option; address : string option; }

# type user = { name: string; details: user_details option };;
type user = { name : string; details : user_details option; }

# let fetch_user name =
  match name with
  | "Risto" ->
    Some { name; details = Some { phone_number = None; address = Some "123 Foo St" } }
  | _ -> None;;
val fetch_user : string -> user option = <fun>

# let get_details user = user.details;;
val get_details : user -> user_details option = <fun>

# let get_address details = details.address;;
val get_address : user_details -> string option = <fun>

# (* With monads, getting values from deeply nested optional values is trivial: *)
  fetch_user "Risto" >>= get_details >>= get_address;;
- : string option = Some "123 Foo St"

# (* It's also trivial to turn this into a function with kliesli composition,
     which can makes things easier to read: *)
  let fetch_user_address = fetch_user >=> get_details >=> get_address;;
val fetch_user_address : string -> string option = <fun>

# fetch_user_address "Risto";;
- : string option = Some "123 Foo St"

As you can see in the example, kliesli_compose can be derived entirely from flat_map.

Monoid Laws

Monads satisfy the monoid laws at the endofunctor level:

# #require "bastet"
# open Bastet;;

# let (>=>) = Option.Infix.(>=>);;
val ( >=> ) : ('a -> 'b option) -> ('b -> 'c option) -> 'a -> 'c option =
  <fun>

# let to_positive_int = function
  | x when x > 0 -> Some x
  | _ -> None;;
val to_positive_int : int -> int option = <fun>

# let to_even_int = function
  | x when x mod 2 = 0 -> Some x
  | _ -> None;;
val to_even_int : int -> int option = <fun>

# let empty = Option.Monad.pure;;
val empty : 'a -> 'a option = <fun>

# let satisfies_identity_law f value = (f >=> empty) value = f value;;
val satisfies_identity_law : ('a -> 'b option) -> 'a -> bool = <fun>

# let satisfies_associative_law f g h value =
  (f >=> (g >=> h)) value = ((f >=> g) >=> h) value;;
val satisfies_associative_law :
  ('a -> 'b option) -> ('b -> 'c option) -> ('c -> 'd option) -> 'a -> bool =
  <fun>

# satisfies_identity_law int_of_string_opt "123";;
- : bool = true
# satisfies_identity_law to_positive_int 123;;
- : bool = true
# satisfies_identity_law to_even_int 123;;
- : bool = true

# satisfies_associative_law int_of_string_opt to_positive_int to_even_int "123";;
- : bool = true
# satisfies_associative_law int_of_string_opt to_positive_int to_even_int "foo";;
- : bool = true

# let string_to_positive_even_int =
  int_of_string_opt >=> to_positive_int >=> to_even_int;;
val string_to_positive_even_int : string -> int option = <fun>
# string_to_positive_even_int "foo";;
- : int option = None
# string_to_positive_even_int "123";;
- : int option = None
# string_to_positive_even_int "124";;
- : int option = Some 124
IO

A good introduction on why monads are used for IO is to consider how javascript handles effects: using callbacks. This is called continuation passing style.

A motivation for developing promises was to avoid callback hell:

doSomeEffect(cb => {
  // do more stuff
  doSomeOtherEffect(cb => {
    // do more stuff
    doYetAnotherEffect(cb => {
      // do more stuff
    })
  })
})

This example shows how you can use monads to flatten callbacks without having to use promises:

# (* IO as a callback *)
  type 'a io = ('a -> unit) -> unit;;
type 'a io = ('a -> unit) -> unit

# let pure (value: 'a): 'a io = fun cb -> cb value;;
val pure : 'a -> 'a io = <fun>

# let map (f: 'a -> 'b) (x: 'a io): 'b io =
  fun cb -> x (fun value -> cb (f value));;
val map : ('a -> 'b) -> 'a io -> 'b io = <fun>

# let flat_map (f: 'a -> 'b io) (x: 'a io): 'b io =
  fun cb -> x (fun value -> (f value) (fun value2 -> cb value2));;
val flat_map : ('a -> 'b io) -> 'a io -> 'b io = <fun>

# let (>>=) a b = flat_map b a;;
val ( >>= ) : 'a io -> ('a -> 'b io) -> 'b io = <fun>

# let (>=>) f g a = f a >>= g;;
val ( >=> ) : ('a -> 'b io) -> ('b -> 'c io) -> 'a -> 'c io = <fun>

# (* Some stubbed functions, assume they do something async: *);;
# let some_effect (x: int): int io = fun cb -> cb (x * 2);;
val some_effect : int -> int io = <fun>

# let some_other_effect (x: int): int io = fun cb -> cb (x + 10);;
val some_other_effect : int -> int io = <fun>

# let yet_another_effect (x: int): string io = fun cb -> cb (string_of_int x);;
val yet_another_effect : int -> string io = <fun>


# let empty = pure;;
val empty : 'a -> 'a io = <fun>

# let satisfies_identity_law f arg =
  ((f >=> empty) arg) (fun value1 -> begin
    (f arg) (fun value2 -> Printf.printf "%B\n" (value1 = value2))
  end);;
val satisfies_identity_law : ('a -> 'b io) -> 'a -> unit = <fun>

# let satisfies_associative_law f g h arg =
  ((f >=> (g >=> h)) arg) (fun value1 -> begin
    (((f >=> g) >=> h) arg) (fun value2 -> Printf.printf "%B\n" (value1 = value2))
  end);;
val satisfies_associative_law :
  ('a -> 'b io) -> ('b -> 'c io) -> ('c -> 'd io) -> 'a -> unit = <fun>

# satisfies_identity_law some_effect 123;;
true
- : unit = ()
# satisfies_identity_law some_other_effect 123;;
true
- : unit = ()
# satisfies_identity_law yet_another_effect 123;;
true
- : unit = ()

# satisfies_associative_law some_effect some_other_effect yet_another_effect 123;;
true
- : unit = ()

As you can see, monads are very useful for IO. Haskell and Purescript more or less uses some variation of this to achieve pure FP with their IO constructs. Lazy semantics are simulated with functions in Bucklescript and Purescript, and what Purescript does is it keeps the side-effects in a function, threads these functions together using flat_map without evaluating it yet by wrapping the computation in a function, and then the runtime (calling main) or unsafePerformIO will run the side-effects by calling the function. This makes the language "pure" because only the runtime or the unsafe escape hatch can run the effects.

If you want to see an example of how Purescript-like lazy effects are written in Ocaml, see Bs-effects.

Category

Category satisfies the monoid laws at the category level.

The simples example is Bastet.Function.Category, where empty is the identity function and append is function composition:

# let empty x = x;;
val empty : 'a -> 'a = <fun>

# let append f g x = f (g x);;
val append : ('a -> 'b) -> ('c -> 'a) -> 'c -> 'b = <fun>

# let satisfies_identity_law = (append string_of_int empty) 123 = string_of_int 123;;
val satisfies_identity_law : bool = true

# let satisfies_associative_law =
  (append string_of_int (append ((+) 1) (( * ) 2))) 123 =
  (append (append string_of_int ((+) 1)) (( * ) 2)) 123;;
val satisfies_associative_law : bool = true

Comparisons

The comparison interfaces Bastet.Interface.EQ and Bastet.Interface.ORD are self-explanatory: use the EQ interface to define how to determine whether two values of a type are equal, and the ORD interface to define whether two values are greater-than, less-than or equal.

Rings

Types that you can both add and multiply are Bastet.Interface.SEMIRINGs. If you include subtract you get a Bastet.Interface.RING, include divide to that and you get a Bastet.Interface.DIVISION_RING, and add modulo to that and you get a Bastet.Interface.EUCLIDEAN_RING.

These are predominantly useful in situations where monoids aren't enough and you want to be able to use ints and floats without having to write the same function over again.

Lattices

The most common lattice-like interface is Bastet.Interface.BOOLEAN_ALGEBRA with bool being the most well-known implementation of it.

Other lattice-like structures in programming come up occasionally. For example, Bastet.Interface.JOIN_SEMILATTICEs are used to describe propagator networks, and Bastet.Interface.HEYTING_ALGEBRAs are useful for describing fuzzy logics which don't satisfy the law of excluded middle.

Further Reading

You might be wondering if there's more to it then what's described here. For the current state of using these kinds of things in FP there really isn't, but it's fascinating stuff on it's own.

Wikipedia actually has some pretty nice articles on abstract algebra that are accessible to people who don't have a strong background in math. Reading these is highly recommended:

And some of the algebraic laws:

There are also different types of relations/relational properties. These are used for the Bastet.Interface.EQ and Bastet.Interface.ORD interfaces, but they also show up with lattice-like structures. They're also good to know if you plan on doing relational or logic programming:

PPX Let

You can integrate monads with ppx_let, a ppx rewriter that provides do notation sugar for Bastet.Interface.MONADs. The rewriter expects a Let_syntax module to be in scope, which you can construct using PPX_Let.Make, like so:

# #require "fmt";;
# module OptionLet = PPX_Let.Make(Option.Monad);;
# let add_optionals = fun x y ->
    let open OptionLet in
    let%bind x' = x in
    let%bind y' = y in
    Some (x' + y');;
# Fmt.pr "%a" (Fmt.option Fmt.int) @@ add_optionals (Some 123) (Some 456);;
- : int option = Some 579

For bucklescript there is the bs-let library.

Background

The initial inspiration for the library came from a sequence demo on the FP slack by rightfold (Chloe) on how to simulate higher kinded types in Ocaml.

The realization was that there was a better way and that it was possible to write a decent interface in Ocaml using local opens, functors, and avoiding monoid instances for containerized types by relying on their monoidal categories instead, such as Bastet.Interface.PLUS, Bastet.Interface.MONAD, and Bastet.Interface.CATEGORY.