DEV Community

Seiya Izumi
Seiya Izumi

Posted on • Edited on

Designing Opaque Type for form fields in Elm: Part 2

In the previous article, we implemented Username module which encapsulates validation logic of form fields.

That's really enough at that moment, but it still has more room to get improved in viewpoint of extensibility.

Say, if we would like to implement one more another field of Email that have almost the same logic of Username, but with different validation rule. At some point, we can follow rules of WET (Write Everything Twice). Abstract something out too early is always a way to ruin your application. So, it would be better to be dumb about it just by cloning functions of Username into Email module. Then, module relation will be as follows.

Module relation diagram

It looks fine for now, but getting stinky. Being WET is nice, but don't always be wet. As solution toward this kind of case, we can take advantage of module composition in order to make modules highly coherent and reusable without breaking border of responsibility each module has. This is really similar to class inheritance, but way better than that.

Explicitness and Implicitness

Elm does not have class-based syntax so that naturally we always will take step to use module composition to extend behavior of one another module. This is a nice thing that we don't have any chance to get into pitfalls of class inheritance.

Class inheritance is usually seemed to be implicit, because the all public behaviour of parent classes is carried over by a child class, sometimes intentionally, but sometimes not. This means class inhertance is not suitable to pick only some specfic behaviours of other classes.

Composition seems way explicit on the contrary. If we don't have to inherit some specific functionality, just ignore it. It even has chance to inherit behaviours of compositing classes just by delegation. Simple and concise.

Using module composition in Elm is a key to make your modules extensible and reusable all the way. Extracting business rules in your application into Opaque Type is nice to decouple interface and implementation. Module composition can empower it to be reusable in a good manner.

Overall design

Now, let's apply module composition into our application.

overall design

Right before using composition, each module had its own implementation intendedly duplicated. However, now, their internal implementations are delegated to Field module that has fundamental implementation abstracted out of Username and Email.

Field module

Field module now has three states initially Username had before.

validator field in Common record is important. That is defined as a function interface that gets injected in init function. The reason why validator interface requires implementation to return ( String, err ) tuple is that, even though validation fails, the current value is needed to be accessible to show it on input field. If it has only err, the value typed by users will be lost. This must not.

module Field exposing (Field, init)


type alias Common err =
    { value : String
    , validator : String -> Result ( String, err ) String
    }


type Field err
    = Partial (Common err)
    | Valid (Common err)
    | Invalid (Common err) err


init : String -> (String -> Result ( String, err ) String) -> Field err
init value validator =
    Partial
        { value = value
        , validator = validator
        }
Enter fullscreen mode Exit fullscreen mode

Of course, Field module has input function and blur function as well, but they are more abstract.

module Field exposing (Field, init, input, blur, mapErrorToString)

import Html exposing (Html)
import Html.Attributes exposing (type_, value)
import Html.Events exposing (onBlur, onInput)


-- ...

input : (Field err -> msg) -> msg -> Field err -> Html msg
input onInputMsg onBlurMsg field =
    let
        onInputHandler =
            \value ->
                onInputMsg <|
                    case field of
                        Partial { validator } ->
                            Partial
                                { value = value
                                , validator = validator
                                }

                        Valid { validator } ->
                            value
                                |> validator
                                |> mapResult field

                        Invalid { validator } _ ->
                            value
                                |> validator
                                |> mapResult field
    in
    Html.input
        [ type_ "text"
        , value <| toString field
        , onBlur onBlurMsg
        , onInput onInputHandler
        ]
        []


blur : Field err -> Field err
blur field =
    case field of
        Partial { value, validator } ->
            value
                |> validator
                |> mapResult field

        _ ->
            field


mapErrorToString : (err -> String) -> Field err -> Maybe String
mapErrorToString translator field =
    case field of
        Invalid _ err ->
            Just <| translator err

        _ ->
            Nothing
Enter fullscreen mode Exit fullscreen mode

Some more internal functions

-- internals


mapResult : Field err -> Result ( String, err ) String -> Field err
mapResult field result =
    let
        validator_ =
            case field of
                Partial { validator } ->
                    validator

                Valid { validator } ->
                    validator

                Invalid { validator } _ ->
                    validator
    in
    case result of
        Ok value ->
            Valid
                { value = value
                , validator = validator_
                }

        Err ( value, err ) ->
            Invalid
                { value = value
                , validator = validator_
                }
                err


toString : Field err -> String
toString field =
    case field of
        Partial { value } ->
            value

        Valid { value } ->
            value

        Invalid { value } _ ->
            value
Enter fullscreen mode Exit fullscreen mode

Next, let's look into how to use this Field module under the way of module composition in order to create specialized modules.

Username module

As the way of module composition, almost all exposed function delegates its functionality to internal dependent module. Username module is just a wrapper of Field in this meaning that provides specialized behaviours extended from Field module.

One more great thing is that, interface of Username module is almost not changed since the last article even though its internal functionality is updated and being delegated! Actually, errorString function was newly introduced in place of error function that is described in the last article, but this is just an optional change.

Everything stays same overall. The stability of module interface is always important to avoid unintentional effect to other functionality.

module Username exposing (Error(..), Username)

import Field


type Username
    = Username (Field.Field Error)


type Error
    = LengthTooLong
    | LengthTooShort


empty : Username
empty =
    Username <|
        Field.init "" <|
            \value ->
                if String.length value > 20 then
                    Err ( value, LengthTooLong )

                else if String.length value < 5 then
                    Err ( value, LengthTooShort )

                else
                    Ok value
Enter fullscreen mode Exit fullscreen mode

input function and blur function are, as they exactly show, just does delagation to functions provided by Field module.

errorString is a little different. It works like a translator of Error type in this Username module. Error patterns vary on every module extensing Field so that Username module must have responsibility of translating error type to String message as a specialized module.

module Username exposing (Error(..), Username, blur, empty, errorString, input)

import Field
import Html exposing (Html)


-- ...

input : (Username -> msg) -> msg -> Username -> Html msg
input onInputMsg onBlurMsg (Username field) =
    Field.input (onInputMsg << Username) onBlurMsg field


blur : Username -> Username
blur (Username field) =
    Username <| Field.blur field


errorString : Username -> Maybe Error
errorString (Username field) =
    Field.mapErrorToString
        (\error ->
            case error of
                LengthTooShort ->
                    "Length is too short"

                LengthTooLong ->
                    "Length is too long"
        )
        field
Enter fullscreen mode Exit fullscreen mode

This article has shown only the example of Username type, but with this strategy, it would be way easier to implement some similar types like Email, Biography that need validation like this. That's because essential logics to help us implement it as fields are now reusable.

Complete example is on Github.

GitHub logo IzumiSy / elm-compositional-form-field

Typed form implementation in Elm

Top comments (1)

Collapse
 
acbwme profile image
Adam Christopher

I'm learning Elm recently and having so much fun with it. Been writing one-file elm apps because I have zero knowledge on refactoring and delegating to files with opaque types. Stumbled upon your post and having tons of fun recreating the form field validation! Thank you so much for your post, it helped me a lot understanding refactoring to opaque types with delegation!