real Models in Yesod

Yesod (and AFAIK other haskell frameworks) do not have strong models. In my experience, the heart of an individual model is validation logic. One can achieve validation by writing their own save function, however this allows multiple possible entries into saving a model because they can write multiple different save functions. Real enforcement of a single entry point with triggers is possible in TCache [1] where triggers are tied into the persistence layer. The main other issue I see causing weak models is the separation of forms from models. So I want to propose a design for stronger models and see what critiques and better ideas there are. Lets start with a data structure for a model: Person { personAge :: Int, personSex :: Maybe String } The main aspect is the addition of triggers, which are callbacks ran before a save to the database. A trigger is either a validator or a modifier. A validator is run as a callback before a save to the database. myTrigger :: Person -> Either ValidateError Person A ValidationError will be associated with a particular model, and possibly with a particular field of that model. We may want a validation errors to stop any more validations from being ran, or we may want to run all validations (just as a form returns all errors, not just one). ValidationError = FieldInvalid EntityField Text | BaseInvalid Text | ShortCircuitValidation ValidationError verifySex :: Person -> Either ValidationError Person verifySex p = when ((personAge p) > 18 && isNothing (personSex p)) FieldInvalid PersonSex "is required" return p Note that this is a non-monadic trigger, but triggers do need the ability to run database queries. One approach to registering callbacks would be to place them in the schema. Person triggers=verifySex, trigger2, trigger3 age Int sex String Maybe So now we can have a save function. ModelSave val = ModelValid val | ModelInvalid val [ValidationError] save :: val -> ModelSave val sav <- savePerson person case sav of ModelValid person -> ModelInvalid (person, errors) -> Note that both Constructors have a person. This is to allow triggers to modify the person. Now we need a technique for tieing the model to the form. I will use some magical functions. getNewPerson = do f <- newPersonForm [|hamlet| ^{f} |] postCreatePerson = do res <- saveForm personForm case res of ModelValid person -> ... ModelInvalid (person, errors) -> editPersonFrom person errors [1] http://hackage.haskell.org/packages/archive/TCache/0.8.0.2/doc/html/Data-TCa...

