Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Is there a way to express read/write/read-write attributes of records in types?

Tags:

haskell

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?

like image 554
Damian Nadales Avatar asked Dec 23 '22 19:12

Damian Nadales


1 Answers

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:

  • The 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.
    It is considered good style to restrict function arguments to the concrete 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.)
  • The 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).
like image 56
leftaroundabout Avatar answered Dec 25 '22 08:12

leftaroundabout