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

#continuehere

  • 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