... | ... | @@ -3,7 +3,10 @@ |
|
|
## Thumbnail Sketch
|
|
|
|
|
|
|
|
|
This proposal is addressing the narrow issue of **namespacing for record field names** by allowing more than one record in the same module to share a field name. Furthermore, it is aiming at a more structured approach to higher-ranked type fields, so that they can be updated using the same surface syntax as for other fields. This actually means a less complex implementation (compared to DORF or SORF). Specifically each field name is overloaded, and there is a Type with the same name (upshifted) so that:
|
|
|
This proposal is addressing the narrow issue of **namespacing for record field names** by allowing more than one record in the same module to share a field name. Furthermore, it is aiming at a more structured approach to higher-ranked type fields, so that they can be updated using the same surface syntax as for other fields. This actually means a less complex implementation (compared to DORF or SORF). This proposal is in the DORF 'stable', but sufficiently different it is worth making it a separate proposal.
|
|
|
|
|
|
|
|
|
Specifically each sharing field name is overloaded, and there is a Type with the same name (upshifted) so that:
|
|
|
|
|
|
- Within the same module, many record types can be declared to share the field name.
|
|
|
- The field name can be exported so that records in other modules can share it.
|
... | ... | @@ -20,7 +23,7 @@ The export/import of both the field name and its punned Type is under usual H98 |
|
|
In case of 'unintended' clash (another module using the same name 'by accident'), usual H98 controls apply to protect encapsulation and representation hiding.
|
|
|
|
|
|
|
|
|
This proposal introduces several new elements of syntax, all of which desugar to use well-established extensions of ghc. The approach is yet to be prototyped, but I expect that to be possible in ghc v 7.2.1. In particular:
|
|
|
This proposal introduces several new elements of syntax (including some shorthands), all of which desugar to use well-established extensions of ghc. The approach is yet to be prototyped, but I expect that to be possible in ghc v 7.2.1. In particular:
|
|
|
|
|
|
- The field name overloading is implemented through usual class and instance mechanisms.
|
|
|
- Field selectors are ordinary functions named for the field (but overloaded rather than H98's monomorphic), so field selection is regular function application. (There is no need for syntactically-based disambiguation at point of use.)
|
... | ... | @@ -34,53 +37,105 @@ Record declarations generate a `Has` instance for each record type/field combina |
|
|
Note that SORF introduces a third argument for the field's resulting type. (This is specifically to support higher-rank typed fields; but despite the complexity it introduces, SORF has no mechanism to update h-r fields.)
|
|
|
|
|
|
|
|
|
TPDORF approaches h-r fields in a different way, which supports both setting and getting those fields. (I'm not claiming this is a *solution*, more a well-principled work-round. And not a hack.)
|
|
|
TPDORF approaches h-r fields in a different way, which supports both setting and getting those fields. (I'm not claiming this is a *solution*, more a well-principled work-round. And not a hack. It is both scalable, and supports class-constrained higher-rank types.)
|
|
|
|
|
|
**The main insight** is that to manage large-scale data models (in which namespacing becomes onerous, and name sharing would be most beneficial), there are typically strong naming conventions and representation hiding for critical fields. For example:
|
|
|
|
|
|
```wiki
|
|
|
newtype Customer_id = Customer_id Int -- data dictionary, could be a data decl
|
|
|
-- constructor named same as type
|
|
|
data Customer = Customer { -- likewise
|
|
|
customer_id :: Customer_id -- field name puns on the type
|
|
|
, firstName :: String -- not a critical/shared field
|
|
|
, lastName :: String -- so using H98 style
|
|
|
, ...
|
|
|
} sharing (Customer_id, ...) deriving (...) -- new sharing syntax
|
|
|
```
|
|
|
|
|
|
|
|
|
TPDORF makes a virtue of this punning. (So extend's H98's and `NamedFieldPuns` punning on the field name.) This allows for some syntactic shorthands, but still supporting H98-style declaring field names within the record decl for backwards compatibility.
|
|
|
|
|
|
|
|
|
, there is a third argument for the field's resulting type. This is set at the instance level using equality constraints in a functional-dependencies style. Here is the `Has` class (`r` is the record, `fld` is the proxy type for the field, `t` is the field's type), with an example record declaration, its `Has` instance, and examples of use:
|
|
|
Here is the `Has` class with instances for the above Customer record, and examples of use:
|
|
|
|
|
|
```wiki
|
|
|
class Has r fld t where
|
|
|
get :: r -> fld -> t -- simplified form
|
|
|
set :: fld -> t -> r -> r -- where not changing record's type
|
|
|
class Has r t where
|
|
|
get :: r -> t -> GetResult r t -- see type instances below
|
|
|
set :: t -> r -> SetResult r t -- use where changing the record's type
|
|
|
|
|
|
data Customer = Cust{ customer_id :: Int, ... } -- declaration syntax same as H98
|
|
|
type family GetResult r t :: *
|
|
|
type family SetResult r t :: *
|
|
|
|
|
|
instance (t ~ Int) => Has Customer Proxy_customer_id t where -- Has instance generated, with ~ constraint
|
|
|
get Cust{ customer_id } _ = customer_id -- DisambiguateRecordFields pattern
|
|
|
set _ x Cust{ .. } = Cust{ customer_id = x, .. } -- RecordWildCards and NamedFieldPuns
|
|
|
instance Has Customer Customer_id where -- Has instance generated for sharing field
|
|
|
get Customer{ customer_id } = customer_id -- DisambiguateRecordFields pattern
|
|
|
set x Customer{ .. } = Customer{ customer_id = x, .. } -- RecordWildCards and NamedFieldPuns
|
|
|
|
|
|
type instance GetResult r Customer_id = Customer_id -- same result all record types
|
|
|
type instance SetResult Customer t = Customer -- record type is not parametric, so doesn't change
|
|
|
|
|
|
customer_id r = get r (undefined :: Customer_id) -- we can auto-decl this, see below
|
|
|
customer_id :: r{ customer_id :: Customer_id } => -- r{ ... } => is sugar for the Has constraint
|
|
|
r -> Customer_id -> Customer_id -- type inferred
|
|
|
|
|
|
|
|
|
newtype FirstName = FirstName String -- newtype generated for non-sharing field
|
|
|
firstName :: Customer -> FirstName -> String -- generated selector is monomorphic
|
|
|
firstName r = get r (undefined :: FirstName)
|
|
|
|
|
|
instance Has Customer FirstName where -- Has instance generated
|
|
|
get Customer{ firstName = (FirstName x) } = x -- DisambiguateRecordFields pattern
|
|
|
set x Customer{ .. } = Customer{ firstName = x, .. } -- RecordWildCards and NamedFieldPuns
|
|
|
|
|
|
type instance GetResult Customer FirstName = String -- specific to this record/field
|
|
|
-- type instance SetResult Customer FirstName = Customer -- not needed/already declared above
|
|
|
-- (but OK because overlaps and confluent)
|
|
|
|
|
|
myCust :: Customer -- usual record decl
|
|
|
... myCust{ customer_id = 27 } -- polymorphic record update
|
|
|
... myCust{ customer_id = 27, firstName = "Fred" } ... -- **polymorphic** record update, no data constr
|
|
|
... (customer_id myCust) ... -- field selection is func apply, or:
|
|
|
... myCust.customer_id ... -- dot notation is sugar for reverse func apply
|
|
|
```
|
|
|
|
|
|
|
|
|
Note that the**`Has` mechanism** uses a Proxy as the type 'peg' for a field (this is the wildcard argument to `get` and `set`):
|
|
|
Note that the**`Has` mechanism** uses **the field's type itself** to locate the field within the record:
|
|
|
|
|
|
- Each field must be a distinct type.
|
|
|
- The Type must be declared once (within the scope), and is then under regular name control.
|
|
|
(Probably you're doing this already for critical fields to share.)
|
|
|
- The type functions are not associated types, because:
|
|
|
|
|
|
- There must be a Proxy_type declared for each distinct field name.
|
|
|
- The Proxy must be declared once, and the Proxy is then under regular name control.
|
|
|
- The field selector function also must be declared once, defined using the Proxy.
|
|
|
- `GetResult` for shared fields depends only on the Field's type (per Customer_id above);
|
|
|
- `SetResult` for non-parametric record types continues the same record type.
|
|
|
- The field selector function also must be declared once, defined punning on the field's type.
|
|
|
(See below for syntactic sugar to declare these.)
|
|
|
- Possible **downside:** for non-`sharing` fields, what's the risk there's already a Type with the same name (upshifted) and that the name is an 'accidental' clash?
|
|
|
|
|
|
> >
|
|
|
> > It is an error to declare a record field without there being a Proxy in scope. The desugar for the data decl would create the instance to use the Proxy, but then the instance would fail.
|
|
|
> > It is an error to be `sharing` a record field without there being a same-named Type in scope. The desugar for the data decl would create the instance to use the Type, but then the instance would fail.
|
|
|
|
|
|
|
|
|
To generate the correct declarations, there is to be a new `fieldLabel` sugar:
|
|
|
To generate the correct field selector function, there is to be a new deriving class; and for record decls a shorthand:
|
|
|
|
|
|
```wiki
|
|
|
fieldLabel customer_id :: r -> Int -- new declaration, desugars to Proxy and func:
|
|
|
data Proxy_customer_id -- phantom
|
|
|
customer_id :: r{ customer_id :: Int } => r -> Int -- r{ ... } is sugar for Has constraint
|
|
|
customer_id r = get r (undefined :: Proxy_customer_id)
|
|
|
newtype Customer_id = Customer_id Int -- newtype or data decl, same name type and constr
|
|
|
deriving (Has, ...) -- generates customer_id function, per above
|
|
|
|
|
|
set (undefined :: Proxy_customer_id) 27 myCust -- record update desugarred from above example
|
|
|
data Customer = Customer { :: Customer_id, ... } -- auto-gen field label pun on type name
|
|
|
sharing (Customer_id, ...)
|
|
|
```
|
|
|
|
|
|
- (Admittedly, this could get onerous to declare a `fieldLabel` for every field, even the ones that appear in a single record type. See "Option Three: Mixed In-situ and DeclaredORF: " further down this page for a suggestion of using the DORF mechanism to generate one-off H98-style fields.)
|
|
|
|
|
|
**Virtual** or **pseudo-** fields are easy to create and use, because field selection is merely function application. Virtual fields look like ordinary fields (but can't be updated, because there is no `Has` instance):
|
|
|
Polymorphic record updates, alternative syntax (note, no data constructor for the record):
|
|
|
|
|
|
```wiki
|
|
|
set (Customer_id 27) ( -- record update desugarred from above example
|
|
|
set (FirstName "Fred") myCust ) -- note nested
|
|
|
|
|
|
... myCust{ cust_id, FirstName "Fred" } -- equiv syntax **only** for polymorphic updates
|
|
|
-- (assuming cust_id :: Customer_id)
|
|
|
```
|
|
|
|
|
|
**Virtual** or **pseudo-** fields are easy to create and use, because field selection is merely function application (plus unwrapping for non-shared H98-style fields). Virtual fields look like ordinary fields (but can't be updated, because there is no `Has` instance):
|
|
|
|
|
|
```wiki
|
|
|
fullName r = r.firstName ++ " " ++ map toUpper r.lastName -- example adapted from SPJ
|
... | ... | @@ -94,11 +149,44 @@ To generate the correct declarations, there is to be a new `fieldLabel` sugar: |
|
|
|
|
|
- Monomorphic fields can be `get` and `set`.
|
|
|
- Parametric polymorphic fields can be applied in polymorphic contexts, and can be `set` including changing the type of the record.
|
|
|
- Higher-ranked polymorphic fields can be applied in polymorphic contexts, but cannot be set -- for the same reasons as under SORF.
|
|
|
The instances use equality constraints to 'improve' types up to polymorphic.
|
|
|
- `Has` uses type family functions to manage type-changing update, which adds complexity -- see Implementer's view.
|
|
|
(This uses the SetResult type function.)
|
|
|
To do: provide example with desugarring.
|
|
|
- Multiple fields can be updated in a single expression (using familiar H98 syntax), but this desugars to nested updates, which is inefficient.
|
|
|
- Pattern matching and record creation using the data constructor prefixed to { ... } work as per H98 (using `DisambiguateRecordFields` and friends).
|
|
|
- But the types are subtlely different vs. polymorphic update: you must explicitly wrap the types.
|
|
|
(So this is not backwards compatible. Can we do this?:
|
|
|
In `Constr{ fld = e }`, if `e` not type `Fld`, enwrap it with a `Fld` constructor.)
|
|
|
- Higher-ranked polymorphic fields (including class-constrained) can be applied in polymorphic contexts, and can be set -- providing they are wrapped in a newtype. Here is SPJ's example:
|
|
|
|
|
|
```wiki
|
|
|
data HR = HR { rev :: (forall a. [a] -> [a]) } -- would generate:
|
|
|
|
|
|
type instance SetResult HR t = HR -- HR is not parametric
|
|
|
|
|
|
newtype Rev = Rev (forall a. [a] -> [a])
|
|
|
rev :: HR -> Rev -> (forall a. [a] -> [a]) -- generated selector is monomorphic
|
|
|
rev r = get r (undefined :: Rev)
|
|
|
|
|
|
instance Has HR Rev where -- Has instance generated
|
|
|
get HR{ rev = (Rev x) } = x -- looks OK, but see GetResult
|
|
|
set x HR{ .. } = HR{ rev = x, .. } -- note x is already wrapped
|
|
|
|
|
|
type instance GetResult HR Rev = (forall a. [a] -> [a]) -- no can do! not allowed forall's on RHS
|
|
|
-- (and can't do equality constraints on type instances)
|
|
|
{- ??can we do better -- perhaps an eq constraint on the Has instance:
|
|
|
(GetResult HR Rev ~ ([a] -> [a])) => Has ...
|
|
|
plus a non-commital result for getResult
|
|
|
-}
|
|
|
|
|
|
{- instead, do explicit newtype wrapping for higher-rank types: -}
|
|
|
|
|
|
newtype Rev = Rev (forall a. [a] -> [a]) deriving (Has)
|
|
|
newtype OrdRev = OrdRev (Ord a => [a] -> [a]) deriving (Has) -- class constrained
|
|
|
|
|
|
data HRW = HRW{ rev :: Rev, ordRev :: OrdRev } sharing (Rev, OrdRev)
|
|
|
```
|
|
|
|
|
|
- Now we can now apply the wrapped function polymorphically (after unwrapping within the user code.
|
|
|
|
|
|
|
|
|
. |