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.