More compile time laziness

· Allanderek's blog


Following on from yesterday I think there are various other ways in which a compile-time elm-to-elm optimiser could improve the performance of Javascript code output by the Elm compiler. It goes without saying that this does not necessarily have to be done as a separate tool, it could easily be incorporated into the Elm compiler itself. It is just potentially a lower barrier for entry to do it the separate tool way. It's also a little easier to describe.

I've taken to calling this compile-time laziness. You cannot quite get to the same level as a language that is truly lazy (such as Haskell). However, I view this as being something of an analogous phase as a strictness analyser for a lazy language. A strictness analser removes some of the inefficiencies of creating 'thunks' by determining that certain values are co-lazy. That is, x and y are co-lazy, if evalauating x implies that you will evaluate y. You can use this to avoid creating a needless thunk for y. Compile-time laziness in a sense attempts to reach the same kind of optimum as a strictness analyser, but instead of starting with all values being lazily evalated and determing that some can be made strict, it starts off with all values being strictly evaluated and determines that some can be lazily evaluated. Rather than being lazily evaluted it can instead be moved. Here is an example, consider the following code:

 1showCouponCode : Coupon -> Discounts -> Html msg
 2showCouponCode coupon discounts =
 3    let
 4        couponIsEmpty =
 5            String.isEmpty coupon
 6        couponIsUsed =
 7            List.any (\discount -> discount.code == coupon) discounts
 8    in
 9    case couponIsEmpty of
10        True ->
11            Html.nothing
12        False ->
13            case couponIsUsed of
14                True ->
15                    Html.div
16                        [ Attributes.class "used-coupon-code" ]
17                        [ Html.text coupon
18                        , Icon.tick
19                        ]
20                False ->
21                    Html.div
22                        [ Attributes.class "unused-coupon-code" ]
23                        [ Html.text coupon
24                        , Icon.cross
25                        ]
26

The insight here is that whilst couponIsEmpty is always evaluated, couponIsUsed is ignored if couponIsEmpty == True. So we could avoid evaluating the expression List.any ..., however, because Elm is a stricly evaluated language this will be evaluated in any case. A compile-time laziness anaylser, which was written as an elm-to-elm transformer, could transform the above code to be:

 1showCouponCode : Coupon -> Discounts -> Html msg
 2showCouponCode coupon discounts =
 3    let
 4        couponIsEmpty =
 5            String.isEmpty coupon
 6    in
 7    case couponIsEmpty of
 8        True ->
 9            Html.nothing
10        False ->
11            let
12                couponIsUsed =
13                    List.any (\discount -> discount.code == coupon) discounts
14            in
15            case couponIsUsed of
16                True ->
17                    Html.div
18                        [ Attributes.class "used-coupon-code" ]
19                        [ Html.text coupon
20                        , Icon.tick
21                        ]
22                False ->
23                    Html.div
24                        [ Attributes.class "unused-coupon-code" ]
25                        [ Html.text coupon
26                        , Icon.cross
27                        ]
28

In this version of the code, couponIsUsed is only evaluated if actually used. It turns out in this case I also think this is actually nicer code. I believe that definitions closer to their use-cases are less likely to be in error, and I also believe it makes sense to limit the scope of declared names to where they are needed. So this highlights another reason it might be useful to write this optimiser as an elm-to-elm transformation. Sometimes the analyser will produce less readable code, and the transformed code should just be handed to the Elm compiler and then thrown away, but sometimes it will produce (in some sense) better code, and it can instead be kept.