/transform

[Elm] Transform recursive data structures from the bottom up

Primary LanguageElmBSD 3-Clause "New" or "Revised" LicenseBSD-3-Clause

Transform

tl;dr: Transform your data structures recursively from the bottom up. Very useful eg. for writing compiler passes.

Inspired by various tweets and my use for something like this in elm-in-elm.


Let's say you're writing a compiler and have this AST data structure:

type Expr
    = Int_ Int
    | Negate Expr
    | Plus Expr Expr
    | List_ (List Expr)

You also have a few optimizations your compiler can do:

simplifyDoubleNegate : Expr -> Expr
simplifyDoubleNegate expr =
    case expr of
        Negate (Negate (Int_ int)) ->
            Int_ int

        _ ->
            expr


simplifyNegate : Expr -> Expr
simplifyNegate expr =
    case expr of
        Negate (Int_ int) ->
            Int_ (negate int)

        _ ->
            expr


simplifyPlus : Expr -> Expr
simplifyPlus expr =
    case expr of
        Plus (Int_ a) (Int_ b) ->
            Int_ (a + b)

        _ ->
            expr

And let's say we've got this AST:

ast : Expr
ast =
  List_
    [ Negate
            (Plus
                (Int_ 1)
                (Negate (Int_ 8))
            )
        , List_
            [ Negate
                (Int_ 8)
            , Plus
                (Int_ 1)
                (Negate
                    (Negate
                        (Int_ 8)
                    )
                )
            ]
        ]

Now, the burning question is, how do we run these recursively on the whole AST? How do we make sure there are no gains to be had if we run one more optimization somewhere?

This package has just what you need. Watch!


  • First, we'll say how and which children we want to recurse to.
  • Then, we'll combine our transformations in an (almost) order-independent way. (Well, it will be much better our way than by just composing them.)
  • Then, as the last step, we'll run the transformations as long as they actually change the data structure.
{-| FIRST: say how to recurse!
-}
recurse : (Expr -> Expr) -> Expr -> Expr
recurse fn expr =
    case expr of
        Int_ int ->
            Int_ int

        Negate e ->
            Negate (fn e)

        Plus left right ->
            Plus (fn left) (fn right)

        List_ es ->
            List_ (List.map fn es)
{-| SECOND: combine the transformations!
-}
simplifyAll : Expr -> Maybe Expr
simplifyAll =
    Transform.orList_
        [ simplifyDoubleNegate
        , simplifyNegate
        , simplifyPlus
        ]
{-| THIRD: run the transformations until nothing changes anymore!
-}
simplifiedAst : Expr
simplifiedAst =
    Transform.transformAll
        recurse
        simplifyAll
        ast

The result?

simplifiedAst
--> List_ [Int_ 7, List_ [Int_ -8, Int_ 9]]

(By the way, the functions here are general enough to work on anything, not just on custom types. Feel free to use it on records, for example!)


Another thing this library can do is use a similar recursion helper to get all the descendants (children, their children, etc.).

recursiveChildren : (Expr -> List Expr) -> Expr -> List Expr
recursiveChildren fn expr =
    case expr of
        Int_ int ->
            []

        Negate e ->
            fn e

        Plus left right ->
            fn left ++ fn right

        List_ es ->
            List.concatMap fn es


children
    recursiveChildren
    (Plus (Int_ 1) (Negate (Int_ 2)))
-->
    [ Plus (Int_ 1) (Negate (Int_ -2))
    , Int_ 1
    , Negate (Int_ 2)
    , Int_ 2
    ]

As you can see, the result of calling children is all the descendants of the original expression, in the depth order.


Conceptually, this is a spiritual equivalent of Control.Lens.Plated from Haskell.

Because we don't have auto-derived lenses, it needs you to give it that recurse function.

In the future, there are other possible functions to add, like getting the immediate children or all the descendants. Let me know if those would be useful for you!