I'm new to Elm (version 0.19
).
On thing that's bugging me is the huge list of arguments I'm passing around. I think the problem is due to my OOP way of thinking. In my code I have a bunch of helper functions that require access to my model
(TEA). I have been using let / in
syntax in the view function to define these helpers as this gives them access to the model argument. However I have 10+ helper functions and I'm constantly passing them around, it makes my code look ugly and hard to comprehend. In OOP these helper functions would all be methods on some object that I would pass instead.
Code snippet below is a contrived example that uses elm-ui. Full example can be run on Ellie
Element.layout []
<| column
[ w |> px |> width
, h |> px |> height
, blueBg
, centerX
, centerY
]
[ el [centerX, centerY, whiteTxt, fontSize 40] <| text "Hello world"
, header w h scale whiteTxt space blueBg pad radius whiteBg fontSize blackTxt greyBg blueTxt
]
header w h scale whiteTxt space blueBg pad radius whiteBg fontSize blackTxt greyBg blueTxt =
-- code here
el [] Element.none
Whats the best way to do this?
There are several strategies for this:
In the header example, you might provide your own types for the style, title and subtitle:
type alias Title = String
type alias SubTitle = String
header : HeaderStyle -> Title -> SubTitle -> Element Msg
When you do have lots of information to pass around, records are usually the first go-to for holding data. For example, if you're reusing the header function multiple times with different styling, then a HeaderStyle record type might be useful:
type alias HeaderStyle = { borderColor : Color
, textColor: Color
, backgroundColor : Color
...
}
Now since the title and subtitle are part of the header you could also pull it all into one record:
type alias HeaderData = { borderColor : Color
, textColor: Color
, backgroundColor : Color
, bannerText : String
, bannerImage : Url
, headerTitle : Title
, headerSubTitle : SubTitle
...
}
If we pay attention to the types supplied by the library, we can see that for elm-ui it might make more sense to keep the styles in a list to match their types. Modifying our record, we get:
type alias HeaderData = { styleList : List (Attribute Msg)
, headerTitle : Title
, headerSubTitle : SubTitle
...
}
The advantage here is that we can extract the style list and use it directly in a function from the Element module by calling the automatic function styleList: HeaderData -> List (Attribute Msg)
.
The disadvantage is that we've lost our nice compiler error message if someone makes a HeaderData missing some key styles.
Either way, now our header function is down to only one input.
header : HeaderData -> Element Msg
Great, but this still means that every time we run header we need to populate a HeaderData. How can we simplify that?
One strategy is to use constants at the top level, and use helper functions / partial application to apply these constants.
We can for example define a top level constant and helper function that lets us create various predefined headers:
pageStyle : List (Attribute Msg)
pageStyle = [ Border.color <| rgb255 35 97 146
, Font.color <| rgb255 35 97 146 ]
redHeaderStyle : Title -> HeaderData
redHeaderStyle title =
{ styleList : pageStyle
++ [ Background.color red
, Font.color black
...
]
, headerTitle : title
}
Here pageStyle can be used elsewhere, and readHeaderStyle adds to it for this specific case. Note that we've left one parameter for later, since the title may change for each application.
Of course in Elm you are not restricted to lists and records - you could also use product and sum types for Header. When to use each type is going to depend on things like where you want the type safety and how you want to compose functions.
So to summarize:
Start by modelling your problem domain using types, rather than thinking in terms of objects or components.
Don't try to make everything generic and reusable. Instead create types that hold information, and use helper functions so you only have to worry about what's relevant.
Don't be afraid of defining constants and helper functions at the top-level. Limit your use of let..in
constructs to improving readability, rather than for grouping definitions as though they're methods.
Write helper functions for your types on an as-needed basis, rather than trying to create a library of every possible functionality.
If you want to compartmentalise similar functions and types, you can use modules and then import them qualified to a convenient name. This way you can say e.g. Background.color
and Element.row
.
Finally, Scott Wlashchin gives a good talk on design strategies in functional programming: Functional programming design patterns by Scott Wlaschin