DEV Community

Mike Skoe
Mike Skoe

Posted on • Updated on

Reselect, but in OCaml

The post was originally written here, so you can go there to play with the code

In this post, I'd like to share with you my implementation of reselect library and few related reflections.

Reselect is a library to make reusable and memoized selectors. Which allows you to put minimal data inside your state. I like the simplicity and the power of that concept, so I decided to build something similar myself.

Let me first put the working implementation. Then we will use it.

let memo ?eq:(eq=(=)) fn =
    let last_arg_res = ref None in
    (fun arg ->
        match !last_arg_res with
        | Some (last_arg, last_res) when eq last_arg arg -> last_res
        | _ ->
            let new_res = fn arg in
            last_arg_res := Some (arg, new_res);
            new_res
    )

type ('a, 'b) t = ('a -> 'b)

let return res = (fun _ -> res)

let id a = a

let (>>=) fn bind_fn =
    fun arg -> arg |> bind_fn @@ fn arg
Enter fullscreen mode Exit fullscreen mode

To make our code a little more elegant, we will prepare some utils.

let (>>) f1 f2 arg = f2 (f1 arg)
let percent all part =
  let part = float_of_int part in
  let all = float_of_int all in
  part /. all *. 100.
  |> int_of_float
Enter fullscreen mode Exit fullscreen mode

Now we are ready to play. Imagine an app to learn words.

module Word = struct
  type t = {
    language: string;
    spelling: string;
    translation: string;
    learned: bool;
  }

  module Get = struct
    let lang {language; _} = language
    let spell {spelling; _} = spelling
    let transl {translation; _} = translation
    let learned {learned; _} = learned
  end
end

module State = struct
  type t = {
    current_language: string;
    words: Word.t list;
  }

  module Get = struct
    let cur_lang {current_language; _} = current_language
    let words {words; _} = words

    let cur_words t = 
      let lang = cur_lang t in
      let words = words t in
      words
      |> List.filter (Word.Get.lang >> (=) lang)

    let learned_cur_words t =
      let words = cur_words t in
      words
      |> List.filter (Word.Get.learned)

    let progress t =
      let words_len = cur_words t |> List.length in
      let learned_words_len = learned_cur_words t |> List.length in
      percent words_len learned_words_len
  end
end

let sample: State.t = {
  current_language="nl";
  words=[
    { language="nl"; spelling="banana"; translation="banaan"; learned=true; };
    { language="eo"; spelling="winter"; translation="vintro"; learned=true; };
    { language="nl"; spelling="kindness"; translation="vriendelijkheid"; learned=false; };
    { language="nl"; spelling="rainbow"; translation="regenboog"; learned=false; };
  ]
}

let progress = State.Get.progress sample
Enter fullscreen mode Exit fullscreen mode

The state data is exposed through getters, which help us to easily get/calculate required data. It is extremely useful. But what if our progress getter could perform some heavy calculations? Performing the calculations every time is not the most efficient decision. Especially if state changes do not relate to data used inside progress getter. But we can rewrite the getter.

let (>>=) fn bind_fn = fn >>= (memo bind_fn) (* Without this line the ">>=" performs only a function composition, which is also nice *)

let progress =
  State.Get.cur_words >> List.length >>= fun words_len ->
  State.Get.learned_cur_words >> List.length >>= fun learned_words_len ->
  return (percent words_len learned_words_len)
Enter fullscreen mode Exit fullscreen mode

This way the getter looks a bit different, but now the body of progress getter will trigger only if words_len or learned_words_len will change. Great!

This concept lies in between things like reactive programming, incremental and lenses, but it focuses only on getting data, it can perform calculations along the way, and it does nothing, unless you ask for it.

Interesting points

  • Since composition is performed using function monad, we have no limits in the number of arguments
  • Memo can accept the optional argument, to customize memoization strategy
  • By default bind (>>=) function does not perform memoization, so we can apply different features to binding functions. Memoization is only an option

Things I've learned in the latest app

  • Hide types and expose setters/getters. It will help you to reason about your structure consistency and will allow you to compose multiple getters or setters
  • Try to fill your state only with the data you need right now. OCaml has variant types, it can help you to describe all possible scenarios and what data they will need
  • Cyclic dependencies can hurt your architecture OCaml and dune will warn you if you have cyclic dependencies. Sometimes it is so easy to fix, but tree-like dependencies will make your code more reusable, testable, and pure.

Discussion (0)