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.