Following is a continuation of the topic of modular implicits , introduced in the previous post on implicit functors. This time we’ll look at how the extension can help simplifying lenses. I covered lenses in OCaml lenses via modules, where a rather verbose definition of a (van Laarhoven) lens was given in the form of a module signature LENS:

module type LENS = sig
  type a
  type b
  module Mk : functor (F : FUNCTOR) -> sig
    val run : (b -> b F.t) -> a -> a F.t
  end
end

With modular implicits, much of the clunkiness will go away. At first, let’s cover some ground and bring into scope a couple of utility functions - a module type for representing functors and two functor instances (for identity and constant):

let id x = x

let const x _ = x

module type TYPE  = sig type t end

module type FUNCTOR = sig
  type 'a t
  val map : ('a -> 'b) -> 'a t -> 'b t
end

module IdFunctor = struct
  type 'a t = 'a
  let map f x = f x
end

module ConstFunctor (T : TYPE) = struct
  type 'a t = T.t
  let map _ x = x
end

All of the definitions are vanilla OCaml and were also described in the previous lens post.

The reason the lens representation required a module signature rather than a simple type is due to OCaml’s inability to parameterize over higher-kinded types. Powered by modular implicits however, there is a work around; We are now able to define lens as a function type:

type ('a, 'b) lens = {F : FUNCTOR} -> ('b -> 'b F.t) -> 'a -> 'a F.t

Note that the implicit argument F is available in scope for other arguments as well as the return type of the function. This is not achievable with normal first class modules in OCaml. More specifically, the following type construction is invalid:

(* Does not compile :( *)
type ('a, 'b) lens = (F : FUNCTOR) -> ('b -> 'b F.t) -> 'a -> 'a F.t

Comparing with the previous version, the view and modify functions used for extracting and updating values respectively are also simplified:

let view (type a) (type b) (l : (a, b) lens) (x : a) : b =
  let module C = ConstFunctor (struct type t = b end) in
  l {C} id x

let modify (type a) (type b) (l : (a, b) lens) (f  : b -> b) (x : a) : a =
  l {IdFunctor} f x

Viewing a lens is accomplished by using the ConstFunctor, instantiated with the concrete type parameter b in order to smuggle out the value that the lens is pointing to. The function modify instead relies on IdFunctor for updating the value.

A utility, set, is introduced for convenience:

let set l x = modify l (const x)

Since lenses are functions, lens composition is nothing but function composition:

let compose (l2 : ('b, 'c) lens) (l1 : ('a, 'b) lens) : ('a, 'c) lens =
  fun { F : FUNCTOR } f x -> l1 {F} (l2 {F} f) x

let (//) l1 l2 = compose l2 l1

To see how the pieces fit together, let’s take look at some concrete examples. Consider the following custom types:

type address = { street : string ; number : int}

type person = { name : string; age : int; address : address }

type compnay = { name : string; ceo : person }

We first introduce lenses for some of the properties manually:

let ceo { F : FUNCTOR } f x = F.map (fun ceo -> { x with ceo }) @@ f x.ceo

let address { F : FUNCTOR } f x =
  F.map (fun address -> { x with address }) @@ f x.address

let street { F : FUNCTOR } f x =
  F.map (fun street -> { x with street }) @@ f x.street

Although an improvement over the module based approach, lenses for record properties still require boiler plate code and should rather be automated by a deriving mechanism.

Now, given a value of type company:

let my_company = {
  name = "Lens Inc";
  ceo = {
    name = "Mary";
    age = 62;
    address = {
      street = "Highstreet";
      number = 13;
    }
  }
}

Using the lenses from above, here is how to update the street component of my_company using a composed lens and the set function:

set (ceo // address // street) "Wallstreet" my_company;;

This code results in the following value:

{
  name = "Lens Inc";
  ceo = {
    name = "Mary";
    age = 62;
    address = {
      street = "Wallstreet";
      number = 13
     }
  }
}

As you may have observed, there is not a single implicit module in the code above - the value proposition of modular implicit goes beyond sparing an extra argument to a function.

Complete code here.