glasserc / elm-form-result / Form.Result.AnyJust

A helper type to look for a Just in a pile of Maybes.

Background

Let's imagine you have a FormError type like this:

type alias FormError =
    { password : Maybe PasswordStrengthError
    , acceptedTerms : Maybe MustAcceptTermsError
    }

You might produce these values by validating a registration form. You'd augment the form with any errors you produced, and as the user updated the form, you would clear the error fields (until you validated it again).

You have a submit button but you want to disable it if there's an error that needs correcting, so you add a disabled attribute according to the condition Maybe.isJust formErrors. But eventually the user will clear the last error and you'll have a FormError full of Nothings. You need to convert that Just "empty" FormError into a Nothing so that the submit button can be activated again.

You could write a function like this:

flatten : FormErrors -> Maybe FormErrors
flatten ({ password, acceptedTerms } as formErrors) =
    case ( password, acceptedTerms ) of
        ( Nothing, Nothing ) ->
            Nothing

        _ ->
            Just formErrors

But this function is brittle. If you add a field to the FormErrors type, this function needs to be updated too, and the compiler won't help you.

Instead, this module provides an AnyJust type that re-uses the constructor to your FormErrors type to ensure that every field is checked for presence. You could re-write the flatten function with it as:

flatten : FormErrors -> Maybe FormErrors
flatten formErrors =
    AnyJust.start FormErorrs
        |> AnyJust.check formErrors.password
        |> AnyJust.check formErrors.acceptedTerms
        |> AnyJust.finish

As the name implies, if any field is Just, then at the end, you wind up with a Just FormErrors (produced by re-constructing it with the values given). If no Just was provided to AnyJust.check, then you end up with Nothing. The reconstruction of the FormErrors is slightly inefficient, but in exchage, we can leverage the compiler to complain if we add another field.


type alias AnyJust a =
{ model : a, seenJust : Basics.Bool }

A utility type for checking for a Just in a pile of Maybes.

It wraps some other type, typically a constructor, and as you feed it arguments using check, it updates a Bool to see if it ever saw a Just. At first, this Bool is False. It switches to True if check is ever given a Just.

You can feel free to think of this type as a writer monad for a Bool. You can also feel free not to think of it that way, if you don't know what that means or find it confusing.

start : a -> AnyJust a

Start an AnyJust that has never seen a Just.

check : Maybe a -> AnyJust (Maybe a -> b) -> AnyJust b

Feed a Maybe to the underlying type, checking if it's a Just.

finish : AnyJust a -> Maybe a

Resolve the AnyJust, producing Just a if we ever saw a Just.

map : (a -> b) -> AnyJust a -> AnyJust b

Functor map, just for completeness.

Be careful not to use this instead of check if you intend to check a Maybe. If you do, the types will still work out, but the finish function will not produce a Just at the end.

andMap : AnyJust a -> AnyJust (a -> b) -> AnyJust b

Applicative andMap, just for completeness.

The resulting AnyJust will register as having seen a Just if either input saw a Just.

Be careful not to use this instead of check if you intend to check a Maybe. If you do, the types will still work out, but the finish function will not produce a Just at the end.

andThen : (a -> AnyJust b) -> AnyJust a -> AnyJust b

Monadic andThen, just for completeness.

The resulting AnyJust will register as having seen a Just if the original AnyJust saw a Just or if the output AnyJust saw a Just.

Be careful not to use this instead of check if you intend to check a Maybe. If you do, the types will still work out, but the finish function will not produce a Just at the end.