Question

I'm attempting to implement a fold over a discriminated union. The DU is called Expr, and represents a program expression, and is often recursive. I'm attempting to write a fold that accumulates the result of an operation over the Exprs recursively. Below is my attempt to write the fold.

let rec foldProceduralExpr (folder : 's -> Expr list -> 's) (state : 's) (expr : Expr) : 's =
    let children =
        match expr with
        | Series s -> s.SerExprs
        | Lambda l -> [l.LamBody; l.LamPre; l.LamPost]
        | Attempt a -> a.AttemptBody :: List.map (fun ab -> ab.ABBody) a.AttemptBranches
        | Let l -> l.LetBody :: List.concat (List.map (fun lb -> match lb with LetVariable (_, expr) -> [expr] | LetFunction (_, _, body, _, pre, post, _) -> [body; pre; post]) l.LetBindings)
        | Case c -> c.CaseTarget :: List.concat (List.map (fun branch -> [branch.TBBody; branch.TBTest]) c.CaseBranches)
        | Condition c -> List.concat (List.map (fun branch -> [branch.TBBody; branch.TBTest]) c.CondBranches)
        | List l -> l.ListElements
        | Array a -> Array.toList a.ArrElements
        | Composite c -> LunTrie.toValueList (LunTrie.map (fun _ mem -> mem.MemExpr) c.CompMembers)
        | _ -> []
    let listFolder = fun state expr -> foldProceduralExpr folder state expr
    let listFolded = List.fold listFolder state children
    folder state (expr :: listFolded)

The problem is that the code doesn't work, and this is known because I get the error the construct causes code to be less generic than indicated by the type annotations. The type variable 's has been constrained to be type 'Expr list' on listFolded on the last line. This is because the definition of foldProceduralExpr is almost certainly wrong.

Now, I'd love to fix the code and consequently the type error, but I simply don't know how. I suppose what I lack is an understanding in how folds over non-list or recursive data structures work. I recently translated a fold for a trie from OCaml to F#, and had a lot of trouble understanding how that fold worked as well.

Question 1: Is there a visible fix for this code that I can just understand slap in?

Question 2: Is there a resource for gaining the background needed to understand how to write these type of folds?

Please let me know if you need more information. I've not included the DU as it's too big and complicated to keep the question clear. Hopefully suffice to say, it's your typical Lisp-style AST data structure.

Follow up question: I'd also like to write a map with that fold once it works. Would that look like a typical map, or would that take some extra brain-power to figure out too?

EDIT: With regards to Tomas' help, I've turned the last three lines into -

let exprs = expr :: children
let listFolder = fun state expr -> foldProceduralExpr folder state expr
let listFolded = List.fold listFolder state exprs
folder listFolded exprs

I hope this still makes sense.

EDIT: Here's my final solution. It's generalized over get children -

let rec foldRdu (getChildren : 't -> 't list) (folder : 's -> 't -> 't list -> 's) (state : 's) (parent : 't) : 's =
    let children = getChildren parent
    let listFolder = fun state child -> foldRdu getChildren folder state child
    let listFolded = List.fold listFolder state children
    folder listFolded parent children

The reason I'm taking an Expr and Expr list as the fold value it that I want to preserve the structure of the parent / children relationship to use later. This is a very domain-specific fold, in a way I suppose.

Was it helpful?

Solution

The first question is, what kind of fold do you want to implement? There is a number of options for tree-like structures. Assuming you have an n-ary tree (that is, any number of children), you can:

  1. Apply the folding function only on Expr nodes in the leafs
  2. Apply the folding function on the leafs and then apply it to all internal nodes (after a recursive call)
  3. Apply the folding function on the internal nodes while processing the tree and then on leafs

In your example, your folder function takes Expr list which is a bit confusing to me - I would think you could just give it the current Expr and not a list of children, but calling it on children is an option too.

You're first making a recursive call and then calling the folder on current expression, so I suppose you wanted to implement the option 2. I can understand the first two lines of your folding:

// This defines a function that recursively folds the a given 
// expression and produces a new state of type 's
let listFolder = fun state expr -> foldProceduralExpr folder state expr 
// Here, you fold all children recursively starting with 'state' and 
// obtaining a result 'listFolded' of type 's
let listFolded = List.fold listFolder state children 

However, the last line is probably wrong:

// From the previous line, 'listFolded' is of type 's, but 
// here you use it as if it was a list:
folder state (expr :: listFolded) 

I assume you probably want to use listFolded as the state passed to the folder. The folder takes a list of expressions, so you can give it children as the second argument (or you can change folder to take just a single Expr and then give it just expr, which would IMHO make more sense). Anyway, this should make it work, so that you can run it and see what is going on:

folder listFolded children

OTHER TIPS

I wrote an article once about a practical approach to maintaining code involving large discriminated unions and their transforms:

http://t0yv0.blogspot.com/2011/09/transforming-large-unions-in-f.html

In practice I find that fold assumes too much, there are frequently transformations that cannot be expressed in terms of fold. You also want control over how the transformation is going - bottom-up or top-down, and which nodes are skipped or handled specially. The article shows how to do this in terms of a simple transform function. I also derive a fold in just a few lines from transform.

It does not directly answer how to fix your problem, but it might shed some light on another way to structure such code.

Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top