vacationlabs / haskell-webapps

Proof-of-concept code for a typical webapp implemented in various Haskell libraries/frameworks
MIT License
134 stars 21 forks source link

Safe updates to records. #39

Open wz1000 opened 8 years ago

wz1000 commented 8 years ago

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.

newtype TenantUpdater = TU { runUpdate :: Tenant -> 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.

class HasName s where
  name :: Lens' s Text
class HasBackofficeDomain s where
   backofficeDomain :: Lens' s Text

instance HasName Tenant where
  ...
instance HasBackofficeDomain Tenant where
  ...

Now, we can modify TenantUpdater

newtype TenantUpdater = 
  TU { runUpdate :: forall a. (HasName a, HasBackofficeDomain a) => a -> a }

Because this function is polymorphic over all a, the only way to create a function of this type is by composing variations of set name and set backofficeDomain. Hence, this type is a safe way of representing updates to specific fields of Tenant.

We can generalise this concept a bit with the help of the ConstraintKinds extention.

newtype Updater (c :: * -> Constraint) = U { runUpdater :: forall a. c a => a -> a }

then

type MyConstraint a = (HasName a, HasBackofficeDomain a)
type TenantUpdater = Updater MyConstraint

We can now define updaters for any other records we might have.

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:

type family AllC (cs :: [k -> Constraint]) (a :: k) :: Constraint where
  AllC '[] a = ()
  AllC (c ': cs) a = (c a, AllC cs a)

Then

AllC '[Num, Ord, Show] a = (Num a, Ord a, Show a)

Now, we can modify our updater type to look like this:

newtype Updater (cs :: [* -> Constraint]) = 
  U { runUpdater :: forall a. AllC cs a => a -> a }

Finally,

type TenantUpdater = Updater '[HasName, HasBackofficeDomain]

Usage example

We can define FromJSON instances that generate specific Updaters.

Example here

We can apply an update to an entity like this:

applyUpdate :: (HasUpdatedAt a, AllC cs a) => Updater cs -> a -> App a
applyUpdate update entity = do
  time <- liftIO getCurrentTime       
  return $ set updatedAt time $ runUpdate update entity

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 of AllC and Updater.

Example:

data Tenant = Tenant { _tenantName :: Text }
makeFields ''Tenant

will generate

class HasName s a where
  name :: Lens' s a
instance HasName Tenant Text where
  ...

while we want

class HasName s where
  name :: Lens' s Text
saurabhnanda commented 8 years ago

Looking for feedback on this

/cc @jfoutz @meditans @sras

saurabhnanda commented 8 years ago

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 reason forall 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 }

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.
wz1000 commented 8 years ago

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 as and bs that would allow it to manipulate them. On the other hand, fst1 "knows" that its input is a tuple of Ints, 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.

wz1000 commented 8 years ago

A few lines on what problem the ContraintKinds extension solves. Or a link to a gentle introduction.

Constraint kinds allows you to manipulate Constraints as you would other types in Haskell. In particular, it allows you to parametrise a type over arbitrary Constraints, and it allows you to write type families to manipulate Constraints.

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.

sras commented 8 years ago

@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?

wz1000 commented 8 years ago

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.

saurabhnanda commented 8 years ago

@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 commented 8 years ago

@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 = ...
wz1000 commented 8 years ago

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 Variants 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
saurabhnanda commented 8 years ago

@wz1000 can we have a usage sample at the end of the write-up?

wz1000 commented 8 years ago

@saurabhnanda Done.

saurabhnanda commented 8 years ago

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:

mrkgnao commented 7 years ago

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.