Poslední články

Defunctionalization in Elm

aneb stories from the war

It was not so long ago that I opened HackerNews and saw the title The Best Refactoring You've Never Heard Of. The refactoring in question was "defunctionalization", which really was a term I never heard of, even though some of the given examples looked familiar. The talk gave some formless blob in my brain's idea space a name ("reified it", hello Rich Hickey!), and all around it was a very cool talk. (Go watch it!)


Fast forward a few days. I was fighting an ugly piece of code in our Elm codebase - one I wrote, again, not that long ago. Yeah, not my proudest moment.

Me reacting to my code.

As you can see, a lightbulb went off in my head. This sounds like THAT thing! There was nothing left to do than to try it.


The problem itself was simple to understand, but hairy in details.

We have a top-level Store.

type alias Store =
    { categories : WebData (Dict CategoryId Category)
    , questions : WebData (Dict QuestionId Question)
    , bookmarks : WebData (Dict BookmarkId Bookmark)
    , savedQueries : WebData (Dict SavedQueryId SavedQuery)
    -- ... you get the idea
    }

We also have our own Msg type in the Store module and update-like functions for fetching stuff.

fetchQuestions : Config msg -> Flags -> Store -> ( Store, Cmd msg )
fetchQuestions config flags store =
    fetch_
        { get = .questions
        , set = \val store_ -> { store_ | questions = val }
        , onSuccess = QuestionsFetched
        , request = API.getQuestions
        }
        config
        flags
        store

-- one such function for each field in the Store record

Now, many of our pages need multiple such entities fetched. So we do something similar to elm-fetch, and define what route needs which entities.

fetchForRoute : Route -> Cmd msg
fetchForRoute route =
    case route of
        Router.CrosstabBuilder CrosstabBuilder.List ->
            Store.fetchXBProjects

        Router.CrosstabBuilder (CrosstabBuilder.Detail _) ->
            Store.fetchMany
                [ Store.fetchQuestions
                , Store.fetchCategories
                , Store.fetchAudiences
                , Store.fetchAudienceFolders
                , Store.fetchXBProjects
                ]

        -- ...

As you can see, we use a fetchMany function which allows us to compose these together. It's essentially a List.foldl over our (Store, Cmd msg) type, with the catch that the fetch functions need a bunch of other stuff in parameters.

fetchMany : List (Config msg -> Flags -> Store -> ( Store, Cmd msg )) -> Config msg -> Flags -> Store -> ( Store, Cmd msg )
fetchMany list config flags store =
    List.foldl
        (\fetchAction ( currentStore, currentCmd ) ->
            let
                ( newStore, newCmd ) =
                    fetchAction config flags currentStore
            in
            ( newStore
            , Cmd.batch [ currentCmd, newCmd ]
            )
        )
        ( store, Cmd.none )
        list

See that type signature? This is where we'll start encountering problems. Due to various constraints -- notably our app being both standalone (all those pages living in one Elm app) and embedded in a legacy JS shell -- we have to pass the pages a knowledge of how to do that fetchMany action.

type alias Config msg =
    { msg : Msg msg -> msg
    , ajaxError : Error -> msg
    , navigateTo : Route -> msg
    , openAlert : String -> String -> msg
    , refreshAudiences : msg
    , fetchMany : List (Store.Config Msg -> Flags -> Store.Store -> ( Store.Store, Cmd Msg )) -> msg
    , disabledExportsAlert : msg
    }

To not get into too many details, we have to do this in one more level because of our Elm-in-JS-shell architecture, and that's where it starts getting really ugly. I don't want you to read and understand the following code, I just want you to agree that it's really ugly. Please?

type Msg
    = FetchMany (List (Store.Config Msg -> Flags -> Store.Store -> ( Store.Store, Cmd Msg )))
    -- | ...

crosstabBuilderConfig : CrosstabBuilderComponent.Config Msg
crosstabBuilderConfig =
    { msg = CrosstabBuilderMsg
    , ajaxError = AjaxError
    , navigateTo = NavigateTo
    , openAlert = OpenAlert
    , refreshAudiences = RefreshAudiences
    , fetchMany =
        \list ->
            let
                innerConfig : Store.Config (CrosstabBuilderComponent.Msg Msg)
                innerConfig =
                    Store.configure
                        { msg = CrosstabBuilderComponent.OuterMsg << StoreMsg
                        , err = \store error -> CrosstabBuilderComponent.OuterMsg <| AjaxError store error
                        }

                changeFn :
                    (Store.Config (CrosstabBuilderComponent.Msg Msg) -> Flags -> Store.Store -> ( Store.Store, Cmd (CrosstabBuilderComponent.Msg Msg) ))
                    -> (Store.Config Msg -> Flags -> Store.Store -> ( Store.Store, Cmd Msg ))
                changeFn fn =
                    \_ flags store ->
                        fn innerConfig flags store
                            |> Glue.map CrosstabBuilderMsg
            in
            FetchMany (List.map changeFn list)
    , disabledExportsAlert = DisabledExportsAlert
    }

