Search code examples
rfunctionrecursion

How can I correctly print a tree structure of an R expression with correct indentation levels?


I am trying to write a function in R called functionTree that takes an R expression and prints its structure in a tree format. The function should display the elements of the expression with their types and indent them according to their depth in the tree. I have the following requirements:

The assignment operator (<-) should be displayed at depth 0, as function: special. The left-hand side of the assignment should be displayed at depth 0. The function call on the right-hand side should be displayed at depth 1, f should be displayed as function:closure. The arguments of the function call should be displayed at depth 1, as atoms.

I have the following code, but it doesn't produce the expected indentation and depth levels:

functionTree <- function(expr, eframe = globalenv(), maxDepth = 3, closureRecursive = FALSE, depth = 0) {
  # Helper function to determine the type description
  getTypeDescription <- function(expr) {
    if (is.symbol(expr)) {
      return("symbol")
    } else if (is.call(expr)) {
      funcName <- as.character(expr[[1]])
      if (funcName %in% c("<-", "=")) {
        return("function:special")
      } else if (funcName == "function") {
        return("function:closure")
      } else {
        return("function:closure")
      }
    } else if (is.atomic(expr) && length(expr) == 1) {
      return("atom")
    } else {
      return("unknown")
    }
  }
  
  # Function to print the current expression and its type
  printExpression <- function(expr, depth) {
    indent <- paste(rep(" ", depth * 2), collapse = "")  # Create indentation based on depth
    typeDescription <- getTypeDescription(expr)
    exprDescription <- deparse(expr)
    if (length(exprDescription) > 1) {
      exprDescription <- exprDescription[1]
    }
    # Ensure there is no attempt to print zero-length variable name
    if (nchar(exprDescription) == 0) {
      exprDescription <- "<empty>"
    }
    cat(indent, depth, " (", typeDescription, ") -> ", exprDescription, "\n", sep = "")
  }
  
  # Base case: Stop the recursion if the maximum depth is exceeded
  if (depth > maxDepth) {
    return(invisible())
  }
  
  # Print the current expression
  printExpression(expr, depth)
  
  # If the expression is a call, recursively analyze its arguments
  if (is.call(expr)) {
    funcName <- as.character(expr[[1]])
    if (funcName %in% c("<-", "=")) {
      # Handle special case for assignment
      functionTree(expr[[1]], eframe, maxDepth, closureRecursive, depth)  # Print the assignment operator
      functionTree(expr[[2]], eframe, maxDepth, closureRecursive, depth)  # Print the left-hand side
      functionTree(expr[[3]], eframe, maxDepth, closureRecursive, depth + 1)  # Print the right-hand side
    } else {
      functionTree(expr[[1]], eframe, maxDepth, closureRecursive, depth + 1)
      lapply(expr[-1], function(sub_expr) functionTree(sub_expr, eframe, maxDepth, closureRecursive, depth + 1))
    }
  }
}

# Example usage of the functionTree function
f <- function(n = 0) { n ^ 2 }
a <- quote(x <- f(5))

# Call the functionTree function with the example expression
functionTree(a)

I tried adjusting the depth and indentation within the function, but I couldn't achieve the desired output. Any help to correct the function to produce the expected output would be appreciated.

My output:

0 (function:special) -> x <- f(5)
0 (symbol) -> <-
0 (symbol) -> x
  1 (function:closure) -> f(5)
    2 (symbol) -> f
    2 (atom) -> 5
[[1]]
NULL

Expected output:

0 (function:special) -> <-
  0 (symbol) -> x
  1 (function:closure) -> f
    1 (atom) -> 5

Solution

  • This version seems to get you closer

    functionTree <- function(expr, eframe = globalenv(), maxDepth = 3, closureRecursive = FALSE, depth = 0) {
      # Helper function to determine the type description
      getTypeDescription <- function(expr) {
        if (is.symbol(expr)) {
          return("symbol")
        } else if (is.call(expr)) {
          funcName <- as.character(expr[[1]])
          if (funcName %in% c("<-", "=")) {
            return("function:special")
          } else if (funcName == "function") {
            return("function:closure")
          } else {
            return("function:closure")
          }
        } else if (is.atomic(expr) && length(expr) == 1) {
          return("atom")
        } else {
          return("unknown")
        }
      }
      
      # Function to print the current expression and its type
      printExpression <- function(expr, depth) {
        indent <- paste(rep(" ", depth * 2), collapse = "")  # Create indentation based on depth
        typeDescription <- getTypeDescription(expr)
        exprDescription <- deparse(expr)
        if (length(exprDescription) > 1) {
          exprDescription <- exprDescription[1]
        }
        # Ensure there is no attempt to print zero-length variable name
        if (nchar(exprDescription) == 0) {
          exprDescription <- "<empty>"
        }
        cat(indent, depth, " (", typeDescription, ") -> ", exprDescription, "\n", sep = "")
      }
      
      # Base case: Stop the recursion if the maximum depth is exceeded
      if (depth > maxDepth) {
        return(invisible())
      }
      
      # If the expression is a call, recursively analyze its arguments
      if (is.call(expr)) {
        funcName <- as.character(expr[[1]])
        if (funcName %in% c("<-", "=")) {
          # Handle special case for assignment
          functionTree(expr[[1]], eframe, maxDepth, closureRecursive, depth)  # Print the assignment operator
          functionTree(expr[[2]], eframe, maxDepth, closureRecursive, depth + 1)  # Print the left-hand side
          functionTree(expr[[3]], eframe, maxDepth, closureRecursive, depth + 1)  # Print the right-hand side
          invisible(NULL)
        } else {
          functionTree(expr[[1]], eframe, maxDepth, closureRecursive, depth)
          lapply(expr[-1], function(sub_expr) functionTree(sub_expr, eframe, maxDepth, closureRecursive, depth + 1))
          invisible(NULL)
        }
      } else {
        printExpression(expr, depth)
      }
    }
    

    The primary thing was to move the printExpression call to the else condition when checking for calls. Otherwise you were just processing the same expression too many times. It also seems like the depth should match the indenting level from the code so this outputs

    0 (symbol) -> <-
      1 (symbol) -> x
      1 (symbol) -> f
        2 (atom) -> 5
    

    Otherwise I'm not sure why you have "0" in "0 (symbol) -> x" in your expected output.