I’ve got an architecture issue in Formless that I could use some help on.
It’ll take a little quick background before I can get to the question:
Background
These are the core types I’ll be discussing:
- a
Form
newtype you define as a user, which is a newtype around a record of fields that are in your form. Each field has anerror
,input
, andoutput
type associated. - This newtype takes an argument,
f
, which is filled in with types likeFormField
. As you can see below,FormField
takes the same error, input, and output types and produces a record from it.
Don’t worry too much about these; these types are unrelated to the probelm I’m discussing, but are still good to know for context as you see code snippets.
-- For context
newtype Form f = Form { name :: f Error String ValidName }
newtype FormField e i o = FormField { input :: i, result :: Either e o }
exampleForm :: Form FormField
exampleForm = Form
{ name: FormField
{ input: ""
, result: Left MyCustomError
}
}
In Formless, I used to let you provide a single validation function which would be run across your entire form:
validator :: Monad m => Form FormField -> m (Form FormField)
This was nice because you could easily do validation on fields that depend on each other – for example, you could say that the password1 field is only valid if it is equal to the value of the password2 field. But it was NOT nice because validation was always run across all fields every time any individual field changed. When you have expensive validation or are making server calls this is a real problem.
I switched to a different style, where you provide a record of validators, each of which operates just on a single field’s input:
newtype Validation m e i o = Validation (i -> m (Either e o))
validators :: Monad m => Form (Validation m)
With this + variants I can run validation on just one field at a time. But I’ve lost access to the overall state with which to do that dependent validation! I can’t just re-introduce it as an argument, like this:
validators :: Form FormField -> Form (Validation m)
… because the validators will just have access to that state the very first time this function is used, and never afterwards. I briefly considered using a Ref
, but didn’t want make the form a mutable variable if I could help it.
To get around that, I opted to have validators actually take the current state as an argument:
newtype Validation st m e i o = Validation (st -> i -> m (Either e o))
validators :: Monad m => Form (Validation (Form FormField) m)
and that actually works quite well…sort of. As you can see in the attached .gif, a field gets the most up-to-date state so long as it’s the one being modified. If it isn’t, then its validation never runs, and so perhaps its dependent condition has been satisfied but the field doesn’t know it yet.
In this example, the two Secret Key fields must be equal. The field you are modifying validates correctly, but the one you are not modifying gets out-of-sync because it hasn’t been refreshed with the latest change from its associated field.
Question
I’m not sure what to do about this. The core of the problem is that I need, somehow, a way to lash two (or more) fields together so that modifying or validating one also triggers validation on the other, and both pull the most recent form state when doing so.
Any ideas?