Bleh. All this was essentially Msg mapping to appease the type system, and it got very complicated very fast.

Getting out of this mess

At the time of writing, I didn't see a way out of this. I knew it was ugly but it was the best I could do. But now, with the knowledge of defunctionalization, could I try to apply that here?

Let's see:

type FetchAction
    = FetchCategories
    | FetchQuestions
    | FetchBookmarks
    | FetchSavedQueries
    -- | ...


fetch : FetchAction -> Config msg -> Flags -> Store -> ( Store, Cmd msg )
fetch =
    -- exercise for the reader
    Debug.todo "Store.fetch"


fetchMany : List FetchAction -> Config msg -> Flags -> Store -> ( Store, Cmd msg )
fetchMany =
    -- exercise for the reader
    Debug.todo "Store.fetchMany"

What I've done above is replaced all those fetchQuestions-like functions with a single fetch one, which looks very much like update now. Also, FetchAction appeared! This is the crux of defunctionalization - we have replaced functions with data and a way to interpret that data later. In essence, nothing changed, but we'll be allowed to have nicer types and get rid of that horrible boilerplate!

And now, because the fetchMany type annotation no longer contains any parameterized msg types, it simplifies all types that touch it to the point where we don't need to Cmd.map the Msg at all! That horrible piece of code becomes:

type Msg
    = FetchMany (List Store.FetchAction)
    -- | ...


crosstabBuilderConfig : CrosstabBuilderComponent.Config Msg
crosstabBuilderConfig =
    { msg = CrosstabBuilderMsg
    , ajaxError = AjaxError
    , navigateTo = NavigateTo
    , openAlert = OpenAlert
    , refreshAudiences = RefreshAudiences
    , fetchMany = FetchMany
    , disabledExportsAlert = DisabledExportsAlert
    }

And so, we had a happy ending (click to enlarge):

Happy ending


Conclusion

To recap, we were making our lives unnecessarily hard by

  • using parameterized msg types
  • sending functions that used them around in Msgs
  • and then had to create Rube Goldberg machines to Cmd.map the Msg types around correctly.

Rube Goldberg machine

To get out of this mess, we defunctionalized by

  • creating a datatype for all the various functions we needed to pass (FetchAction)
  • creating a function that used them (fetch : FetchAction -> ...)
  • passed the datatype around instead of the functions
  • applied the new function with the datatype instead of running the old, now nonexistent functions directly

Defunctionalization has other benefits than just simplifying type signatures. For example it allows you to serialize the action and send it over the wire if you need to!


I encourage you to watch the talk about defunctionalization if you haven't yet. It has more examples of defunctionalization in practice - which is always great for cementing a new concept in your head. Also sorry for probably a bit rushed blogpost - I was just excited by this simplification of our code and wanted to share it, without sharing too many unnecessary details. Hopefully I've managed to do that!

Notes from Elm Europe 2017

aneb ...wow!

I don't have much time today (I'm in Copenhagen in Denmark for a holiday) but I want to share my set of notes from Elm Europe 2017 (it was AMAZING, by the way!)

General advice

Persuading CTOs to let you use Elm

  • make them watch the Elm Europe 2017 talks by Gizra CTO Amitai Burstein and Nomalab CTO Sébastien Crème!

"Unlocking yourself" process

  • What are you trying to do?
  • What are the important features?
  • Can you write an example of usage?
  • Consult the community if you're stuck

Growing your Elm app

  • Split functions into smaller functions that can fit in your head
  • Narrow the types (both of arguments and return values)
  • Extensible records - restrict arguments, not model, and allow you to eliminate need for nested records
  • Html.map and Cmd.map solve the "too much config" problem
  • update can return whatever you want!

Designing APIs

  • Start with a real use case
  • Test with real people.
  • Allow for advanced use cases, but have simple defaults
  • Have both simple and advanced examples

Visualization talks (by both Jakub Hampl and Tereza Sokol)

  • Read "The Visual Display of Quantitative Information" by Edward Tufte!

CSS

  • in CSS, anything unexpected is an error

Check libraries and packages


All in all, again, it was an AWESOME event! I feel so inspired :)

By the way, my talk, my slides, repo and package!

Elm Europe, See you next year! :)

"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.