Records in Haskell - updating Higher-Ranked fields

AntC anthony_clayden at
Wed Mar 28 02:18:33 CEST 2012

Gábor Lehel <illissius <at>> writes:
> ..., but DORF actually requires less type system magic than
> SORF, and also already has a working prototype implementation, ...
> ... My main complaint against DORF is
> that having to write fieldLabel declarations for every field you want
> to use is onerous. ...

    (Here's a possible way to reduce the need for declarations,
     for non-sharing fields; also avoid the fieldLabel new decl.)

SPJ introduced some fancy type-handling for higher-ranked fields. But still it 
only helped in accessing a H-R field, not updating it.

If we accept that updating H-R fields in liberated-namespace records is not 
possible, can we simplify the implementation? I think so. Here's what I'd call 
a well-principled work-round (not a hack, but then neither a solution).

(I'm showing it in DORF-like style, but I think it would work equally in SORF 
style with Stringy Kinds. The advantage of showing DORF is that we can try it -
- which I've done.)

You write:
    data HR = HR{ objectId :: ObjectId              -- type Pun
                , rev :: forall a. [a] -> [a] }     -- SPJ's example
             sharing (ObjectId) deriving (...)      -- new syntax
         -- this decl is not sharing `rev`, so no further code needed

         -- HR is sharing objectId, so you need a field Label in scope:
         -- probably you're already declaring newtypes/data
    newtype ObjectId = ObjectId Int                 -- declaring a field
          deriving (Has, ... )                      -- `Has` makes it a label

Field access can be polymorphic:
    f :: HR -> ([Bool], [Char])
    f r = (r.rev [True], r.rev "hello")

Record update looks like:
            ... myHR{ rev = Rev reverse } ...       -- annoying pun
But perhaps we could support sugar for it:
            ... myHR{ Rev reverse } ...             -- fewer keystrokes!

The HR decl desugars to:
    newtype Rev = Rev (forall a. [a] -> [a])        -- newtype punning
    data HR = HR{ objectId :: ObjectId, rev :: Rev }
    rev :: HR -> (forall a. [a] -> [a])             -- monotype field selector
    rev r = let (Rev fn) = get r (undefined :: Rev) in fn
    instance Has HR Rev        where
        get HR{ rev } _ = rev                       -- have to wrap the fn
        set (Rev fn) HR{ .. } = HR{ rev = (Rev fn) }
    type instance FieldTy HR Rev = Rev              -- have to wrap this
                                                    -- else update don't work

So I've simplified `Has` to two type arguments (like the more naieve form SPJ 
considers then rejects). And used the field's type itself as the index (so 
that we're punning on the field name):
    class Has r fld       where
        get :: r -> fld -> FieldTy r fld
        set :: fld -> r -> SetTy r fld              -- for type change update

For the record type's `sharing` fields we desugar to:
    instance Has HR ObjectId   where
        get HR{ objectId } _ = objectId             -- yeah dull, dull
        set x HR{ .. }       = HR{ objectId = x, .. }

and the `deriving (Has)` on the newtype or data desugars to:
    objectId :: {Has r ObjectId} => r -> ObjectId   -- overloaded record 
    objectId r = get r (undefined :: ObjectId)      --         selector
    type instance FieldTy r ObjectId = ObjectId     -- not parametric


More information about the Glasgow-haskell-users mailing list