TreeLog

repository·main·Indexed 42 days ago

https://github.com/lancewalton/treelog

A Scala library for writing hierarchical logs alongside computations using a Writer monad. It builds a Tree representing a computation's history to help analyze complex, non-linear processes. Supports Scala 2.10.x through 3.6.x, with specific integrations for Scalaz and Cats. Features include DescribedComputation for tracking success/failure, custom annotations via LogTreeLabel, and utilities for serializing and visualizing log trees.

Tokens
3.5K
Snippets
14
Records
16
Agent score
53%

What's inside TreeLog

  1. How TreeLog works: The Writer monad and Tree structure

    main
    TreeLog is designed to capture the hierarchical nature of complex computations. Instead of linear logging, it uses a Writer monad to write to a Tree structure. This allows you to associate log information directly with the values being computed, creating a structured, hierarchical representation of what happened during a computation (what was done, why, and how results were derived).
  2. Install TreeLog for modern Scala

    main

    To use TreeLog with Scala 2.13.x or Scala 3.3.x through 3.6.x, add the following dependency to your libraryDependencies in your build configuration (e.g., build.sbt):

    libraryDependencies ++= Seq("com.casualmiracles" %% "treelog" % "1.9.5")
  3. Install TreeLog for older Scala and Scalaz/Cats versions

    main

    If you are using older versions of Scala or specific functional programming libraries like Scalaz or Cats, use the corresponding dependency configuration:

    Scala 2.12.x and scalaz 7.2.x

    libraryDependencies ++= Seq(
        "com.casualmiracles" %% "treelog-scalaz-72x" % "1.4.3",
        "org.scalaz" %% "scalaz-core" % "7.2.8")

    Scala 2.12.x, 2.11.x and scalaz 7.3.x

    libraryDependencies ++= Seq(
        "com.casualmiracles" %% "treelog" % "1.4.10",
        "org.scalaz" %% "scalaz-core" % "7.3.0-M18")

    Scala 2.12.x and cats 1.6.0

    libraryDependencies ++= Seq("com.casualmiracles" %% "treelog-cats" % "1.4.9")

    Scala 2.12.x and scalaz 7.1.x

    libraryDependencies ++= Seq(
        "com.casualmiracles" %% "treelog-scalaz-71x" % "1.4.0",
        "org.scalaz" %% "scalaz-core" % "7.1.11")

    Scala 2.11.x and scalaz 7.2.x

    libraryDependencies ++= Seq(
        "com.casualmiracles" %% "treelog" % "1.3.0",
        "org.scalaz" %% "scalaz-core" % "7.2.0")

    Scala 2.11.x and scalaz 7.1.x

    libraryDependencies ++= Seq(
        "com.casualmiracles" %% "treelog" % "1.2.6",
        "org.scalaz" %% "scalaz-core" % "7.1.7")

    Scala 2.10.x

    libraryDependencies ++= Seq(
        "com.casualmiracles" %% "treelog" % "1.2.2",
        "org.scalaz" %% "scalaz-core" % "7.0.6")
  4. Using Booleans as success/failure indicators

    main

    The BooleanSyntax allows you to treat a Boolean as a signal for the success or failure of a computation.

    • b ~>? description: If true, succeeds with true and the description. If false, fails with the description.
    • b.decribedWith(failureDescription, successDescription): Allows different descriptions for the false and true cases.

    Syntactic Sugar:

    • b ~>? (failureDesc, successDesc): Equivalent to decribedWith.
    val b = true
    val dc = b ~>? "Is my boolean true?"
    
    // Using different descriptions
    val dc2 = b ~>? ("It was false", "It was true")
  5. Construct a tree using Node and Leaf

    main

    You can build a tree structure using the Tree.Node and Tree.Leaf objects.

    • Tree.Node(root, forest): Creates a node with a root label and a subForest (a LazyList of child Tree objects).
    • Tree.Leaf(root): A convenience method to create a node with no children.

    Both constructors use call-by-name parameters (=> A and => LazyList[Tree[A]]), allowing for lazy evaluation of the tree structure.

    import treelog.Tree
    
    // Create a leaf
    val leaf = Tree.Leaf("end")
    
    // Create a node with children
    val tree = Tree.Node("root", LazyList(
      Tree.Leaf("child 1"),
      Tree.Leaf("child 2")
    ))
  6. Folding over values with log tree updates

    main

    The FoldSyntax allows you to fold over an Iterable[V] while accumulating both the result and the log tree.

    • description ~> / (initial, f): Folds over values starting with initial. For each element, it applies f(accumulator, element), which must return a DescribedComputation. The entire process is wrapped in a branch named description.

    Note: If any step in the fold results in a failure, the subsequent steps are skipped (due to the monadic nature of DescribedComputation).

    val values = List(1, 2, 3)
    val initial = 0.logSuccess("Start")
    
    // Fold: adding each value to the sum, logging each step
    val result = "Summing" ~>/ (values, initial) { (acc, v) => 
      (acc + v).logSuccess(s"Added $v")
    }
  7. Visualize a tree with drawTree

    main

    The drawTree method produces a 2D String representation of the tree, suitable for logging or console output. It uses ASCII-style characters (+-, `, |) to represent branches and stems.

    Note: This method requires an implicit Show[A] instance for the type A contained in the tree to convert labels into strings.

    // Assuming an implicit Show[String] is available
    val visualString = tree.drawTree
    println(visualString)
  8. Serializing DescribedComputations

    main

    You can convert a DescribedComputation[V] into a serializable form to persist the log tree and the result.

    • toSerializableForm(dc): Returns a Tuple2(Either[String, V], SerializableTree[Annotation]).
    • fromSerializableForm(sdc): Reconstructs the DescribedComputation[V] from the serializable tuple.

    This is useful for saving the state of a long-running computation and its associated log history.

    val dc: DescribedComputation[Int] = 42.logSuccess("Done")
    
    // Serialize
    val serialized = toSerializableForm(dc)
    
    // Deserialize
    val restored = fromSerializableForm(serialized)
  9. Using Either as success/failure indicators

    main

    The EitherSyntax converts an Either[String, V] into a DescribedComputation[V].

    • either ~>? (leftDesc, rightDesc): If Left(err), fails with leftDesc(err). If Right(v), succeeds with v and rightDesc.
    • either ~>? description: If Left(err), fails with description - err. If Right(v), succeeds with v and description.
    • either ~>? description: If Right(v), succeeds with v and description. If Left(err), fails with err (the error message itself becomes the failure description).
    val e: Either[String, Int] = Right(10)
    
    // Success with custom description
    val dc = e ~>? (err => s"Error: $err", v => s"Value is $v")
  10. Lifting values into DescribedComputations

    main

    Use LogTreeSyntax to convert raw values into DescribedComputation[V], which tracks both the result (success/failure) and a log tree of the process.

    • logSuccess(description: String): Creates a successful computation with a given description.
    • logFailure(description: String): Creates a failed computation with a given description.

    Syntactic Sugar:

    • value ~> description: Success with description.
    • value ~>! description: Failure with description.
    // Success
    val success = 1.logSuccess("One")
    val successSugar = 1 ~> "One"
    
    // Failure
    val failure = 1.logFailure("One")
    val failureSugar = 1 ~>! "One"
  11. Flatten a tree using pre-order traversal

    main

    The flatten method performs a pre-order traversal of the tree, returning an Eval[LazyList[A]]. This allows you to process all elements in the tree in a single linear sequence.

    // Returns an Eval containing a LazyList of all elements
    val elements: Eval[LazyList[String]] = tree.flatten
    
    // To access the values:
    val list = elements.value
  12. Using Options as success/failure indicators

    main

    The OptionSyntax converts an Option[V] into a DescribedComputation[V].

    • option.log(noneDescription, someDescription): If Some(x), succeeds with x and the someDescription. If None, fails with noneDescription.
    • option ~>? (noneDescription, someDescription): Syntactic sugar for log.
    • option ~>| [B] (f, dflt): If Some, applies f to the value; if None, returns the provided default dflt computation.

    Syntactic Sugar:

    • option ~>? description: Uses the same description for both Some and None cases.
    val opt: Option[Int] = Some(42)
    
    // Success with value included in description
    val dc = opt.log("Missing value") { x => s"Found $x" }
    
    // Simple success/failure
    val dc2 = opt ~>? "The option should be present"