Laziness would be good for Time.Extra.posixToParts

· Allanderek's blog

A motivation for why laziness is a good programming language feature.
#elm #gren #programming #laziness

In this post I'm going to show a good example of where laziness would work well. This is of course not an argument that lazy programming languages are somehow better than strictly evaluated programming languages. Rather what I wish to do here is answer the question, what is laziness good for?

My example here comes from the justinmimbs/time-extra Elm package. The main purpose of this library is to provide a means for working with the standard library's Time.Posix values. Functions are provided to calculate the difference between two time values, and also to add/minus a given interval from a given time value. So for example it provides a convenient way to take a given time value and add one day, or two hours, or six months.

However, as an additional utility it provides the posixToParts which takes in a Time.Posix value (and a zone) and gives back a record:

1type alias Parts =
2    { year : Int
3    , month : Month
4    , day : Int
5    , hour : Int
6    , minute : Int
7    , second : Int
8    , millisecond : Int
9    }

So a common way you might use this, is to get the 'date' part of a time value. That is get the year, month and day values. So suppose you have a bunch of events, all that have start-times stored as a Time.Posix. Now, what you might want to do is show all of those events which are on today. Assuming that you have the current time as a Time.Posix, you can do something like this:

 1type alias Event =
 2    { start : Time.Posix 
 3    , -- Presumably other relevant data about an event.
 4    }
 5        
 6getTodaysEvents : Time.Zone -> Time.Posix -> List Event -> List Event
 7getTodaysEvents zone now events =
 8    let
 9        todaysParts : Time.Extra.Parts
10        todaysParts =
11            Time.Extra.posixToParts zone now
12        isToday : Event -> Bool
13        isToday event =
14            let
15                eventParts : Time.Extra.Parts
16                eventParts =
17                    Time.Extra.posixToParts zone event.start
18            in
19            eventParts.year == todaysParts.year
20              && eventParts.month == todaysParts.month
21              && eventParts.day == todaysParts.day
22    in
23    List.filter isToday events

This will work perfectly well, but it's doing quite a lot of work that is unnecessary. For each event it is calculating not just the, year, month, and day associated with the start Time.Posix, but also the hour, minute, second, and millisecond. These values are calculated, but just thrown-away. If there are a lot of events, then it might be desirable to write our own version of Time.Extra.posixToParts:

 1type alias Date =
 2    { year : Int
 3    , month : Time.Month
 4    , day : Int
 5    }
 6
 7posixToDate : Time.Zone -> Time.Posix -> 
 8posixToDate zone time =
 9    { year = Time.getYear zone time
10    , month = Time.getMonth zone month
11    , day = Time.getDay zone month
12    }
13        
14getTodaysEvents : Time.Zone -> Time.Posix -> List Event -> List Event
15getTodaysEvents zone now events =
16    let
17        today : Date
18        today =
19            posixToDate zone now
20        isToday : Event -> Bool
21        isToday event =
22            (posixToDate event.start == today)
23    in
24    List.filter isToday events

This works well enough, but it's slightly unsatisfying that I've had to re-implement a library function just because the library function did too much work.

But note, that even this is doing potentially too much work, if the year is not correct, we needn't check the month nor day.

 1getTodaysEvents : Time.Zone -> Time.Posix -> List Event -> List Event
 2getTodaysEvents zone now events =
 3    let
 4        todayDay : Int
 5        todayDay =
 6            Time.getDay zone now
 7        todayMonth : Time.Month
 8        todayMonth =
 9            Time.getMonth zone now
10        
11        todayYear : Int
12        todayYear =
13            Time.getYear zone now
14        isToday : Event -> Bool
15        isToday event =
16            Time.getYear zone event.start  == todayYear
17                && Time.getMonth zone event.start == todayMonth
18                && Time.getDay zone event.start == todayDay
19    in
20    List.filter isToday events

Because && does not evaluate the right-hand side if the left-hand side is False we avoid calculating the month and day of an event's start time if the year is not the same as the current one.

Even this version potentially does a small amount of work that it needn't. If none of the event start times are in the correct year then we will needlessly calculate today's month and day. Of course in this case, that's a minor extra calculation (and probably faster than the lazy version if laziness is done via thunks). However, note that it's non-trivial to see when you might be doing unnecessary work, and remember, that this is pretty simple exmaple.

It's not difficult to find such cases in your own code, where either you're potentially doing more work than is necessary, or you're crafting conditional evaluation in order to avoid unnecessary calculation. These conditionals will make your code more complex and hence more diffcult to maintain.

A thought experiment, how could the justinmimbs/time-extra Elm package achieve this configurability? First of all it could attempt having multiple functions which get different 'parts', just as our posixToDate function got the parts we needed. Note though that that was still insufficiently lazy, and the number of different functions is combinatorial, though in this particular case you could probably guess at a few common ones, such as 'date', or 'time of day'.

Of course it could return a lambda for each field of the record, but then you might evaluate that lambda more than once. In any case if it did this it would be re-implementing laziness.

The main point I'm making here is not that laziness is more efficient, it's that it frees you from having to consider unnecessary work and as such can lead to simplier code structure, that is easier to maintain.