Suppose you have the following product type:
data D = D { getA :: Int, getB :: Char, getC :: [Double] }
and suppose you have a function:
f :: D -> D
which only reads the getA
field, but modifies getB
and getC
.
Is there a convenient way to express this in the type of f
?
So, let's consider an example:
f :: D -> D
f d = d { getC = map (+ fromIntegral (getA d)) (getC d) }
Clearly, as soon as you have a concrete type like D -> D
, all guarantees are off: this function could conceivably be doing anything with its argument.
If you want to prevent that, you need to replace the concrete D
with an abstract one, like
f :: d -> d
But of course then the implementation wouldn't work anymore, because on d
there's nothing you can do.
• Couldn't match expected type ‘d’ with actual type ‘D’
‘d’ is a rigid type variable bound by
the type signature for:
f :: forall d. d -> d
To re-enable just those particular operation you want, you can pass them in as arguments. So, what is a “read-operation or modify-operation parameter”?
Enter lenses. Let's first rewrite all the original example using them:
{-# LANGUAGE TemplateHaskell #-}
import Control.Lens
data D = D { _getA :: Int, _getB :: Char, _getC :: [Double] }
makeLenses ''D
f :: D -> D
f d = d & getC %~ map (+ fromIntegral (d^.getA))
Now, this can be readily generalised / strengthified, by making d
abstract but passing the necessary access operations as arguments:
type AGetter' s a = Getting a s a -- for some reason this isn't defined
-- in the `lens` library
f' :: AGetter' d Int -> ASetter' d [Double] -> d -> d
f' getInt setDbls d = d & setDbls %~ map (+ fromIntegral (d^.getInt))
Which allows you to obtain the old behaviour by passing the getA
and getC
lenses:
f :: D -> D
f = f' getA getC
The reasons this works is that lens
uses typeclass/universal-quantification type trickery to encode a subtype relationship: getA
has type Lens' D Int
, but AGetter' D Int
is a supertype of that with reduced capability, thus guaranteeing that you really only read the focused element, nothing else.
Technical detail: you've noticed I wrote ASetter'
and not Setter'
or ASetter
. What this means:
AnOᴘᴛɪᴄ
versions of Oᴘᴛɪᴄ
s are their rank-0 correspondents. So e.g. ALens
can only be used as a lens, not as e.g. a getter, whereas Lens
can be used as a getter or setter or traversal or fold.AnOᴘᴛɪᴄ
version, because that means the compiler doesn't actually have to juggle around with rank-2 types. (The type of a Lens
itself is merely rank-1 polymorphic, but passing it as an argument would make the accepting function rank-2 polymorphic.)Oᴘᴛɪᴄ'
version of Oᴘᴛɪᴄ
s are the non-type-changing variants. In principle, an e.g. setter could also change the type of a field it focuses on – e.g. when you change the snd
type of a (Bool, Char)
tuple to String
, that would be a Setter (Bool,Char) (Bool,String) Char String
, but if you just change the second field to another Char
, it's simply a Setter' (Bool,Char) Char
(which is actually a synonym for a type-changing setter which happens to change to the same type).If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With