Bringing Functions into the Fold

Jasper Van der Jeugt Steven Keuchel Tom Schrijvers Department of Applied Mathematics, Computer Science and Statistics Ghent University, Krijgslaan 281, 9000 Gent, Belgium {jasper.vanderjeugt,steven.keuchel,tom.schrijvers}@ugent.be

Abstract sum :: [Int ] → Int Programs written in terms of higher-order recursion schemes like sum xs = foldr (+) 0 xs foldr and build can benefit from program optimizations like short- Gibbons [6] has likened the transition from the former style to cut fusion. Unfortunately, programmers often avoid these schemes the latter with the transition from imperative programming with in favor of explicitly recursive functions. gotos to structured programming. Indeed the use of recursion This paper shows how programmers can continue to write pro- schemes has many of the same benefits for program construction grams in their preferred explicitly recursive style and still bene- and program understanding. fit from fusion. It presents syntactic algorithms to automatically One benefit that has received a lot of attention in the Haskell find and expose instances of foldr and build. The algorithms have community is program optimization. Various equational laws have been implemented as GHC compiler passes and deal with all reg- been devised to fuse the composition of two recursion schemes ular algebraic datatypes, not just lists. A case study demonstrates into a single recursive function. Perhaps the best-known of these that these passes are effective at finding many instances in popular is shortcut fusion, also known as foldr/build fusion [7]. Haskell packages. ∀c n g.foldr c n (build g) = g c n Keywords catamorphisms, fold/build fusion, analysis, transfor- mation This law fuses a list producer, expressed in terms of build, with a list consumer, expressed in terms of foldr in order to avoid the construction of the allocation of the intermediate list; the latter is 1. Introduction called deforestation [22]. Higher-order functions are immensely popular in Haskell, whose A significant weakness of foldr/build fusion and other fusion Prelude alone offers a wide range of them (e.g., map, filter, any, approaches is that programmers have to explicitly write their pro- . . . ). This is not surprising, because they are the key abstraction grams in terms of the appropriate recursion schemes. This is an mechanism of languages. They enable easy requirement when programmers only reuse library functions capturing and reusing common frequent patterns in function def- written in the appropriate style. However, when it comes to writ- initions. ing their own functions, programmers usually resort to explicit re- Recursion schemes are a particularly important class of patterns cursion. This not just true of novices, but applies just as well to captured by higher-order functions. They capture forms of recur- experienced Haskell programmers, including, as we will show, the sion found in explicitly recursive functions and encapsulate them authors of some of the most popular Haskell packages. in reusable abstractions. In Haskell, the foldr function is probably Hence, already in their ’93 work on fold/build fusion, Gill et the best known example of a recursion scheme. It captures struc- al. [7] have put forward an important challenge for the compiler: tural recursion over a list. to automatically detect recursion schemes in explicitly recursive functions. This allows programmers to have their cake and eat it foldr :: (a → b → b) → b → [a ] → b too: they can write functions in their preferred style and still benefit foldr f z [ ] = z from fusion. foldr f z (x : xs) = f x (foldr f z xs) As far as we know, this work is the first to pick up that challenge By means of a recursion scheme like foldr, the programmer can and automatically detect recursion schemes in a Haskell compiler. avoid the use of explicit recursion in his functions, like Our contributions are in particular: sum :: [Int ] → Int 1. We show how to automatically identify and transform explicitly sum [ ] = 0 recursive functions that can be expressed as folds. sum (x : xs) = x + sum xs 2. In order to support a particular well-known optimization, fold- in favor of implicit recursion in terms of the recursion scheme: build fusion, we also show how to automatically detect and transform functions that can be expressed as a call to build. 3. We provide a GHC compiler plugin1 that performs these detec- tions and transformations on GHC Core. Our plugin not only covers folds and builds over lists, but over all inductively de- fined directly-recursive datatypes. 4. A case study shows that our plugin is effective on a range of well-known Haskell packages. It identifies a substantial num-

[Copyright notice will appear here once ’preprint’ option is removed.] 1 http://github.ugent.be/javdrjeu/what-morphism

