Polymorphic Update with van Laarhoven Lenses

2012-06-23T10:49:01Z
import Control.Applicative (Const(..), (<$>))
import Control.Monad.Identity (Identity(..))
import Data.Functor.Compose (Compose(..))

There has been a lot of interest in record proposals for Haskell lately. Many people want to integrate lenses with the record system, but one problem is that most lens libraries are incompatible with polymorphic record updating.

Here is a simple example of polymorphic record updating. Suppose we make the following simple polymorphic record type.

data Pair a b = Pair {pi1 :: a, pi2 :: b} deriving Show

examplePair :: Pair Int Char
examplePair = Pair {pi1 = 1, pi2 = 'b'}

In Haskell, we can then update examplePair (by making an updated copy) as follows.

λ> examplePair {pi1 = "a"}
Pair {pi1 = "a", pi2 = 'b'}

Notice the type has changed from Pair Int Char to Pair String Char. This is what we mean by polymorphic record updates. Polymorphic record updates allows records with type parameters to be updated in such a way that their type parameters change. Most lens libraries do not support polymorphic record updates.

However, I recently realized that van Laarhoven style lenses do support polymorphic record updates. In fact, they support it right out of the box. We would have seen this in Twan’s original blog post had he not given a type alias for them.

Imagine this alternative history of Haskell. It is 1996, and the Haskell committee is about to introduce record syntax into Haskell 1.3. However, a (very) young Twan van Laarhoven joins the Haskell committee with fresh ideas. Instead of introducing field names as projection functions pi1 :: Pair a b -> a and pi2 :: Pair a b -> b, he pursuade them to give field names the following unusual definitions instead.

pi1 :: Functor f => (a -> f a') -> Pair a b -> f (Pair a' b)
pi1 f (Pair a b) = (\x -> Pair x b) <$> f a

pi2 :: Functor f => (b -> f b') -> Pair a b -> f (Pair a b')
pi2 f (Pair a b) = (\x -> Pair a x) <$> f b

These are the van Laarhoven lenses for the first and second fields of Pair. The Haskell committee adds some new Prelude functions to support these lenses.

-- getter
infixl 8 ^.
(^.) :: a -> ((b -> Const b b') -> a -> Const b a') -> b
x ^. l = getConst $ l Const x

-- modifier
infixr 4 %=
(%=) :: ((b -> Identity b') -> a -> Identity a') -> (b -> b') -> a -> a'
l %= f = runIdentity . l (Identity . f)

-- setter
infixr 4 ^=
(^=) :: ((b -> Identity b') -> a -> Identity a') -> b' -> a -> a'
l ^= v = l %= (const v)

Now we can do field access and simple polymorphic field updates:

λ> examplePair ^. pi1
1

λ> examplePair ^. pi2
'b'

λ> (pi1 ^= "a") examplePair 
Pair {pi1 = "a", pi2 = 'b'}

λ> (pi1 ^= "a") . (pi2 %= fromEnum) $ examplePair 
Pair {pi1 = "a", pi2 = 98}
Moreover, we get lens composition. In fact, van Laarhoven lenses compose with the composition function (.) and id is the identity lens. Suppose we define the following nested record.
nestedPair :: Pair (Pair Int Char) String
nestedPair = Pair {pi1 = examplePair, pi2 = "FTW!"}
Now we can access and update nested elements of this pair
λ> nestedPair ^. pi1 ^. pi2
'b'

λ> nestedPair ^. (pi1 . pi2)
'b'

λ> (pi1 . pi2 ^= ["nested", "polymorphic", "update"]) nestedPair
Pair {pi1 = Pair {pi1 = 1, pi2 = ["nested","polymorphic","update"]}, pi2 = "FTW!"}
and we can do useless things with the identity lens.
λ> examplePair ^. id
Pair {pi1 = 1, pi2 = 'b'}

λ> (id ^= ()) examplePair
()

Further more, users can make their own lenses for Data.Map, etc. that are compatible with the prelude lens operations as long as they follow the two van Laarhoven lens laws:

lens Identity === Identity

lens (composeCoalgebroid f g) === composeCoalgebroid (lens f) (lens g)
 where
  composeCoalgebroid :: (Functor f, Functor g) => (b -> f c) -> (a -> g b) -> a -> (Compose g f) c
  composeCoalgebroid f g a = Compose $ f <$> g a

There are some caveats with this approach though. It only natively supports simple polymorphic record update. In Haskell, you can do complex polymorphic updates. For instance, with the following record

data Complicated a b = Complicated {field1 :: a, field2 :: a, field3 :: b} deriving Show

complexExample :: Complicated Int Char
complexExample = Complicated {field1 = 1, field2 = 2, field3 = 'c'}
you can polymorphically update field1 and field2 together:
λ> complexExample {field1 = False, field2 = True}
Complicated {field1 = False, field2 = True, field3 = 'c'}

However it is not possible to compose the polymorphic van Laarhoven lenses field1 and field2 to do this. As consolation, it is possible to hand create a van Laarhoven lens to do the update.

handmadeLens :: Functor f => ((a, a) -> f (a', a')) -> Complicated a b -> f (Complicated a' b)
handmadeLens g (Complicated f1 f2 f3) = (\(n1, n2) -> Complicated n1 n2 f3) <$> g (f1, f2)
λ> (handmadeLens ^= (False, True)) complexExample 
Complicated {field1 = False, field2 = True, field3 = 'c'}

It might be possible to create syntax that derives these complex lenses just in time from a sequence of field names.

In conclusion, contrary to popular belief, polymorphic record updates are possible with lenses. None of this post has addressed the namespace issue with records, nor was it intended to be a record proposal for Haskell. However, it might be possible to incorporate this with a namespace proposal to somehow get a nice record syntax with first-class lenses and at least simple polymorphic record updates.

Tags

,

Responses


Russell O’Connor: contact me