jcreed blog > Type Combinators for Interactive Forms

Type Combinators for Interactive Forms

I read this blog post titled "UI for sums must remember products " the other day, and I thought it was a really interesting idea. The core of it, I think, is the observation that the type of data that represents the state of a web form as you're editing it is not the same as the type of data that gets returned from a form when you submit it.

Just to copy the post's example here, if we have a form like

Then the type of the UI state while we're editing is something like the record type (I'm going to use subscript $0$ for all the variables pertaining to this example)

$E_0 := \{$ $projectType : \mathsf{Local} \celse \mathsf{Clone},$ $localProjectName : \mathsf{str},$ $gitRepoUrl : \mathsf{str},$ $accessToken : \mathsf{str},$ $clonedProjectName : \mathsf{str},$ $\}$

Whereas the type of data that results when the user clicks the submit button is more like a tagged union of two distinct record types:

$R_0 :=$ $\celse \mathsf{Local}\ \{\ localProjectName : \mathsf{str}\ \}\ $ $\celse \mathsf{Cloned}\ \{$ $gitRepoUrl : \mathsf{str},$ $accessToken : \mathsf{str},$ $clonedProjectName : \mathsf{str},$ $\}$
So although the "eventual type" of the data we're editing is $(\mathsf{str}) + (\mathsf{str} \x \mathsf{str} \x \mathsf{str})$, the "editing type" is actually something like $2 \x \mathsf{str} \x (\mathsf{str} \x \mathsf{str} \x \mathsf{str})$.

Defining Form-Types

In the blog post above, the author notes correctly that although it seems intuitive that we should turn $A + B$ into $2 \x A \x B$, it doesn't seem correct to naively apply this recursively. $(A + B) + C$ would turn into $(2 \x A \x B) + C$, and thence to $2 \x (2 \x A \x B) \x C$, which appears to have too many choices in it! What we really wanted was $3 \x A \x B \x C$.

So I asked myself: if there isn't a binary operation on types that associates how we want, is there an operation on "something more than types"?

Here's the answer I came up with: briefly, "yes, an operation on 3-tuples of types with a function". Define a "form-type" to be a tuple $(C, P, R, f)$ where $C, P, R$ are types, and $f : C \x P \to R$. This can represent the type of an individual form element, or a larger form made of multiple elements. The intuitions for these are:

Combinators for Building Form Types

Here's how we can build up form types. A simple text box is the form type $\overline{\mathsf{str}} = (1, \mathsf{str}, \mathsf{str}, \lambda \ll c,p\rr . p)$. There are no interesting choices to be made, so the choice type is the unit type. The data to be edited is string, and the data to be returned is string.

We now define two binary operations $\oplus$, and $\otimes$, each taking two form types and yielding a form type. They can be thought of as "disjunctive" and "conjunctive" ways of combining forms, but I'm not claiming they're the coproduct or product in any particular category right now.

The operation $\oplus$ is defined by saying

\((C_1, P_1, R_1, f_1) \oplus (C_2, P_2, R_2, f_2) = (\) $C_1 + C_2,$ $P_1 \x P_2,$ $R_1 + R_2,$ $\lambda \ll c, \ll p_1, p_2\rr\rr. \bb{case} c \bb{of}$ $\celse (\bb{inl} c_1) \Rightarrow f_1\ll c_1, p_1\rr$ $\celse (\bb{inl} c_2) \Rightarrow f_2\ll c_2, p_2\rr,$ \()\)
and the operation $\otimes$ is defined by saying
\((C_1, P_1, R_1, f_1) \otimes (C_2, P_2, R_2, f_2) = (\) $C_1 \x C_2,$ $P_1 \x P_2,$ $R_1 \x R_2,$ $\lambda \ll\ll c_1, c_2\rr, \ll p_1, p_2\rr\rr. $ $\ll f_1\ll c_1, p_1\rr, f_2\ll c_2, p_2\rr \rr,$ \()\)

Associativity

Now we can revisit how this solves the "$3 \x A \x B \x C$ problem". Let's say the "edit type" $\E(\hbox{---})$ of a form-type is defined by $\E(C,P,R,f) := C \x P$. Suppose we have form types with trivial choice-types $\ol X_i = (1, P_i, R_i, f_i)$ for $i \in \{1,2,3\}$.