1 2013/6/14 ber of explicitly recursive functions that fit either the fold or where the acc parameter varies in the recursive call. Typically, such build pattern, reveals that our approach works well with GHC’s a function is defined in terms of the foldl variant of foldr, which existing list fusion infrastructure. folds from the left rather than the right. foldl :: (a → b → a) → a → [b ] → a 2. Overview foldl f z [ ] = z This section briefly reviews folds, builds and fusion to prepare for foldl f z (x : xs) = foldl f (f z x) xs the next sections, that provide systematic algorithms for finding sumAcc l acc = foldl (+) acc l folds and builds in explicitly recursive functions. However, these functions too can be expressed in terms of foldr. 2.1 Folds The trick is to see such a function not as having an extra parameter Catamorphisms are functions that consume an inductively defined but as returning a function that takes a parameter. For instance, datastructure by means of structural recursion. Here are two exam- sumAcc should not be considered as a function that takes a list and ples of catamorphisms over the most ubiquitous inductive datatype, an integer to an integer, but a function that takes a list to a function lists. that takes an integer to an integer. This becomes more apparent when we make the precedence of the function arrow in the type upper :: String → String signature explicit, as well as the binders for the extra parameter. upper [ ] = [ ] sumAcc :: [Int ] → (Int → Int) upper (x : xs) = toUpper x : upper xs sumAcc [ ] = λacc → acc product :: [Int ] → Int sumAcc (x : xs) = λacc → sumAcc xs (x + acc) product [ ] = 1 product (x : xs) = x ∗ product xs Now we have an obvious catamorphism without extra parameter that can be turned trivially into a fold. Instead of using explicit recursion again and again, the catamor- phic pattern can be captured once and for all in a higher-order func- sumAcc l = foldr (λx xs acc → xs (x + acc)) tion, the fold function. In case of list, that fold function is foldr. (λacc → acc) l upper = foldr (λx xs → toUpper x : xs)[] As a last step we may want to η-expand the above definition. product = foldr (∗) 1 sumAcc l acc = foldr (λx xs acc → xs (x + acc)) (λacc → acc) l acc Folds with Parameters Some catamorphisms take extra parame- foldl ters to compute their result. Hinze et al. [9] distinguish two kinds Finally, is an example of a function with both a constant f z of extra parameters: constant and accumulating parameters. List parameter and an accumulating parameter . It is expressed as a foldr concatenation is an example of the former: thus: cat :: [a ] → [a ] → [a ] foldl f z l = foldr (λx xs z → xs (f x z)) (λz → z) l z cat [] ys = ys Note that as a minor complication both extra parameters precede cat (x : xs) ys = x : cat xs ys the list parameter. The ys parameter is a constant parameter because it does not Other Datatypes The above ideas for folds of lists easily gener- change in the recursive calls. We can get rid of this constant pa- alize to other inductively defined algebraic datatypes. We illustrate rameter by hoisting it out of the loop. that on leaf trees. cat :: [a ] → [a ] → [a ] data Tree a cat l ys = loop l = Leaf a where | Branch (Tree a)(Tree a) loop :: [a ] → [a ] loop [ ] = ys The sumTree function is an example of a directly recursive loop (x : xs) = x : loop xs catamorphism over leaf trees. Now, the local function can be rewritten in terms of a foldr without sumTree :: Tree Int → Int having to bother with passing the constant parameter. sumTree (Leaf x) = x sumTree (Branch l r) = sumTree l + sumTree r cat xs ys = loop l where This catamorphic recursion scheme can be captured in a fold func- loop l = foldr (:) ys l tion too. The generic idea is that a fold function transforms the in- ductive datatype into a value of a user-specified type r by replacing Finally, we can inline the local function entirely. every constructor with a user-specified function. cat xs ys = foldr (:) ys l foldT :: (a → r) The intermediate steps can of course be skipped in practice and our → (r → r → r) algorithm in Section 3 does so. → Tree a Accumulating parameters are trickier to deal with as they may → r vary in recursive calls. An example is the accumulator-based sum foldT l (Leaf x) = l x function: foldT l b (Branch x y) = sumAcc :: [Int ] → Int → Int b (foldT l b x)(foldT l b y) sumAcc [] acc = acc The fold over leaf trees enables us to define sumTree succinctly sumAcc (x : xs) acc = sumAcc xs (x + acc) as

