>
Firstly, thank you to SPJ for putting some detailed design into 'Overloaded 
record fields' [SPJ 1].
What I'm showing here draws heavily on the techniques he demonstrates.

I wasn't happy with several parts of the design proposal;
especially not with the amount of not-yet-available type machinery it involved 
(explicit type application, anonymous types, the kind system, String types).

But then SPJ wasn't happy himself with the limitations on Record update for 
polymorphic fields:
        "This problem seems to be a killer: if record-update syntax is
         interpreted as a call to `set', ...".

It's (too) easy to chuck rocks - the "glaring weakness" of (Haskell's record 
system) is still a swamp despite a pile of rocks (to mix my metaphors).
        

This posting (as a .lhs) is the result of exploring within the "narrow issue: 
namespacing for record field names".
I've used GHC + recent type extensions (v7.2.1). Findings in short:
- I've followed SPJ with a `Has' class, and methods `get' and `set'.
- I seem to have a way to update Higher-ranked fields,
- and change the type of the record,
- and even update Existentially-quantified fields (to a limited extent)
         - which should please the object-oriented oriented.

I'd appreciate some feedback:
- Have I misunderstood the results?
- Are there still unacceptable limitations?
         (Certainly the error messages are impenetrable when you go wrong.)
- The `Has' class, instances and type functions are ugly
         - can we be more elegant?
        (I expect the instances to be generated systematically from the data
         decl. So usually the application programmer won't have to see them.
        I like SPJ's 'Syntactic sugar for Has' to pretty-up Has constraints.)

Disclaimer:
- I'm keeping close to SPJ's objective of improving the situation w.r.t.
        name clashes for record fields. [SPJ 2]
- I'm _not_ claiming this is a design for 'first class record types'
        /extensible records/record polymorphism.
- I'm _not_ making a proposal for 'Records in Haskell'.
        (Not yet: if this is approach is deemed 'workable',
         then I have a design in mind.)
- I'm _not_ envisaging anything like 'first-class labels'.
        (I think that idea is probably not the right objective,
         but that's a debate for another day.)

The basic idea:
- Field selection uses dot notation as reverse-application,
  applying a field selector via Has/get and resolving the instance to the
  record type and field, that is:
        r.fld ==> fld r, ==> get (undefined :: Proxy_fld) (r@DCons{fld}) = fld
       (I've used (.$) = flip ($) to fake the syntax for dot notation.)
- Record construction and pattern matching with explicit data constructors
  works as with -XDisambiguateRecordFields
- Record update uses H98 syntax, to be compiled to Has/set
  and resolving the instance to the record type and field with explicit data
  constructor:
        r {fld = val} ==> set fld val (r@DCons{..}) ==> DCons{fld = val, ..}
        -- using Puns and WildCards
        (I've used (.=) = set to fake the syntax for update: r.$(fld.=val).)
- Of course, I can't use the field name itself, because that would clash with
  the H98 selector. Instead:
        _fld = undefined :: Proxy_fld           -- for the update syntax
        fld_ = get (undefined :: Proxy_fld)     -- for the (overloadable)
                                                -- selector function

The approach uses a 'loose coupling' between the type arguments of Has/get/set,
with type functions to control the linkage.
  (Some could be Associated Types -- a matter of taste?)

The recipe needs:

> {-# OPTIONS_GHC -XDisambiguateRecordFields -XNamedFieldPuns -XRecordWildCards
                                                                        #-}
> {-# OPTIONS_GHC -XTypeFamilies                                        #-}
> {-# OPTIONS_GHC -XRankNTypes  -XImpredicativeTypes -XGADTs -XEmptyDataDecls
                                                                        #-}
> {-# OPTIONS_GHC -XMultiParamTypeClasses -XFlexibleInstances
                  -XUndecidableInstances                                #-}

> module HasGetSet      where

        SPJ's example of a higher-ranked data type
                     -- imported so that we have clashing declarations of `rev'

>  import HRrev
>  {- data HR   = HR {rev :: forall a.[a] -> [a]}       -}

>  data Tab a b         where   -- a different data type with a HR field `rev'
>    Ta :: {tag :: String, rev :: forall a_.([a_] -> [a_]), flda :: a }
>      -> Tab a b
>    Tb :: (Num n, Show b) => {tag :: String, fldn :: n, fldnb :: n -> b}
>       -> Tab a b
>                              -- Existential fields (GADT syntax)

        overloadable definitions for field `rev':

>  data Proxy_rev              -- phantom, same role as SPJ's String kind "rev"
>  _rev = undefined :: Proxy_rev
>  rev_ r = get (undefined :: Proxy_rev) r

build some syntax to fake the dot notation, and assignment within record update

>  (.$) = flip ($)
>  infixl 9 .$

>--  (fld .= val) r = set fld val r     -- sadly, this doesn't quite work
>  infix  9 .=                          -- so define (.=) as a method in Has

test rig for higher-rank rev, per SPJ's example, and demos

>  testrev r = (r.$rev_ $ [True, False, False], r.$rev_ $ "hello")
>                            -- dot notation would bind tighter than func apply

>  rHR0 = HR{}               -- `rev' is undefined, to show we can update it
>  rHR1 = rHR0.$(_rev.=reverse)           -- equivalent to rHR0{rev = reverse}
>                            -- testrev rHR1 ==> ([False,False,True],"olleh")
>  rHR2 = rHR1.$(_rev.=(drop 1 . reverse))
>                            -- testrev rHR2 ==> ([False,True],"lleh")

>  rTab0 = Ta{tag="tagged"}  -- `rev' is undefined, to show we can update it
>  rTab1 = rTab0.$(_rev.=reverse)
>                            -- testrev rTab1 ==> ([False,False,True],"olleh")
>  rTab2 = rTab1.$(_flda.='a').$(_rev.=(reverse . take 3))
>                            -- rTab1 :: Tab a b ; rTab2 :: Tab Char b
>                            -- testrev rTab2 ==> ([False,False,True],"leh")


here's the mechanism -- declarations for Has/get/set, and type families

>  class Has r fld t    where
>    get  :: fld -> r -> GetTy r fld t
>    set, (.=)  :: fld -> (forall a_.SetTy r fld t a_) -> r -> SetrTy r fld t
> --   (fld .= val) r = set fld val r     -- } sadly, neither of these work
> --   (.=) = set                         -- } (trying to define a default)

>  type family GetTy  r fld t     :: *  -- the type to get at `fld' in `r'
>  type family SetTy  r fld t a_  :: *  -- type to set at `fld' in updated `r'
>  type family SetrTy r fld t     :: *  -- the type to set for updated `r'

The instance for field `rev' to be a Higher-ranked type.

>  instance (t ~ ([a_]->[a_])) => Has HR Proxy_rev t    where
>                                -- the constraint needs -XUndecidableInstances
>    get _     HR{rev}  = rev
>    set _ fn  HR{..}   = HR{rev = fn, ..}
>    (_ .= fn) HR{..}   = HR{rev = fn, ..}

>  type instance GetTy  r Proxy_rev t    = t
>                        -- that is ([a_] -> [a_]) from the instance constraint
>  type instance SetTy  r Proxy_rev t a_ = ([a_] -> [a_])
>                        -- that is ([a_] -> [a_]) from `set's forall a_
>  type instance SetrTy r Proxy_rev t    = r
>                        -- updating `rev' doesn't change the type

the above type instances apply for any record with field `rev', so also type 
`Tab'

>  instance (t ~ ([a_]->[a_])) => Has (Tab a b) Proxy_rev t     where
>                                -- the constraint needs -XUndecidableInstances
>    get _     Ta{rev}  = rev
>    set _ fn  Ta{..}   = Ta{rev = fn, ..}
>    (_ .= fn) Ta{..}   = Ta{rev = fn, ..}

>  data Proxy_flda              -- `flda's type is a parameter to the data type
>  _flda = undefined :: Proxy_flda
>  flda_ r = get (undefined :: Proxy_flda) r
>  instance Has (Tab a b) Proxy_flda t  where  -- note no constraint on `t',
>                                      -- might be different to `a' for update
>    get _     Ta{flda} = flda
>    set _ x   Ta{..}   = Ta{flda = x, ..}
>    (_ .= x)  Ta{..}   = Ta{flda = x, ..}

>  type instance GetTy  (Tab a b) Proxy_flda t    = a
>                                 -- this is where we constrain `t' for the get
>  type instance SetTy  (Tab a b) Proxy_flda t a_ = t
                                  -- type to set is whatever we're given
>  type instance SetrTy (Tab a b) Proxy_flda t    = Tab t b
                                 -- set the result type: substitute `t' for `a'

and Has/get/set for the other fields of Tab, including the Existential

>  data Proxy_tag
>  _tag = undefined :: Proxy_tag
>  tag_ r = get (undefined :: Proxy_tag) r
>  instance (t ~ String) => Has (Tab a b) Proxy_tag t   where
>                          -- constraint on `t', because tag is always a String
>    get _     Ta{tag}  = tag
>    get _     Tb{tag}  = tag
>    set _ x   Ta{..}   = Ta{tag = x, ..}
>    set _ x   Tb{..}   = Tb{tag = x, ..}
>    (_ .= x)  Ta{..}   = Ta{tag = x, ..}
>    (_ .= x)  Tb{..}   = Tb{tag = x, ..}
>  type instance GetTy  (Tab a b) Proxy_tag t    = String
>  type instance SetTy  (Tab a b) Proxy_tag t a_ = String
>  type instance SetrTy (Tab a b) Proxy_tag t    = Tab a b
>                          -- changing the tag doesn't change the record's type

>  data Proxy_fldn            -- } the Existential fields must be set together
>  data Proxy_fldnb           -- } possible approach for multiple update
>  _fldn  = undefined :: Proxy_fldn
>  _fldnb = undefined :: Proxy_fldnb
>  -- no point in a 'getter' function: the types would escape

>  instance (t ~ (tn, tn -> b'), Show b', Num tn)
>                                              -- needs -XUndecidableInstances
>           => Has (Tab a b) (Proxy_fldn, Proxy_fldnb) t        where
>                                   -- the use case is: r{fldn = n, fldnb = nb}
> -- get _     Tb{fldn, fldnb}  = (fldn, fldnb)       -- No! types would escape
>    set _ (n, nb)   Tb{..}     = Tb{fldn = n, fldnb = nb, ..}
>    (_ .= (n, nb))  Tb{..}     = Tb{fldn = n, fldnb = nb, ..}
>  type instance GetTy  (Tab a b) (Proxy_fldn, Proxy_fldnb) t    = t
>                       -- that is: (tn, tn -> b') from the instance constraint
>  type instance SetTy  (Tab a b) (Proxy_fldn, Proxy_fldnb) t a_ = t
>  type instance SetrTy (Tab a b) (Proxy_fldn, Proxy_fldnb) (tn, tn -> b')
>                                                                = Tab a b'
>                                      -- changing the fields changes the type

demo

>  nb' Tb{fldn, fldnb} = fldnb fldn           -- test rig

>  rTab3 = Tb{tag="tagged", fldn = 6 :: Double, fldnb = negate}
>                                             -- nb' rTab3 ==> -6.0 :: Double
>  rTab4 = rTab3.$((_fldn, _fldnb).=(5::Int, (6 +)))
>                                             -- rTab3{fldn = 5, fldnb = (6 +)}
>                                             -- nb' rTab4 ==> 11 :: Int


Notes/difficulties:

- Language options needs -XUndecidableInstances because the approach
  "uses a functional-dependency-like mechanism (but using equalities)" [SPJ 1]
  to 'improve' type inference for some instances.
  Since this _doesn't_ use FunDeps, can it safely exceed Paterson Conditions?

- The inability to declare (.=) = set is curious/irritating.
  (Neither as a free-standing function, nor a method of Has with a default
   implementation.
   And even declaring (.=) with a type signature identical to `set'.
   From the error messages, GHC can't match the forall'd `a_'.)

If you've managed to read this far: thank you!


Refs:
[SPJ 1] http://hackage.haskell.org/trac/ghc/wiki/Records/OverloadedRecordFields
[SPJ 2] http://hackage.haskell.org/trac/ghc/wiki/Records



_______________________________________________
Glasgow-haskell-users mailing list
Glasgow-haskell-users@haskell.org
http://www.haskell.org/mailman/listinfo/glasgow-haskell-users

Reply via email to