Poslední články

"Being clever" antipattern in Elm

aneb sometimes more is less

Disclaimer: I write this article based on my experience and "feels" - and thus the points might not be right and are open to discussion! In the post I say things like "Elm values X and Y" - and might be wrong. I believe I'm not though :) Definitely feel free to write a comment below!


I sometimes lurk on the Elm Slack (obligatory registration link!) and talk with people hanging there. We have the #beginners and #help channels for questions of any kind, however trivial.

Today a person wanted to optimize this expression:

List.partition ((==) 2) [1,2,3]

with regards to parentheses count (ie. get rid of them). A few solutions appeared:

equals2 =
    (==) 2
List.partition equals2 [1,2,3]

flip List.partition [1,2,3] <| (==) 2

and some judgement was made:

"personally, the parentheses before made it easier to read and like more normal elm code"

I understand the poster's question - he wants to see if there's a better way to write his code. I often feel this way in Haskell. Given how there are many (many many many) operators, there often is a way to write more succint code.

In Elm, though, there is usually One Good Way™ to do a particular thing, and it's a simple way at that.

In this particular example, the original code is probably good enough. But I would even go as far as to not use the ((==) 2) part, and go for maximum readability. For me, that means:

List.partition (\x -> x == 2) [1,2,3]

The thing is, ((==) 2) might not be too bad, but ((<) 2) would stop me for anywhere from 5 to 30 seconds before I was sure of what it does. ("Do I have the condition reversed in my head?")

Compare it to this version, where it's immediately obvious what the function does and that it's correct.

List.partition (\x -> x < 2) [1,2,3]

Some functions are commutative (a == b is the same as b == a) and some are not (a < b vs b < a), and I really really don't want to think about "do I have it right?" two weeks from now just because I used the partial function application a few minutes ago and feel clever about it.


The same goes for "point-free" style (ommiting arguments from function definitions):

sum : List number -> number
sum =
    List.foldl (+) 0

Again, this particular example isn't too bad but it makes me freeze for a few seconds: "Wait a minute, the type definition says something else than the arguments of the function!"

I guess going a bit more silly with this example would illustrate it better:

sumWithStartingValue : number -> List number -> number
sumWithStartingValue =
    List.foldl (+)

I'd much rather have:

sumWithStartingValue : number -> List number -> number
sumWithStartingValue startingValue numbers =
    List.foldl (+) startingValue numbers

because there is absolutely zero magic, zero cleverness, zero figuring out how the particular function works.


Haskell has this culture of "clever is better", and sure, it allows the programs be very, very terse. But I don't really see the value in that, and as far as I can tell, Elm doesn't either.

Elm doesn't value writing terse code, or clever code. Elm values readability. If you get thoughts like "This is so painfully explicit", like I do when seeing and writing code like (\x -> x == 2), don't let them make you refactor it into something more clever, but less readable.

Union types antipattern in Elm

aneb still some ways to shoot yourself in a foot

Note for English speakers: Sorry for not having an all-English blog interface - it's mostly Czech blog after all, and for now I'm far too lazy to do something about a localization switch :) I'm sure you'll be able to guess what means what, though - comments, etc. You're smart!


So, lately I've been creating a microsite for my employer's conference. That seemed like a great place for sneaking some Elm in (no JS-only people to talk me out of it), and guess what - it is! I'm absolutely loving it so far.

During the development of a tab view, I dared make an union type:

type Town
    = Ostrava
    | Praha

Motivation: we have a tab for each town, and only one is visible at one time. So this would serve as an ID for whatever function is working with the towns.

So far, this has been a no-brainer. The problems started to crop up when I wanted to connect this to the model. My first try led me into a blind alley of runtime checks and unnecessary Maybe wrappers for things I KNEW were going to be found. The second try was much cleaner and, retrospectively, obvious, but... this is the story of the first try.


You see, I started with this:

type alias Model =
    { towns :
        { ostrava : TownInfo
        , praha : TownInfo
        }
    }


type alias TownInfo =
    { score : Int
    , selected : Bool
    }

(For the purposes of this post, I'll set the goal of getting a score for the currently selected town.)

It seemed okay. But it leads to:

score : Model -> Int
score model =
    if model.towns.ostrava.selected then
        model.towns.ostrava.score
    else if model.towns.praha.selected then
        model.towns.praha.score
    else
        -- pick one of the two above and use it as a default?
        -- ... or wrap everything here in Maybe and have THAT propagate above?
        ???

As you can imagine, this is quite error-prone. You can forget to add a new if, and the compiler won't tell you. It can't enumerate over a record!

You might not give up so easily though. Let's give this one more try.

score : Model -> Int
score model =
    let
        { ostrava, praha } =
            model.towns

        towns =
            [ ( Ostrava, ostrava.selected )
            , ( Praha, praha.selected )
            ]
    in
        towns
            |> List.filter (\( town, selected ) -> selected)
            |> List.head
            |> Maybe.map Tuple.first
            |> Maybe.withDefault Ostrava -- kinda arbitrary

This whole Maybe stuff seems absolutely unnecessary! And again, you're enumerating the cases of the union type by hand - this is error-prone, you can forget some, and by putting it into a list you have to jump through hoops just to make the compiler certain you didn't shoot yourself in a foot.


The right thing to do? Have the selected ID be more top-level.

type alias Model =
    { selectedTown : Town
    , towns :
        { ostrava : TownInfo
        , praha : TownInfo
        }
    }


type alias TownInfo =
    { score : Int }

You can then use functions which use compiler's exhaustiveness checking with the case town of ... pattern:

score : Model -> Int
score model =
    case model.selectedTown of
        Ostrava ->
            model.towns.ostrava.score

        Praha ->
            model.towns.praha.score

Suddenly all the hard stuff is gone. Writing this was so easy it's almost embarassing I didn't think of it the first time. But hey, experience comes with practice!


All in all, this ties back to Richard Feldman's talk "Making Impossible States Impossible". With the final model, it's guaranteed you can only have one town selected. With the former model, you could have any number of the towns selected! A bug in your app could make none or all of them selected, and the compiler was right to give you trouble with all the Maybe stuff!

So, the conclusion is: pay attention to how easy it is to write stuff! If it doesn't want to come out nicely, there's probably a better pattern hiding.

And remember: if in doubts, ask on the Elm Slack (registration) - we're a friendly bunch! :)

Životní moudro

aneb ouch

Jen vám chci sdělit, že co na internet napíšete, vás dřív či později dohoní. A kousne. (Souvislost s tím, že tady chybí jeden článek, je jen... čistě náhodná. :D )