Considering the usefulness of a type parameter [Library / API Design Question]

halogen
formless

#1

As I continue working on Formless, I’d like to continue publicly discussing problems and their solutions, whether it’s straightforward PureScript, library / API design, something Halogen focused, and so on. I hope that others are able to stumble across the discussions and glean some insight from them!

Today, I’m struggling with a library / API design question, and I’m curious to hear from any users of Formless as well as anyone experienced writing PureScript libraries / Halogen components as to what they prefer.

I’ve given it a longer treatment below, but here’s the shortest version of the question:

I have the choice of creating two versions of some type X:

data X userForm formOutput = ...
data X userForm = ...

This type represents the set of form inputs (userForm) and in an alternate version also represents what type successful form submission is meant to parse to (formOutput).

In the first case, the user sees both their set of form inputs and the eventual output they want to parse a successful form submission into. In the second case, the user sees only their set of form inputs, which they can then transform into whatever they want after the fact.

The type parameter brings almost no new conveniences to using the library. Its only purpose is to let the library apply your submission function on your behalf rather than just return the form to you. Given that you could just apply the submission once you’ve got the form the benefit there is minimal at best.

But this extra type parameter is interesting for another reason: it lets you know, at a glance, what type this form is going to result in. That might save you from another frustration: digging through your source code to remember what you were going to parse that form into.

My question: given that the type parameter adds almost no functionality to a component using it, but it does add some possibly-useful information at a glance, is it worth the extra complexity of carrying it around and teaching it to beginners?


#2

The long version

The Formless component has a lot of type parameters. Here’s the query type:

data Query parentQuery childQuery childSlot userForm formOutput m a = ...

The first three types are all used so that you can embed whatever components you’d like into a form. The m parameter specifies the monad Formless runs in, which can be whatever you want, and there’s the standard a for Halogen queries. Let’s look at a simpler type synonym:

type Query' userForm formOutput m a = ...
  • userForm is the form type the user has specified and passed to Formless. It might be { password1, password2, email }, for example.
  • formOutput is the resulting type the form is meant to produce. It might be { email, password }, for example, collapsing the password fields.

Formless accepts a function, submitter, which is essentially a function userForm -> m formOutput. The entire point of this function is that it will turn the form type into the output type before returning that to the end user in a message:

data Message formOutput
  = Submitted formOutput
  ...

How things work right now

In the current design, the user must pass a submitter function as input, and Formless will apply it before returning the output in a message when the user submits a form.

This doesn’t save you much work – you had to write the function either way, and you could have just applied it when you received the Submitted message rather than it being applied before the message is sent. It’s the different between (Submitted userForm) and (Submitted formOutput).

For this marginal benefit, Formless picks up an extra type parameter, formOutput. Worse, this type parameter can get confusing if, say, you’re doing a computation that could produce an error (now it’s Either Error FormOutput) or multiple types, depending (now it’s Either Form1 Form2), or you still have to do a transformation afterwards.

That said, the nice thing about this type parameter is that it’s quite informative – it’s essentially saying “This form will produce this type.” It’s a particularly nice parameter:

Formless.Query pq cq cs MyForm User m a
-- vs.
Formless.Query pq cq cs MyForm m a

As it stands, I’m conflicted as to which of these is better. You could just write UserForm, assume that it will parse to a User, and call it a day. It might be frustrating to have to carry the extra type everywhere and it makes the component more intimidating type-signature-wise.

But it may also be frustrating to have to look in your code to remember what the form parses into on a successful submission. Much easier to glance at the type and see there.

To restate the question:

given that the type parameter adds almost no functionality to a component using it, but it does add some possibly-useful information at a glance, is it worth the extra complexity of carrying it around?


#3

hot take: if you can solve it with a comment, why bother with code?


#4

I don’t know, I was thinking that maybe keeping the validators but removing the result type would be a good idea to increase form reusability.
But then how often does it happen that a form with the same fields is used to produce different results?

Anyway, I think it’s not that urgent to make any change to this as I don’t see this as inosrmountable obstacle for inexperienced users.