Open wz1000 opened 8 years ago
Looking for feedback on this
/cc @jfoutz @meditans @sras
TenantUpdater
in the following code to avoid any confusion?
newtype TenantUpdater = TU { runUpdate :: Tenant -> Tenant }
- Possible to define a
Tenant
record with regular$(makeLenses)
for this to work?- Is the
forall a
really required in the following type signature? For some strange reasonforall
is a mind-block for me. It signals the usage of HKT or RankNTypes which I don't fully understand.
newtype TenantUpdater = TU { runUpdate :: forall a. (HasName a, HasBackofficeDomain a) => a -> a }
ContraintKinds
extension solves. Or a link to a gentle introduction.However, we have to define a new kind of Constraint for every such Updater. In order to let us anonymously compose Constraints, we can use this type family:
- If you wrap-up with two code snippets it would make the benefits of this type machine absolutely clear: (a) definition and usage of
TenantUpdater
without the type-machinery, and (b) definition and usage with the type-machinery.
Possible to change the constructor to TenantUpdater in the following code to avoid any confusion?
Yes, but I don't use this constructor in my actual code any way. This was just for demonstration purposes.
Is the forall a really required in the following type signature? For some strange reason forall is a mind-block for me. It signals the usage of HKT or RankNTypes which I don't fully understand.
Yes. This is because the more polymorphic a function is, the less ways there are to write it.
Consider a function fst
that takes a tuple and return the first element of the tuple.
The most polymorphic type of this function is
fst :: forall a b. (a,b) -> a
However, it can also have the type
fst1 :: (Int, Int) -> Int
Now, you can be sure of the behaviour of fst
immediately by looking at its type, but that doesn't hold for fst1
Pretty much the only "reasonable" definition of fst
that the compiler will accept is
fst (x,y) = x
However, the compiler will accept all the following definitions of fst1
fst1 (x,y) = x+y
fst1 (x,y) = x*y
fst1 (x,y) = 2^x
fst1 (x,y) = 7
...
Polymorphism limits the amount of information we have about a particular type, which in turn limits the possible manipulations we can perform with values of that type. In the above case, fst
doesn't "know" anything about a
s and b
s that would allow it to manipulate them. On the other hand, fst1
"knows" that its input is a tuple of Int
s, and hence can perform arbitrary Int
manipulations on them.
In our case, the only thing TenantUpdater
"knows" about the value it needs to manipulate is that it has a name
and a backofficeDomain
. It can't change its ID
or creationTime
because it knows nothing about them.
A few lines on what problem the ContraintKinds extension solves. Or a link to a gentle introduction.
Constraint kinds allows you to manipulate Constraint
s as you would other types in Haskell. In particular, it allows you to parametrise a type over arbitrary Constraint
s, and it allows you to write type families to manipulate Constraint
s.
newtype Updater (c :: * -> Constraint) = U { runUpdater :: forall a. c a => a -> a }
In this type, the constraint we have is a type parameter, instead of being fixed. So we could have a
type NumUpdater = Updater Num
where NumUpdater
represents all the manipulations you can perform on types that implement the Num
typeclass. This means that you can only use the functions with are implemented by Num
to manipulate your input and produce your output. For instance NumUpdater
can't return the sin
of its input.
@wz1000
Consider a function fst that takes a tuple and return the first element of the tuple.
The most polymorphic type of this function is...
But wouldn't a signature of fst::(a, b) -> a, be as polymorphic and work the same?
But wouldn't a signature of fst::(a, b) -> a, be as polymorphic and work the same?
The signatures are equivalent. I just added an explicit forall
for clarity. GHC makes (almost) no distinction between these two type signatures.
GHC automatically assumes all free type variables to be universally quantified.
@wz1000 so, IIUC this type-machinery needs to be defined only once and then any record with regular lenses can use it:
updateProduct :: Product -> Updater '[HasName HasDescription HasProperties HasVariants] -> AppM Product
updateVariant :: Variant -> Updater '[HasName HasSku HasQuantity HasWeight] -> Variant
Is that correct?
Follow-up question: Does this scale to nested records? In the example above, a Product
might have a list of nested [Variants']
? How will that work?
@wz1000 so, IIUC this type-machinery needs to be defined only once and then any record with regular lenses can use it. Is that correct?
Yes.
updateProduct :: Product -> Updater '[HasName HasDescription HasProperties HasVariants] -> AppM Product
You are missing the commas in the type level list
updateProduct :: Product -> Updater '[HasName, HasDescription, HasProperties, HasVariants] -> AppM Product
Follow-up question: Does this scale to nested records? In the example above, a Product might have a list of nested [Variants']? How will that work?
It'll work as long as you define the appropriate type classes with the necessary lenses and prisms to access the underlying fields.
Also, this allows multiple methods of updating the same record. For instance, we may not want the user to update their password without reauthentication.
updateUser :: User -> Updater '[HasFirstName, HasLastName, HasEmail] -> AppM User
updatePassword :: Session -> User -> Updater '[HasPassword] -> AppM User
updatePassword s user upd | isSecure s = ...
| otherwise = ...
Follow-up question: Does this scale to nested records? In the example above, a Product might have a list of nested [Variants']? How will that work?
Sorry, I misunderstood. You want the underlying Variant
s to also be updated in a safe manner. In this case you would have to make a type class such as:
class HasUpdatableVariants s where
updateVariants :: Updater '[HasName, HasSku, HasQuantity, HasWeight] -> s -> s
instance HasUpdatableVariants Product where ...
then
updateProduct :: Product -> Updater '[HasName, HasDescription, HasProperties, HasUpdatableVariants] -> AppM Product
@wz1000 can we have a usage sample at the end of the write-up?
@saurabhnanda Done.
Any thoughts on the core approach chalked out in the question at http://stackoverflow.com/questions/40171037/apply-a-function-to-all-fields-in-a-record-at-the-type-level/40171268# ? I do not really like the current solution to my question on Stackoverflow, but I got two alternative approaches on IRC:
The basic idea is to have a bunch of tightly-related record-types and an easy way to generate them from a common source (eg a TH declaration, on the lines of what Persistent does). For example:
updateProduct :: ProductDiff -> Product -> AppM (Product)
data Product = Product {
id :: ProductID
,name :: Text
,sku :: SKU
,quantity :: Int
,description :: Maybe Text
}
data ProductDiff = ProductDiff {
id :: NonPatchable ProductID
,name :: Patchable Text
,sku :: Patchable SKU
,quantity :: Patchable Int
,description :: Patchable (Maybe Text)
}
data Patchable a = Same | Change a
data NonPatchable a = NonPatchable
Once we have these types, it should be easy to write generic functions that do the following:
Diff
data type (eg. ProductDiff
)The recent PureScript update added some reflection-like features for working with records, and it enables some things that may be of interest.
Here's a link to a Try PureScript demo that uses them to implement extensible records where some fields can be marked mandatory and some can be marked optional, and everything's statically checked as one might expect.
The problem
We want a nice way to describe updates to a record. These updates are only allowed to edit certain fields of the record. Ideally, we don't want to have a new record to describe each kind of update as this leads to a lot of boilerplate and namespace pollution
Proposed solution
Let's consider a record
Tenant
with attributes like a name, backoffice domain, creation and updation times, status etc. Out of these, we only want the name and backoffice domain to be editable by the user.First, let's consider a generic way to represent updates to
Tenant
.This type can represent any kind of an update to a
Tenant
.In order to make this represent only specific kinds of updates, we first write typeclasses that implement lenses from
Tenant
to the fields we want to edit.Now, we can modify
TenantUpdater
Because this function is polymorphic over all
a
, the only way to create a function of this type is by composing variations ofset name
andset backofficeDomain
. Hence, this type is a safe way of representing updates to specific fields ofTenant
.We can generalise this concept a bit with the help of the
ConstraintKinds
extention.then
We can now define updaters for any other records we might have.
However, we have to define a new kind of
Constraint
for every suchUpdater
. In order to let us anonymously composeConstraints
, we can use this type family:Then
Now, we can modify our updater type to look like this:
Finally,
Usage example
We can define
FromJSON
instances that generate specificUpdater
s.Example here
We can apply an update to an entity like this:
Why the lens typeclasses cannot be automatically generated using Control.Lens.TH
Out of all the functions in
Control.Lens.TH
,makeFields
comes the closest to generating the typeclasses we want. Unfortunately it generates typeclasses parametrising over the field type, which complicates the usage ofAllC
andUpdater
.Example:
will generate
while we want