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.