2
\$\begingroup\$

A while back, I answered this question on Stack Overflow that involved deserializing a binary tree breadth-first using functional programming (the question itself isn't relevant). I'd like to make sure that I'm following functional programming principles and whether or not this is stack-safe. I'd also like to know if I can make my code more performant, less bloated, clearer, etc. If it helps, the question provides this link to explain the algorithm.

I first defined this rather simple tree type. Every child is an instance of Node, and the absence of a left or right child is denoted with Empty.

sealed trait Tree[+T]
case class Node[+T](data: T, left: Tree[T], right: Tree[T]) extends Tree[T]
case object Empty extends Tree[Nothing]

And this trait that represents a function that takes the current list of serialized items (every item is an Option, with None meaning there is no child). It consumes one or more of these items and returns the rest, along with an Either, where a Right contains a fully built tree, and a Left contains another RecFun that will later consume more items to finish building the current tree.

trait RecFun[T]
    extends (List[Option[T]] => (List[Option[T]], Either[RecFun[T], Tree[T]]))

The entry point is makeTree. First, a RecFun is made using createRec below, and the recursive helper inside makeTree starts processing the tree from there.

def makeTree[T](list: List[Option[T]]): Tree[T] = {
  def helper(f: RecFun[T], l: List[Option[T]]): Tree[T] =
    f(l) match {
      case (_, Right(tree)) => tree
      case (next, Left(f))  => helper(f, next)
    }

  list match {
    case Some(x) :: tail => helper(createRec(x), tail)
    case _               => Empty
  }
}

createRec takes the value of the root node of a subtree, and makes a RecFun to continue building it.

def createRec[T](data: T): RecFun[T] = {
  //No more children, so return a Right with a node whose children are Emptys
  case None :: Nil | Nil => (Nil, Right(Node(data, Empty, Empty)))
 //There's a left child, but no right child to be had, so return here too
  case Some(l) :: Nil => (Nil, Right(Node(data, Node(l, Empty, Empty), Empty)))
  case lo :: ro :: rest => //Possible left child :: possible right child :: rest of the items
    //Return the rest, and an Either depending on what lo and ro are
    (rest, (lo, ro) match {
      case (Some(l), Some(r)) =>
        //Both children exist, so wait for both of them before building the tree
        Left(waitForChildren(data, createRec(l), createRec(r)))
      case (Some(l), None) =>
        //Need to wait only for left child
        Left(waitForChild(Node(data, _, Empty), createRec(l)))
      case (None, Some(r)) =>
        //Need to wait only for right child
        Left(waitForChild(Node(data, Empty, _), createRec(r)))
      //Both children don't exist, so build tree and return right now
      case (None, None) => Right(Node(data, Empty, Empty))
    })
  }

This helper simply combines functions that build a tree where the root's value is known, but the children are not.

//Here, both the children need to be deserialized and have their own RecFuns
def waitForChildren[T](data: T, leftF: RecFun[T], rightF: RecFun[T]): RecFun[T] =
  input => {
    //Attempt to deserialize the left child first
    val (next, res) = leftF(input)
    res match {
      //If it succeeded, use waitForChild to wait for the right child
      case Right(tree) =>
        (next, Left(waitForChild(Node(data, tree, _), rightF)))
      //Otherwise, try to deserialize the right child, then run the new left RecFun leftF2 again
      case Left(leftF2) => {
        val (next2, res2) = rightF(next)
        //Return the rest of the list, along with the new RecFun
        (next2, Left(res2 match {
          //If the right child is constructed, wait for the left child to be constructed
          case Right(tree)   => waitForChild(Node(data, _, tree), leftF2)
          //Otherwise, run leftF2 first, then the new right RecFun rightF2
          case Left(rightF2) => waitForChildren(data, leftF2, rightF2)
        }))
      }
    }
  }

Here, one child is known, and we're just waiting for another.

/**
 * @param ctor Function that takes a child and finishes building parent tree
 * @param f    Function to construct child
 */
def waitForChild[T](ctor: Tree[T] => Node[T], f: RecFun[T]): RecFun[T] =
  input => {
    //Try deserializing the child based on the function f
    val (next, res) = f(input)
    (next, res match {
      //If it's fully built, build the parent tree by applying ctor to it
      case Right(tree)  => Right(ctor(tree))
      //Otherwise, wait for the new RecFun
      case Left(recFun) => Left(waitForChild(ctor, recFun))
    })
  }

Here is the code I used for testing:

val tree = Node(
  1,
  Node(2, Node(3, Node(5, Empty, Empty), Empty), Node(4, Empty, Empty)),
  Empty
)
val tree2 = makeTree(List(Some(1), Some(2), None, Some(3), Some(4), Some(5), None))

println(tree2)
assert(tree == tree2)

Here is a Scastie.

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Browse other questions tagged or ask your own question.