Observe that $\E(\ol X_1 \oplus \ol X_2) = (1 + 1) \x (P_1 \x P_2) $, as expected, but also $\E((\ol X_1 \oplus \ol X_2) \oplus \ol X_3) = (1 + (1 + 1)) \x (P_1 \x (P_2 \x P_3)) $, as desired.

More generally, if we had had $\ol X_i = (C_i, P_i, R_i, f_i)$ for $i \in \{1,2,3\}$, then we would have \[\E((\ol X_1 \oplus \ol X_2) \oplus \ol X_3) = (C_1 + (C_2 + C_3)) \x (P_1 \x (P_2 \x P_3)) \] which seems intuitively right to me.

Validation

This discrepancy between "edit types" and "result types" is even bigger if we consider input fields that have more complex validation. Suppose our form also had a numeric input, like:

and suppose we want the user to be able to edit its ephemeral value as a string, and defer forcing validation until they try to submit. This subtlety has actually come up for me implementing forms; if you try to make a react form and force the state to always be a number, you can't fully delete the number that's there as a default before typing in your preferred value.

So in order to handle validation, we need to represent the possibility that validation fails!

So let's generalize form-types by including one more component type, for errors: a form type is now a tuple $(C, P, R, E, f)$ where $C, P, R$ are types, and $f : C \x P \to R + E$. The act of submitting takes the choices $C$ and the partially used data $P$, and produces either a result $R$ or an error $E$.

Revising the definition of combinators

Now we can still define a sensible $\oplus$ and $\otimes$. We say:
\((C_1, P_1, R_1, E_1, f_1) \oplus (C_2, P_2, R_2, E_1, f_2) = (\) $C_1 + C_2,$ $P_1 \x P_2,$ $R_1 + R_2,$ $E_1 + E_2,$ $\lambda \ll c, \ll p_1, p_2\rr\rr. \bb{case} c \bb{of}$ $\celse (\bb{inl} c_1) \Rightarrow \iota_1 (f_1\ll c_1, p_1)\rr$ $\celse (\bb{inl} c_2) \Rightarrow \iota_2 (f_2\ll c_2, p_2)\rr,$ \()\)
and
\((C_1, P_1, R_1, E_1, f_1) \otimes (C_2, P_2, R_2, E_2, f_2) = (\) $C_1 \x C_2,$ $P_1 \x P_2,$ $R_1 \x R_2,$ $E_1 + E_2,$ $\lambda \ll\ll c_1, c_2\rr, \ll p_1, p_2\rr\rr. \bb{case} f_1\ll c_1, p_1\rr \bb{of} $ $\celse (\bb{inr} e_1) \Rightarrow \iota_3(e_1)$ $\celse (\bb{inl} r_1) \Rightarrow \bb{case} f_1\ll c_1, p_1\rr \bb{of} $ $\celse (\bb{inr} e_2) \Rightarrow \iota_4(e_2)$ $\celse (\bb{inl} r_2) \Rightarrow \bb{inl}(\ll r_1, r_2 \rr),$ \()\)
where
$\iota_1 : (R_1 + E_1) \to (R_1 + R_2) + (E_1 + E_2)$ $\iota_2 : (R_2 + E_2) \to (R_1 + R_2) + (E_1 + E_2)$ $\iota_3 : E_1 \to (R_1 \x R_2) + (E_1 + E_2)$ $\iota_4 : E_2 \to (R_1 \x R_2) + (E_1 + E_2)$
are the obvious injections which seemed like they would be more confusing if written out explicitly.

Let us briefly note what's going on here. In the $\oplus$ of two forms,

In the $\otimes$ of two forms,

Open Question

It's not clear to me if it's possible, but it seems desirable if so — can this asymmetry be repaired somehow? It seems otherwise so pleasant that $\oplus$ is "3/4s sum types" and $\otimes$ is "3/4s product types". Connecting any of this to lenses or monads or anything also seems tantalizingly possible, but I don't yet see how.