2 2013/6/14 sumTree = foldT id (+) 2.3 Fold/Build Fusion Leaf trees also have a counterpart of left folds for lists, where The foldr/build fusion rule expresses that a consumer and producer the pattern is called downward accumulation. For instance, the can be fused: following function from [9] labels every leaf with its depth: foldr cons nil (build g) ≡ g cons nil depths :: Tree a → Int → Tree Int Consider the following expressions: depths (Leaf x) d = Leaf d depths (Branch l r) d = Branch (depths l (d + 1)) sum (replicate n x) (depths r (d + 1)) which consumes a list with sum after generating it with replicate. This catamorphism is rewritten into a fold in a similar way as left With the help of the fusion rule and simple transformations, we can folds. optimize it as follows. depths t d = sum (replicate n x) foldT (λd → Leaf d) ≡ [[ inline sum ]] (λl r d → Branch (l (d + 1)) (r (d + 1))) t d foldr (+) 0 (replicate n x) ≡ [[ inline replicate ]] 2.2 Builds foldr (+) 0 (build $ λcons nil → Builds are the opposite of folds: they produce datastructures. For let g n x lists, this production pattern is captured in the function build. | n 6 0 = nil build :: (∀b.(a → b → b) → b → b) → [a ] | otherwise = cons x (g (n − 1) x) build g = g (:) [ ] in g n x) ≡ [[ foldr/build fusion ]] where g is a function that builds an abstract list in terms of the provided ‘cons’ and ‘nil’ functions. The build function constructs (λcons nil → a concrete list by applying g to the conventional (:) and [] list let g n x constructors. | n 6 0 = nil Consider for instance the function replicate. | otherwise = cons x (g (n − 1) x) in g n x) (+) 0 replicate :: Int → a → [a ] replicate n x ≡ [[ β-reduction ]] | n 6 0 = [ ] let g n x | otherwise = x : replicate (n − 1) x | n 6 0 = 0 | otherwise = x + g (n − 1) x which can be expressed in terms of build as follows: in g n x replicate n x = build (λcons nil → Although arguable less readable, we can see that the final version let g n x of sum (replicate n x) does not create an intermediate list. | n 6 0 = nil | otherwise = cons x (g (n − 1) x) Pipelines Haskell best practices encourage building complicated in g n x) functions by producing, repeatedly transforming and finally con- suming an intermediate datastructure in a pipeline. An example of Other Datatypes The notion of a build function also generalizes. such a pipeline is the following sum-of-odd-squares function: For instance, this is the build function for leaf trees: sumOfSquaredOdds :: Int → Int buildT :: (∀b.(a → b) → (b → b → b) → b) sumOfSquaredOdds = → Tree a sum ◦ map (ˆ2) ◦ filter odd ◦ enumFromTo 1 buildT g = g Leaf Branch This code is quite compact and easy to compose from exist- With this build function we can express a producer of leaf trees ing library functions. However, when compiled naively, it is also rather inefficient because it performs four loops and allocates three range :: Int → Int → Tree a intermediate lists (the results of respectively enumFromTo 1, range l u filter odd and map (ˆ2)) that are immediately consumed again. | u > l = let m = l + div (u − l) 2 With the help of fusion, whole pipelines like sumOfSquaredOdds in Branch (range l m)(range (m + 1) u) can be fused into one recursive function: | otherwise = Leaf l sumOfSquaredOdds :: Int → Int as a build: sumOfSquaredOdds n = go 1 where range l u = go :: Int → Int buildT (λleaf branch → go x let g l u | x > n = 0 | u > l = let m = l + div (u − l) 2 | odd x = x ˆ 2 + go (x + 1) in branch (g l m)(g (m + 1) u) | otherwise = go (x + 1) | otherwise = leaf l in g l u) which performs only a single loop and allocates no intermediate lists.

