Created: 2022-06-05 16:40
Generate web forms from pure functions got me thinking how would a DSL for defining forms look like.
Headless
In my experience with different form libraries, trying to come up with a DSL to derive the UI of the forms doesn’t work. There are always limitations and edge cases that make the complexity skyrocket.
On UI applications, more often than not, the UI is what changes but the logic stays the same, specially the boring logic like attaching event handlers and running validation. There has been a trend in many different languages across frameworks and even languages to build headless components that take care of the heavy lifting and let the developer care about how things look. Some examples: halogen-formless, Downshift, HeadlessUI.
And so a form DSL should focus only on the correctness of the data and validations, and let the user define the layout.
A pure function at the core
The DSL should then allow to define forms as pure functions, those functions would take the input, which defines the fields of the form, and return and output which corresponds to the validation and results.
let
validate_email email = (#foreign yup.email) email
validate_pass pass =
pass.length < 3 | Error TooShort
pass.length > 100 | Error TooLong
otherwise | Succeed pass
in
\{ username, email, password, password_confirm } ->
{ username =
username.length < 3 | Error TooShort
otherwise | Succeed username
, email = validate_email email
, pass = validate_pass pass
, pass_confirm =
pass_confirm == pass | Succeed
otherwise | Error DoesNotMatch
}
The DSL would compile to code than can then be integrated in the application consuming the form.
For example the snipped above could produce a React hook that returns the fields and handlers ready to be used in the form and field elements, pretty much like react-hook-form, in fact it could even compile to react-hook-form code under the hood.
External vs. embedded DSLs
The goal of an external DSL, as opposed to an embedded one, is to be able to produce the simplest compiled code possible.
Eg. in the case of TypeScript instead of having library code highly dependent on generics and all the TS type level machinery, the compiled code could result in very simple and specified code. Which in turn should make the type inference and errors simpler and easier to understand.
Static powers
Another benefit of using a non-turing complete DSL focused on pure functions would be leverage the static analysis to improve the correctness and development experience.
Eg. a simple playground, like the Grace one, could allow the developer to implement and test the forms.
Open questions
- How to handle:
- asynchronous validation?
- optional fields?
- variadic fields?
- What should be the interface for
foreing
code? - Is this just halogen-formless for TypeScript?
- How to deal with dependencies in the form?
- And option is that the form-defining function could take extra arguments that would be passed to the resulting code during runtime.
\dep1 -> \dep2 -> ... -> \depN -> \input -> output