2

I'm interested in a higher-order way (recursion scheme) to write recursive code in which there might be dependencies between recursive calls.

As a simplified example, consider a function that traverses a tree of integers, checking if the sum is less than some value. We could sum the entire tree and compare it against the maximum. Alternatively, we could keep a running sum and short-circuit as soon as we've exceeded the maximum:

data Tree = Leaf Nat | Node Tree Tree

sumLT :: Nat -> Tree -> Bool
sumLT max t = sumLT' max t > 0

sumLT' :: Nat -> Tree -> Int
sumLT' max (Leaf n) = max - n
sumLT' max (Node l r) = 
  let max' = sumLT' max l
   in if max' > 0 then sumLT' max' r else 0

Is there a way to express this idea -- essentially an ordered traversal -- with a recursion scheme? I'm interested in as-generically-as-possible composing ordered traversals like this.

Ideally, I'd like some way to write traversals in which the function being folded (or unfolded) over the data structure determines the order of traversal. Whatever abstraction I end up with, I'd like to be able to also write the reverse-ordered version of the sumLT' traversal above, where we go right-to-left instead:

sumLT'' :: Nat -> Tree -> Int
sumLT'' max (Leaf n) = max - n
sumLT'' max (Node l r) = 
  let max' = sumLT'' max r
   in if max' > 0 then sumLT'' max' l else 0
trpnd
  • 455
  • 2
  • 10
  • 1
    You don't have a tree of integers, you have a tree with no values in it. Therefore, `sumLT'` won't compile. Please make sure your example code actually compiles, unless your question is about why the code doesn't compile. – amalloy Jan 29 '21 at 18:42
  • `Int`s can be negative. Did you mean to use `Natural`? Or to assume a small enough number of sufficiently non-negative `Int`s that they won't wrap around? – dfeuer Jan 29 '21 at 19:08
  • 1
    Another good catch. Let's make em `Nat`s and pretend we have a theorem prover or something. The example is (clearly poorly thought-out) and sort of besides the point -- I was just trying to think of a simple example of a recursive traversal in which there's a dependency between recursive calls. – trpnd Jan 29 '21 at 19:11
  • I suspect you'll be better off using something like `Foldable` or `MFoldable` for this sort of thing, since those bake in a notion of order. `foldr` is certainly up to the task. – dfeuer Jan 29 '21 at 19:13
  • What if I don't want a hard-coded notion of order? `Foldable` is the only option I have so far -- mostly curious if recursion schemes (or something else) provides more general options. So far I suspect that there might not be anything more general since the whole point of recursion-schemes and whatnot is to abstract over the recursive structure, but I'm curious if there's something more general hiding behind all the category theory. – trpnd Jan 29 '21 at 19:18
  • Don't know if it counts as a recursion scheme you could do `any` of a `scanl`. `scanl` isn't actually on Traversable but it's basically a special case of mapAccumL which is. In general if you want to inspect intermediate results you could work off a scan. – David Fletcher Jan 29 '21 at 19:19
  • Hmm `scan` seems to still traverse in whatever order the Foldable instance determines, no? Ideally I'd like the algebra (function being folded) itself to somehow determine the traversal order – trpnd Jan 29 '21 at 19:26

2 Answers2

2

As usual, folding into endofunctions gives you a notion of processing order / state passing:

import Numeric.Natural

data Tree = Leaf Natural | Node Tree Tree

cata :: (Natural -> r) -> (r -> r -> r) -> Tree -> r
cata l n (Leaf a)     = l a
cata l n (Node lt rt) = n (cata l n lt) (cata l n rt)

sumLT :: Natural -> Tree -> Bool
sumLT max t = cata (\ a max -> max - a)     -- left-to-right
                   (\ l r max -> let max' = l max in
                        if max' > 0 then r max' else 0)
                   t max > 0

sumLT' :: Natural -> Tree -> Bool
sumLT' max t = cata (\ a max -> max - a)     -- right-to-left
                    (\ l r max -> let max' = r max in
                         if max' > 0 then l max' else 0)
                    t max > 0

Trying it out:

> sumLT 11 (Node (Leaf 10) (Leaf 0))
True

> sumLT 11 (Node (Leaf 10) (Leaf 1))
False

> sumLT 11 (Node (Leaf 10) (Leaf undefined))
*** Exception: Prelude.undefined

> sumLT 11 (Node (Leaf 11) (Leaf undefined))
False

> sumLT 11 (Node (Leaf 10) (Node (Leaf 1) (Leaf undefined)))
False

> sumLT' 11 (Node (Leaf undefined) (Leaf 11))
False

As is also usual, Haskell's laziness provides for the ability to short-circuit / exit early. As can be seen in the examples, if cata's second argument, the node-folding function, does not demand the value of one of its arguments, the corresponding branch is not actually visited at all.

Will Ness
  • 70,110
  • 9
  • 98
  • 181
  • Cool! I think this might be just what I wanted -- my actual use case is a little more complicated than my example (which might have been a mistake in terms of getting useful answers), but I should be able to use this idea! – trpnd Jan 30 '21 at 22:31
0

I would use Haskell's laziness to my advantage. Turn the tree to list (that's a catamorphism), create partial sums, and find the first that's larger than the limit.

{-# language DeriveFoldable #-}

module ShortSum where
import Data.Foldable
  
data Tree a = Leaf a | Node (Tree a) (Tree a)
  deriving Foldable

type Nat   = Int
type TreeN = Tree Nat

sumLt :: Nat -> TreeN -> Bool
sumLt mx = any (> mx) . scanl1 (+) . toList
Bartosz Milewski
  • 11,012
  • 5
  • 36
  • 45