Designing Opaque Type for form fields in Elm: Part 2
Seiya Izumi
Posted on April 12, 2020
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.
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.
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
}
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
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
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
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
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.
IzumiSy / elm-compositional-form-field
Typed form implementation in Elm
Posted on April 12, 2020
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.