Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Safe updates to records. #39

Open
wz1000 opened this issue Oct 25, 2016 · 13 comments
Open

Safe updates to records. #39

wz1000 opened this issue Oct 25, 2016 · 13 comments

Comments

@wz1000
Copy link
Collaborator

wz1000 commented Oct 25, 2016

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
Copy link
Contributor

Looking for feedback on this

/cc @jfoutz @meditans @sras

@saurabhnanda
Copy link
Contributor

  • Possible to change the constructor to 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 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 }

  • A few lines on what problem the ContraintKinds extension solves. Or a link to a gentle introduction.
  • I got completely lost from the following point onwards. Although, now, I do understand what this type machinery does.

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
Copy link
Collaborator Author

wz1000 commented Oct 26, 2016

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
Copy link
Collaborator Author

wz1000 commented Oct 26, 2016

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
Copy link
Contributor

sras commented Oct 26, 2016

@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
Copy link
Collaborator Author

wz1000 commented Oct 26, 2016

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
Copy link
Contributor

@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
Copy link
Collaborator Author

wz1000 commented Oct 26, 2016

@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
Copy link
Collaborator Author

wz1000 commented Oct 26, 2016

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
Copy link
Contributor

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

@wz1000
Copy link
Collaborator Author

wz1000 commented Oct 26, 2016

@saurabhnanda Done.

@saurabhnanda
Copy link
Contributor

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:

  • parse an incoming JSON into a Diff data type (eg. ProductDiff)
  • and, given a diff apply it on the original record

@mrkgnao
Copy link

mrkgnao commented Apr 8, 2017

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

4 participants