When building an Elm web app you may need to use a third-party package to help solve your problem. In some cases, your usage requirements may not perfectly align with the API provided by the package. In these cases, rather than scatter your use of the package throughout different modules of your application, it is usually better to limit your use of the package to a single module (which I will refer to as a wrapper module) with an API that better suits your application's needs.
Why use a wrapper module?
- It helps you reduce the coupling between different parts of your application. This in turn makes your application more flexible to changes. For e.g. if another package comes along that is better in some way, say it has better performance, then because the old package is used exclusively within a wrapper module it will be much easier to switch to the new package.
- You can define the API of your wrapper module independently of the API of the package. This allows you to design an API that's a better fit for your application.
- It can be easier to unit test your wrapper module.
Obviously, there are trade-offs and not every situation warrants it's use.
Learn more: Using third-party libraries - always use a wrapper?.
A practical example
The Flight Booker task from 7GUIs had certain date requirements that were easier to satisfy if I used a preexisting package. justinmimbs/date package had support for everything I needed to do and more but it did things a little differently.
My requirements were straightforward:
- Be able to get today's date.
- Be able to create a date from a string of the form
DD.MM.YYYY
. - Be able to determine if one date comes after another date.
- Be able to convert a date to a string of the form
DD.MM.YYYY
.
By writing a wrapper module I was able to contain my use of justinmimbs/date
package and expose the precise API I needed to handle the date requirements.
module Task.FlightBooker.Date exposing
( Date
, fromString
, isLaterThan
, toString
, today
)
import Char
import Date as JDate
import Parser as P exposing ((|.), (|=))
import Task
import Task.FlightBooker.Parser as P
type Date
= Date JDate.Date
today : (Date -> msg) -> Cmd msg
today toMsg =
JDate.today
|> Task.perform (toMsg << Date)
fromString : String -> Maybe Date
fromString s =
case P.run dateParser s of
Ok iso ->
case JDate.fromIsoString iso of
Ok date ->
Just <| Date date
Err _ ->
Nothing
Err _ ->
Nothing
dateParser : P.Parser String
dateParser =
P.succeed (\dd mm yyyy -> yyyy ++ "-" ++ mm ++ "-" ++ dd)
|= P.chompExactly 2 Char.isDigit
|. P.chompIf ((==) '.')
|= P.chompExactly 2 Char.isDigit
|. P.chompIf ((==) '.')
|= P.chompExactly 4 Char.isDigit
|. P.end
isLaterThan : Date -> Date -> Bool
isLaterThan (Date date1) (Date date2) =
JDate.compare date2 date1 /= LT
toString : Date -> String
toString (Date date) =
let
dd =
JDate.day date
|> String.fromInt
|> String.padLeft 2 '0'
mm =
JDate.monthNumber date
|> String.fromInt
|> String.padLeft 2 '0'
yyyy =
JDate.year date
|> String.fromInt
|> String.padLeft 4 '0'
in
dd ++ "." ++ mm ++ "." ++ yyyy
Source: Task.FlightBooker.Date.
From the source code above you can see how I ended up with a smaller and focused API.
- There's a custom
Date
type that wraps theDate
type from thejustinmimbs/date
package. A client of the wrapper module can only use the functions that work on myDate
type. This is how usage of the external package is limited and controlled. - There's one constructor named
fromString
that takes aString
and returns aDate
if theString
is of the formDD.MM.YYYY
. - There's a function named
isLaterThan
, which is much nicer to use over direct use of thecompare
function that thejustinmimbs/date
package provides. - There's a converter named
toString
that converts myDate
type to aString
of the formDD.MM.YYYY
. - And finally, there's a command tailored to my
Date
type for getting today's date.
Conclusion
Elm has great support for writing highly cohesive modular code that's loosely coupled and easy to maintain. With Elm's modules it's simple to hide implementation details and expose an API that caters to the precise requirements of your application. I hope the simple example I shared above gets you thinking about ways to clean up your own applications with helpful wrapper modules.
Top comments (2)
Amazing! Thank you for this post.
You can also do something "similar" but the other way around. Create a unified module with a custom type that will interface with multiple different libraries.
One scenario where that happened for me was when trying to use the same color values for different libraries. I wanted to use it for
elm-css
, which exposes aColor
type, but I also wanted to manipulate it in ways thatCss.Color
wouldn't allow me to, so I created a "proxy" type that allowed me to convert it to different types as needed, while my application only had to deal with my ownColor
type.That's a nice approach.