3 2013/6/14 The key to pipeline optimization is the fact that transformation 1. GHC does not explicitly cater for other datatypes than lists. functions like map and filter can be expressed as a build whose While the programmer can replicate the existing list infrastruc- generator function is a fold. ture for his own datatypes, it requires a considerable effort and rarely happens in practice. map :: (a → b) → [a ] → [b ] map f l = build (λcons nil → foldr (cons ◦ f ) nil l) 2. Programmers or libraries need to explicitly define their func- tions in terms of foldr and build. If they don’t, then fusion is filter :: (a → Bool) → [a ] → [a ] not possible. This too turns out to be too much to ask as we see filter p l = many uses of explicit recursion in practice (see Section 6). build (λcons nil → foldr (λx → if p x then cons x else id) nil l) This work addresses both limitations. It allows programmers to write their functions in explicitly recursive style and performs This means they can fuse with both a consumer on the left and a fold/build fusion for any directly inductive datatype. producer on the right. Other Datatypes Any datatype that provides both a fold and build 3. Finding Folds function, has a corresponding fusion law. For leaf trees, this law is: This section explains our approach to turning explicitly recursive foldT leaf branch (buildT g) ≡ g leaf branch functions into invocations of fold. It is key to fusing two loops into one: 3.1 Syntax and Notation sumTree (range l u) To simplify the presentation, we do not explain our approach in ≡ [[ inline sumTree ]] terms of Haskell source syntax or even GHC’s core syntax (based foldT id (+) (range l u) on System F). Instead, we use the untyped lambda-calculus ex- ≡ [[ inline range ]] tended with constructors and pattern matching, and (possibly re- cursive) bindings. foldT id (+) (buildT $ λleaf branch → let g l u binding b ::= x = e | u > l = let m = l + div (u − l) 2 pattern p ::= K x in branch (g l m)(g (m + 1) u) expression e ::= x | otherwise = leaf l | e e | λx → e in g l u) | K ≡ [[ fold/build fusion ]] | case e of p → e (λleaf branch → The extension to GHC’s full core syntax, including types, is rela- let g l u tively straightforward. | u > l = let m = l + div (u − l) 2 We will also need an advanced form of (expression) context: in branch (g l m)(g (m + 1) u) | otherwise = leaf l E ::= x in g l u) id (+) | E x | E  ≡ [[ β-reduction ]] | E 4 let g l u A context E denotes a function applied to a number of arguments. | u > l = let m = l + div (u − l) 2 The function itself and some of its arguments are given (as vari- in g l m + g (m + 1) u ables), while there are holes for the other arguments. In fact, there | otherwise = l are two kinds of holes: boxes  and triangles 4. The former is used in g l u for a sequence of extra parameters, while the latter marks the main argument on which structural recursion is performed. The function 2.4 Automation E[e; e] turns a context E into an expression by filling in the holes The main Haskell compiler, GHC, automates fold/build fusion by with the given expressions. means of its rewrite rules infrastructure [11]. The fusion law is x[; e] = x expressed as a rewrite rule in the GHC.Base module (E x)[e; e] = E[e; e] x {-# RULES (E )[e, e1; e] = E[e; e] e1 "fold/build" (E 4)[e; e] = E[e; e] e forall k z (g::forall b. (a->b->b) -> b -> b) . Note that this function is partial; it is undefined if the number of foldr k z (build g) = g k z expressions e does not match the number of box holes. #-} which is applied whenever the optimizer encounters the pattern on 3.2 Finding Folds the left. In addition, various library functions have been written2 Figure 1 shows our non-deterministic algorithm for rewriting func- in terms of foldr and build. Whenever the programmer uses these tion bindings in terms of folds. To keep the exposition simple, the functions and combines them with his own uses of foldr and build, algorithm is specialized to folds over lists; we discuss the general- fusion may kick in. ization to other algebraic datatypes later on. Unfortunately, GHC’s infrastructure shows two main weak- nesses: Single-Argument Functions The top-level judgement is of the 0 form b b , which denotes the rewriting of a function binding b 2 or come with rewrite rules that rewrite them into those forms to b0. The judgement is defined by a single rule (F-Bind), but we

4 2013/6/14 0 0 e1 = [y 7→ [ ]]e1 f 6∈ fv(e1) E[u; y] = f x y z ws fresh E 0 0 e2 vs ws e2 {f, x, vs} ∩ fv(e2) = ∅ (F-BIND) f = λx y z → case y of {[] → e1;(v : vs) → e2 } 0 0 0 b b f = λx y z → foldr (λv ws u → e2)(λu → e1) y u

E 0 ei x y ei (∀i) (F-REC) (F-REFL) E 0 E 0 E e x y e E[e; x] x y y e e x y e

E 0 E 0 E 0 e x y e e1 x y e1 e2 x y e2 (F-ABS) (F-APP) E 0 E 0 0 λz → e x y λz → e e1 e2 x y e1 e2

E 0 E 0 e x y e ei x y ei (∀i) (F-CASE) E 0 0 case e of p → e x y case e of p → e

Figure 1. Fold discovery rules

first explain a specialized rule for single argument functions: where the higher-order pattern of paramorphisms is 0 e1 = [y 7→ [ ]]e1 f 6∈ fv(e1) ws fresh para :: (a → [a ] → b → b) → b → [a ] → b f 4 0 0 para f z [ ] = z e2 vs ws e2 {f, x, vs} ∩ fv(e2) = ∅ (F-BIND’) para f z (x : xs) = f x xs (para f z xs) f = λy → case y of {[] → e1;(v : vs) → e2 } 0 0 f = λy → foldr (λv ws → e2) e1 y 2. If f appears in any other form than as part of recursive calls This rule rewrites a binding like of the form f vs, then again the function is not a proper catamorphism. An example of that case is the following non- sum = λy → case y of terminating function: [] → 0 (v : vs) → (+) v (sum vs) f = λx → case x of [] → 0 into (v : vs) → v + f vs + f [1, 2, 3] sum = λy → foldr (λv ws → (+) v ws) 0 y mostly by simple pattern matching and replacement. The main Note that we know in the branches e1 and e2 the variable x is an alias for respectively [] or (y : ys). Rule (F-BIND’) exploits the work is to replace all recursive calls in e2, which is handled by 0 the auxiliary judgement of the form former case to eliminate x in e1. The latter case reveals an improper catamorphism, where ys appears outside of a recursive call (issue E 0 e x y e 2 above); hence, rule (F-BIND’) does not allow it. which is defined by five rewriting rules: rule (F-REC) takes care of Folds with Parameters Rule (F-BIND) generalizes rule (F- the actual rewriting of recursive calls, while rule F-REFL provides BIND’) by supporting additional formal parameters x and z before the reflexive closure and the other three rules provide congruence and after the scrutinee argument y. The algorithm supports both closure. In the restricted setting of single argument functions rule constant and accumulating parameters. Rule (F-BIND) does not (F-Rec) takes the simplified form explicitly name the constant parameters, but captures them instead (F-REC’) in the context E for recursive calls. For