[snip]
The main aspect is the addition of triggers, which are callbacks ran before a save to the database. A trigger is either a validator or a modifier. A validator is run as a callback before a save to the database.
[snip] The key, IMHO, will be the order of execution of the callbacks. In most, if not all, systems I've used so far, based around such architecture this was their Achilles heel. Drupal hooks are one high-profile examples. The problem stems from the weird abstractions - you either don't know enough what else is out there, or you know too much. both of which are bad. Subject-observer while good, has its downsides :( The other option is to enforce separation of the results of the callbacks, so that the output is always deterministic. Cheers, Vlado

On Wed, Jul 13, 2011 at 7:53 AM, Vladimir Zlatanov
The key, IMHO, will be the order of execution of the callbacks. In most, if not all, systems I've used so far, based around such architecture this was their Achilles heel. Drupal hooks are one high-profile examples.
The problem stems from the weird abstractions - you either don't know enough what else is out there, or you know too much. both of which are bad. Subject-observer while good, has its downsides :(
The other option is to enforce separation of the results of the callbacks, so that the output is always deterministic.
Cheers, Vlado
I like the idea, and I think the callbacks can be executed in the order they are defined in the model. Even if it only saves you from writing all the validation boilerplate code seems worth giving a try.
_______________________________________________ web-devel mailing list web-devel@haskell.org http://www.haskell.org/mailman/listinfo/web-devel

Ordering was a concern I had also, and the plan was as Nubis suggested-
callbacks are executed in the order given (so very explicitly defined by the
user).
One aspect that can make ordering more confusing is having different
triggers for different life cycles of the model. It is often enough the case
that you want a trigger to operate on a create but not on an update. This
also points out that I forgot to pass a Maybe Key to the trigger function.
But there are more subtle life cycles for triggers- before or after create,
save, or destroy. A first implementation of triggers will probably avoid
this and just operate before a save, but I would like to think about an
extensible design. I have though of 2 possible apis to support life cycles:
[|
Person triggers=beforeCreate, CREATE, afterCreate | verifySex, trigger2,
trigger3 SAVE, afterSave, | beforeDestroy, DESTROY, afterDestroy
age Int
sex String Maybe
|]
A multi-line alternative:
[|
Person
age Int
sex String Maybe
BeforeCreate=beforeCreate
AfterCreate=afterCreate
BeforeSave=verifySex, trigger2, trigger3
AfterSave =afterSave
BeforeDestroy = beforeDestroy
AfterDestroy=afterDestroy
|]
On Wed, Jul 13, 2011 at 5:48 AM, Nubis
On Wed, Jul 13, 2011 at 7:53 AM, Vladimir Zlatanov
wrote: The key, IMHO, will be the order of execution of the callbacks. In most, if not all, systems I've used so far, based around such architecture this was their Achilles heel. Drupal hooks are one high-profile examples.
The problem stems from the weird abstractions - you either don't know enough what else is out there, or you know too much. both of which are bad. Subject-observer while good, has its downsides :(
The other option is to enforce separation of the results of the callbacks, so that the output is always deterministic.
Cheers, Vlado
I like the idea, and I think the callbacks can be executed in the order they are defined in the model. Even if it only saves you from writing all the validation boilerplate code seems worth giving a try.
_______________________________________________ web-devel mailing list web-devel@haskell.org http://www.haskell.org/mailman/listinfo/web-devel

Personally, I think we should only provide a single function instead
of allowing the definition of multiple functions. If users need
multiple functions, they can define a "wrapper" function in normal
Haskell. The reasoning here is to avoid "magic" in the TH code
whenever possible.
As for different types of triggers, how about attributes like
"onSave", "onUpdate", "onCreate" and "onDelete"? onSave could be
called for both onUpdate and onCreate (with some explicit rules about
what happens when *both* of those are defined).
Michael
On Wed, Jul 13, 2011 at 6:09 PM, Greg Weber
Ordering was a concern I had also, and the plan was as Nubis suggested- callbacks are executed in the order given (so very explicitly defined by the user). One aspect that can make ordering more confusing is having different triggers for different life cycles of the model. It is often enough the case that you want a trigger to operate on a create but not on an update. This also points out that I forgot to pass a Maybe Key to the trigger function. But there are more subtle life cycles for triggers- before or after create, save, or destroy. A first implementation of triggers will probably avoid this and just operate before a save, but I would like to think about an extensible design. I have though of 2 possible apis to support life cycles: [| Person triggers=beforeCreate, CREATE, afterCreate | verifySex, trigger2, trigger3 SAVE, afterSave, | beforeDestroy, DESTROY, afterDestroy age Int sex String Maybe |] A multi-line alternative: [| Person age Int sex String Maybe BeforeCreate=beforeCreate AfterCreate=afterCreate BeforeSave=verifySex, trigger2, trigger3 AfterSave =afterSave BeforeDestroy = beforeDestroy AfterDestroy=afterDestroy |]
On Wed, Jul 13, 2011 at 5:48 AM, Nubis
wrote: On Wed, Jul 13, 2011 at 7:53 AM, Vladimir Zlatanov
wrote: The key, IMHO, will be the order of execution of the callbacks. In most, if not all, systems I've used so far, based around such architecture this was their Achilles heel. Drupal hooks are one high-profile examples.
The problem stems from the weird abstractions - you either don't know enough what else is out there, or you know too much. both of which are bad. Subject-observer while good, has its downsides :(
The other option is to enforce separation of the results of the callbacks, so that the output is always deterministic.
Cheers, Vlado
I like the idea, and I think the callbacks can be executed in the order they are defined in the model. Even if it only saves you from writing all the validation boilerplate code seems worth giving a try.
_______________________________________________ web-devel mailing list web-devel@haskell.org http://www.haskell.org/mailman/listinfo/web-devel
_______________________________________________ web-devel mailing list web-devel@haskell.org http://www.haskell.org/mailman/listinfo/web-devel

sounds fine to me. We will need to provide a sequencing function to users
then to combine triggers with.
On Thu, Jul 14, 2011 at 11:29 PM, Michael Snoyman
Personally, I think we should only provide a single function instead of allowing the definition of multiple functions. If users need multiple functions, they can define a "wrapper" function in normal Haskell. The reasoning here is to avoid "magic" in the TH code whenever possible.
As for different types of triggers, how about attributes like "onSave", "onUpdate", "onCreate" and "onDelete"? onSave could be called for both onUpdate and onCreate (with some explicit rules about what happens when *both* of those are defined).
Michael
participants (4)
-
Greg Weber
-
Michael Snoyman
-
Nubis
-
Vladimir Zlatanov