r-lib / lobstr

Understanding complex R objects with tools similar to str()
https://lobstr.r-lib.org/
Other
303 stars 27 forks source link

Add `tree` function to print nested structure list-like objects #56

Closed nstrayer closed 3 years ago

nstrayer commented 3 years ago

This PR adds the function tree() (and its helper function tree_label().

The purpose of tree is to replace str() for complex nested structures. The original inspiration was HTML tag lists with children and various other valuable information hanging off of them.

Examples

Printing simple lists

library(tree)

plain_list <- list(
  list(id = "a",
       val = 2),
  list(id = "b",
       val = 1,
       children = list(
         list(id = "b1",
              val = 2.5),
         list(id = "b2",
              val = 8,
              children = list(
                list(id = "b21",
                     val = 4)
              )))))
tree(plain_list)
#> 
#> [list]
#> ├─1:{list}
#> │ ├─id:"a"
#> │ └─val:2
#> └─2:{list}
#>   ├─id:"b"
#>   ├─val:1
#>   └─children:[list]
#>     ├─1:{list}
#>     │ ├─id:"b1"
#>     │ └─val:2.5
#>     └─2:{list}
#>       ├─id:"b2"
#>       ├─val:8
#>       └─children:[list]
#>         └─1:{list}
#>           ├─id:"b21"
#>           └─val:4

Output depth can be limited to avoid information overload

tree(plain_list, max_depth = 2)
#> 
#> [list]
#> ├─1:{list}
#> │ ├─id:"a"
#> │ └─val:2
#> └─2:{list}
#>   ├─id:"b"
#>   ├─val:1
#>   └─children:[list]
#>     ├─1:{list}
#>     └─2:{list}

You can further simplify output by avoiding printing indices for id-less elements

tree(plain_list, index_arraylike = FALSE)
#> 
#> [list]
#> ├─{list}
#> │ ├─id:"a"
#> │ └─val:2
#> └─{list}
#>   ├─id:"b"
#>   ├─val:1
#>   └─children:[list]
#>     ├─{list}
#>     │ ├─id:"b1"
#>     │ └─val:2.5
#>     └─{list}
#>       ├─id:"b2"
#>       ├─val:8
#>       └─children:[list]
#>         └─{list}
#>           ├─id:"b21"
#>           └─val:4

Prints atomic vectors inline (up to 10 elements)

list_w_vectors <- list(
  name = "vectored list",
  num_vec = 1:10,
  char_vec = letters
)
tree(list_w_vectors)
#> 
#> {list}
#> ├─name:"vectored list"
#> ├─num_vec:1,2,3,4,5,6,7,8,9,10
#> └─char_vec:"a","b","c","d","e","f","g","h","i","j",...(n = 26)

Attempts to work with any list-like object.

For instance: html tag structures

tree(shiny::sliderInput("test", "Input Label", 0, 1, 0.5))
#> 
#> {shiny.tag}
#> ├─name:"div"
#> ├─attribs:{list}
#> │ └─class:"form-group shiny-input-container"
#> └─children:[list]
#>   ├─1:{shiny.tag}
#>   │ ├─name:"label"
#>   │ ├─attribs:{list}
#>   │ │ ├─class:"control-label"
#>   │ │ ├─id:"test-label"
#>   │ │ └─for:"test"
#>   │ └─children:[list]
#>   │   └─1:"Input Label"
#>   └─2:{shiny.tag}
#>     ├─name:"input"
#>     ├─attribs:{list}
#>     │ ├─class:"js-range-slider"
#>     │ ├─id:"test"
#>     │ ├─data-skin:"shiny"
#>     │ ├─data-min:"0"
#>     │ ├─data-max:"1"
#>     │ ├─data-from:"0.5"
#>     │ ├─data-step:"0.01"
#>     │ ├─data-grid:"true"
#>     │ ├─data-grid-num:10
#>     │ ├─data-grid-snap:"false"
#>     │ ├─data-prettify-separator:","
#>     │ ├─data-prettify-enabled:"true"
#>     │ ├─data-keyboard:"true"
#>     │ └─data-data-type:"number"
#>     └─children:[list]

Attributes can be shown (but arent by default)

list_w_attrs <- structure(
  list(
    structure(
      list(id = "a",
           val = 2),
      level = 2,
      name = "first child"
    ),
    structure(
      list(id = "b",
           val = 1,
           children = list(
             list(id = "b1",
                  val = 2.5),
             list(id = "b2",
                  val = 8,
                  children = list(
                    list(id = "b21",
                         val = 4)
                  )))),
      level = 2,
      name = "second child",
      class = "custom-class"
    )),
  level = "1",
  name = "root"
)
tree(list_w_attrs, show_attributes = TRUE)
#> 
#> [list]
#> ├─1:{list}
#> │ ├─id:"a"
#> │ ├─val:2
#> │ ├┄<attr>names:"id","val"
#> │ ├┄<attr>level:2
#> │ └┄<attr>name:"first child"
#> ├─2:{custom-class}
#> ┊ ├─id:"b"
#> ┊ ├─val:1
#> ┊ ├─children:[list]
#> ┊ ┊ ├─1:{list}
#> ┊ ┊ │ ├─id:"b1"
#> ┊ ┊ │ ├─val:2.5
#> ┊ ┊ │ └┄<attr>names:"id","val"
#> ┊ ┊ └─2:{list}
#> ┊ ┊   ├─id:"b2"
#> ┊ ┊   ├─val:8
#> ┊ ┊   ├─children:[list]
#> ┊ ┊   ┊ └─1:{list}
#> ┊ ┊   ┊   ├─id:"b21"
#> ┊ ┊   ┊   ├─val:4
#> ┊ ┊   ┊   └┄<attr>names:"id","val"
#> ┊ ┊   └┄<attr>names:"id","val","children"
#> ┊ ├┄<attr>names:"id","val","children"
#> ┊ ├┄<attr>level:2
#> ┊ ├┄<attr>name:"second child"
#> ┊ └┄<attr>class:"custom-class"
#> ├┄<attr>level:"1"
#> └┄<attr>name:"root"

You can customize the tree appearance by swapping out unicode characters

Find characters at unicode-table.com.

tree(plain_list,
     char_vertical = "\u2551",
     char_horizontal = "\u2550",
     char_branch = "\u2560",
     char_final_branch = "\u255A")
#> 
#> [list]
#> ╠═1:{list}
#> ║ ╠═id:"a"
#> ║ ╚═val:2
#> ╚═2:{list}
#>   ╠═id:"b"
#>   ╠═val:1
#>   ╚═children:[list]
#>     ╠═1:{list}
#>     ║ ╠═id:"b1"
#>     ║ ╚═val:2.5
#>     ╚═2:{list}
#>       ╠═id:"b2"
#>       ╠═val:8
#>       ╚═children:[list]
#>         ╚═1:{list}
#>           ╠═id:"b21"
#>           ╚═val:4

You can also pass custom style functions for class and value printing that transform text. E.g. using crayon to style.

library(crayon)
tree(plain_list, 
     class_printer = white$bgBlue,
     val_printer = underline$red)
Screenshot of output is consoles with styling support.

Screenshot of output in consoles with styling support.

Limitations/Drawbacks

Built entirely outside of lobstr at first:

Testing

hadley commented 3 years ago

Fixes #2. Should we call it rst()?

wch commented 3 years ago

Does rst stand for anything or is it just an anagram of str?

hadley commented 3 years ago

@wch ooops I meant rts() which standards for restricted tree structure. I think that pairs nicely with ast(), ref(), sxp().

nstrayer commented 3 years ago

What about having it as rts with a longer formed alias of tree or something like that for people like me who can never remember acronym names like that?

wch commented 3 years ago

For environments, this should print out something sensible:

A <- R6::R6Class("A",
  public = list(getx = function() private$x),
  private = list(x = 1)
)
a <- A$new()
lobstr::tree(a)

Currently it seems to repeat the same information a bunch of times:

<environment: 0x7fe909664dd8>
├─.__enclos_env__: <environment: 0x7fe909665120>
│ ├─private: <environment: 0x7fe909664a58>
│ │ └─x: 1
│ └─self: <environment: 0x7fe909664dd8>
│   ├─.__enclos_env__: <environment: 0x7fe909665120>
│   │ ├─private: <environment: 0x7fe909664a58>
│   │ │ └─x: 1
│   │ └─self: <environment: 0x7fe909664dd8>
│   │   ├─.__enclos_env__: <environment: 0x7fe909665120>
│   │   │ ├─private: <environment: 0x7fe909664a58>
│   │   │ │ └─x: 1
│   │   │ └─self: <environment: 0x7fe909664dd8>
│   │   │   ├─.__enclos_env__: <environment: 0x7fe909665120>
│   │   │   │ ├─private: <environment: 0x7fe909664a58>
│   │   │   │ │ └─x: 1
│   │   │   │ └─self: <environment: 0x7fe909664dd8>
│   │   │   │   ├─.__enclos_env__: <environment: 0x7fe909665120>
│   │   │   │   │ ├─private: <environment: 0x7fe909664a58>
│   │   │   │   │ │ └─x: 1
│   │   │   │   │ └─self: <environment: 0x7fe909664dd8>...
│   │   │   │   ├─clone: function(){...}
│   │   │   │   └─getx: function(){...}
│   │   │   ├─clone: function(){...}
│   │   │   └─getx: function(){...}
│   │   ├─clone: function(){...}
│   │   └─getx: function(){...}
│   ├─clone: function(){...}
│   └─getx: function(){...}
├─clone: function(){...}
└─getx: function(){...}

I think it should also display classes for environments (a is an environment with classes "A" and "R6").

hadley commented 3 years ago

OTOH it might be better to leave environments off this PR, and tackle them once the main shape of the function has been merged.

nstrayer commented 3 years ago

I switched around the logic a bit and now the R6 example returns this: Do you still think this is too verbose?


A <- R6::R6Class("A",
                 public = list(getx = function() private$x),
                 private = list(x = 1)
)
a <- A$new()
lobstr::tree(a)
#> <environment: 0x7fc388566038>
#> ├─.__enclos_env__: <environment: 0x7fc3885665b0>
#> │ ├─private: <environment: 0x7fc388565cf0>
#> │ │ └─x: 1
#> │ └─self: <environment: 0x7fc388566038>
#> │   ├─clone: function(){...}
#> │   └─getx: function(){...}
#> ├─clone: function(){...}
#> └─getx: function(){...}```
wch commented 3 years ago

In the tree of the R6 object, self is the same object as the top-level R6 object, so I think you could just display something like:

<environment: 0x7fc388566038>
├─.__enclos_env__: <environment: 0x7fc3885665b0>
│ ├─private: <environment: 0x7fc388565cf0>
│ │ └─x: 1
│ └─self: <environment: 0x7fc388566038> (Already seen)
├─clone: function(){...}
└─getx: function(){...}
nstrayer commented 3 years ago

Removing the recusion into the parent simplified the code substantially. Here's the latest output for simple environment situations:

ea <- rlang::env(d = 4, e = 5)
eb <- rlang::env(ea, a = 1, b = 2, c = 3)
lobstr::tree(eb)
#> <environment: 0x7fcfd6fc10e0>
#> ├─a: 1
#> ├─b: 2
#> └─c: 3

A <- R6::R6Class(
  "A",
  public = list(getx = function() private$x),
  private = list(x = 1)
)
a <- A$new()
lobstr::tree(a)
#> <environment: 0x7fcfa719a130>
#> ├─.__enclos_env__: <environment: 0x7fcfa7199ad8>
#> │ ├─private: <environment: 0x7fcfa719a4b0>
#> │ │ └─x: 1
#> │ └─self: <environment: 0x7fcfa719a130> (Already seen)
#> ├─clone: function(){...}
#> └─getx: function(){...}
hadley commented 3 years ago

@nstrayer if you fix the build failures, I think this is in a good place to merge, and we can continue iterating in future PRs.

nstrayer commented 3 years ago

All checks are passing -- finally! So baring name changes this should be good to go.