Hi, folks. I’m trying to work out the idiomatic Elm-way of accomplishing this task. I have a case in an update function to which I’d like to pass another function. This passed function is a setter on a record:
update msg model =
case msg of
SetLatLon assign s ->
let
oldCoord =
case model.location of
Nothing -> {lat = 0.0, lon = 0.0}
Just c -> c
f = String.toFloat s |> Maybe.withDefault 0.0
newCoord = assign oldCoord f
loc = if isEmpty s then Nothing else Just newCoord
in
({model | location = loc}, Cmd.none)
...
The Coord type is an alias for {lat: Float, lon: Float} and model.location is a Maybe Coord.
This SetLatLon message is being invoked from view in this manner:
, input
[ type_ "text"
, title "Latitude"
, SetLatLon (\c f -> {c | lat = f}) |> onChange
] []
, input
[ type_ "text"
, title "Longitude"
, SetLatLon (\c f -> {c | lon = f}) |> onChange
] []
My puzzle is how to define the Msg custom type. What I have that’s working (after considerable tinkering) is to declare another alias:
type alias Setter b a = { b | lat : a } -> a -> { b | lat : a }
Something bothers me about having to explicitly specify lat in the Setter definition, yet I’m still passing in a function that sets either lat or lon. I’d like to be explicit in the Setter alias that a Coord is expected rather than simply a type of record having a lat member. It would also be nice to be able to omit the explicit Setter alias altogether and just define the SetLatLon subtype using the plain type definition of the passed function. I haven’t figured out how to do either of these things, which leads me to suspect I’m going about this in a suboptimal, non-idiomatic way.
I appreciate any tips on how I could approach this differently or could approach it the same way but with clearer syntax.
type alias Setter b a = { b | lat : a } -> a -> { b | lat : a }
...
, SetLatLon (\c f -> {c | lon = f}) |> onChange
But perhaps the compiler infers that c must have a lat field from the SetLatLon type, but is also happy since that is an extensible type that it can also have a lon field.
-- You define Setter over an extensible type with params a and b
type alias Setter b a =
{ b | lat : a } -> a -> { b | lat : a }
-- This is the concrete Coord type, not extensible but does satisfy { b | lat : Float }
type alias Coord =
{ lat : Float, lon : Float }
-- Here you give Setter the Coord type as argument a.
type Msg
= SetLatLon (Setter Coord Float) String
In the last one where you give Setter the Coord type as argument a, you are instantiating the constructor with the Setter to have the type Coord -> Float -> Coord. So it all works out right.
Nice trick with polymorphic updates, I think this has been posted on here before somewhere. Shame about the function in Msg type though - it works just bordering on bad practice.
I see. That makes sense that it ends up satisfying the type constraint by calling the Setter constructor with specific values. I only stumbled across the existence of extensible types in the process of trying to sort out this issue and am still trying to wrap my head around them. I was surprised to see that Elm essentially supports what is effectively type-checked duck typing.
The way I came up with that initial overly-generic declaration for the Setter alias was going by what the repl reported when I declared that anonymous function. Now I see I didn’t need that level of genericness. I think I just wasn’t thinking clearly enough about the fact that regardless of what was being done within the anonymous function, ultimately I was just passing in a Coord and a Float to get back a Float.
Yes, I suppose that is what an extensible type is - so long as this thing has such and such properties, we don’t care what the rest of it is. Strictly speaking, I think duck typing means that this only has to hold true at runtime, but in Elm with extensible records this is checked at compile time.
Note though, that it is not the same as subtyping. I mention this as the title of the topic has the word ‘subtype’ in it.
Right. In Elm parlance I think the term I was searching for is “variant” rather than “subtype.”
Just now noting your earlier comment about it bordering on being a bad practice to define a Msg variant as having a function parameter. What is it that’s undesirable about that?
Generally, we try to avoid putting functions in Msg or Model but there is really nothing in Elm or TEA that stops you doing it, and it will work just fine. Sometimes there is some neat trick like this that is just too tempting to ignore.
The downsides are:
If you were running the debugger and exported the debug session, as functions cannot be serialised the output would not be something that could be used to recreate the application state. Similarly if you logged the msg to the console you would not get something that tells you what function was being applied.
From the perspective of someone reading your update logic, some function is applied to the model to update it, which does not make it obvious what is actually being done to the model. The code would be more explicit with individual messages for updating lat and updating lon. You traded off boiler plate code against being explicit.
Imagine some application where all updates to the model are done with functions:
type alias Model = { ... }
type alias Msg =
Model -> Model
update : Msg -> Model -> (Model, Cmd Msg)
update msg model =
(msg model,
Cmd.none) -- No side effects in this example.
This latter point is especially compelling. I initially implemented this functionality by defining another custom type (LatLon = Lat | Lon) and passing one of its variants in as a parameter to the update message, using a case to build up the new Coord accordingly. I didn’t like the verbosity of creating a whole new type just for that one purpose, but I hadn’t really accounted for the tradeoff I was making in terms of consistency in where business decisions are being made. Thanks for the clarification.
type LatLon = Lat | Lon
type Msg
= UpdateField LatLon Float
| ...
Is really just a nested version of:
type Msg
= UpdateLat Float
| UpdateLon Float
| ...
BTW, there is a technique by which data models in Elm can be simplified by math. A custom type behaves like + (plus), and record types behave like * (multiplication).
latlon = lat + lon
msg = latlon * float + x
msg = lat * float + lon * float + x
Following that guidance, this moves the logic back into update, incurring a minimal amount of redundancy and without introducing a new union type for a predicate:
--- a/src/Main.elm
+++ b/src/Main.elm
@@ -89,7 +89,8 @@ type Msg
| GetLocalZone Time.ZoneName
| RequestGeo
| ReceiveGeo (Maybe Coord)
- | SetLatLon (Coord -> Float -> Coord) String
+ | SetLat String
+ | SetLon String
| IsoDateChange String
| TemplateChange String
| SetTimeZone String
@@ -99,6 +100,18 @@ type Msg
update : Msg -> Model -> (Model, Cmd Msg)
update msg model =
+ let
+ setLatLon assign s =
+ let
+ oldCoord =
+ case model.location of
+ Nothing -> {lat = 0.0, lon = 0.0}
+ Just c -> c
+ f = String.toFloat s |> Maybe.withDefault 0.0
+ newCoord = assign oldCoord f
+ in
+ if isEmpty s then Nothing else Just newCoord
+ in
case msg of
SubmitRequest ->
(model, getThisDate model)
@@ -133,17 +146,11 @@ update msg model =
in
(newmod, getThisDate newmod)
- SetLatLon assign s ->
- let
- oldCoord =
- case model.location of
- Nothing -> {lat = 0.0, lon = 0.0}
- Just c -> c
- f = String.toFloat s |> Maybe.withDefault 0.0
- newCoord = assign oldCoord f
- loc = if isEmpty s then Nothing else Just newCoord
- in
- ({model | location = loc}, Cmd.none)
+ SetLat s ->
+ ({model | location = setLatLon (\c f -> {c | lat = f}) s}, Cmd.none)
+
+ SetLon s ->
+ ({model | location = setLatLon (\c f -> {c | lon = f}) s}, Cmd.none)
IsoDateChange isoDate ->
( {model | isoDate = isoDate}, Cmd.none)
@@ -268,15 +275,15 @@ formDisplay model =
, title "Latitude"
, placeholder "45.1023"
, mayFloat (.lat) |> value
- , SetLatLon (\c f -> {c | lat = f}) |> onChange
+ , SetLat |> onChange
] []
, label [] [text "Lon: "]
, input
[ type_ "text"
, title "Longitude"
, placeholder "-122.4180"
, mayFloat (.lon) |> value
- , SetLatLon (\c f -> {c | lon = f}) |> onChange
+ , SetLon |> onChange
] []
]