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