{"id":23163193,"url":"https://github.com/brucou/functional-rose-tree","last_synced_at":"2025-08-18T03:32:09.252Z","repository":{"id":57241187,"uuid":"128127067","full_name":"brucou/functional-rose-tree","owner":"brucou","description":"A small (2Kb zipped minified) tree-shakeable functional library to manipulate generic rose (a.k.a multi-way) trees","archived":false,"fork":false,"pushed_at":"2023-02-17T22:47:41.000Z","size":679,"stargazers_count":12,"open_issues_count":4,"forks_count":0,"subscribers_count":4,"default_branch":"master","last_synced_at":"2024-04-29T06:01:11.476Z","etag":null,"topics":["data-structures","functional","javascript","lenses","library","rose-tree","tree"],"latest_commit_sha":null,"homepage":"","language":"JavaScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/brucou.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2018-04-04T22:00:17.000Z","updated_at":"2024-06-19T19:19:02.223Z","dependencies_parsed_at":"2024-06-19T19:18:44.814Z","dependency_job_id":"6edfbd2c-9500-4128-a793-6efde1bb08e5","html_url":"https://github.com/brucou/functional-rose-tree","commit_stats":{"total_commits":98,"total_committers":3,"mean_commits":"32.666666666666664","dds":"0.020408163265306145","last_synced_commit":"6aa434aa99705cb2a66e1c6f822964885ef3b26e"},"previous_names":[],"tags_count":1,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/brucou%2Ffunctional-rose-tree","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/brucou%2Ffunctional-rose-tree/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/brucou%2Ffunctional-rose-tree/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/brucou%2Ffunctional-rose-tree/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/brucou","download_url":"https://codeload.github.com/brucou/functional-rose-tree/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":230198051,"owners_count":18188787,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["data-structures","functional","javascript","lenses","library","rose-tree","tree"],"created_at":"2024-12-18T00:17:05.496Z","updated_at":"2024-12-18T00:17:06.135Z","avatar_url":"https://github.com/brucou.png","language":"JavaScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"- [Motivation](#motivation)\n- [Concepts](#concepts)\n- [Key contracts](#key-contracts)\n- [API](#api)\n  * [breadthFirstTraverseTree :: Lenses -\u003e TraverseSpecs -\u003e Tree -\u003e A](#breadthfirsttraversetree--lenses---traversespecs---tree---a)\n  * [preorderTraverseTree :: Lenses -\u003e TraverseSpecs -\u003e Tree -\u003e A](#preordertraversetree--lenses---traversespecs---tree---a)\n  * [postOrderTraverseTree :: Lenses -\u003e TraverseSpecs -\u003e Tree -\u003e A](#postordertraversetree--lenses---traversespecs---tree---a)\n  * [reduceTree :: Lenses -\u003e TraverseSpecs -\u003e Tree -\u003e A](#reducetree--lenses---traversespecs---tree---a)\n  * [forEachInTree :: Lenses -\u003e TraverseSpecs -\u003e Tree -\u003e A](#foreachintree--lenses---traversespecs---tree---a)\n  * [mapOverTree :: Lenses -\u003e MapFn -\u003e Tree -\u003e Tree'](#mapovertree--lenses---mapfn---tree---tree)\n  * [pruneWhen :: Lenses -\u003e Predicate -\u003e Tree -\u003e Tree](#prunewhen--lenses---predicate---tree---tree)\n  * [visitTree :: ExtendedTraversalSpecs -\u003e Tree -\u003e A](#visittree--extendedtraversalspecs---tree---a)\n  * [switchTreeDataStructure :: Lenses -\u003e Lenses -\u003e Tree](#switchtreedatastructure--lenses---lenses---tree)\n  * [traverseObj :: ExtendedTraversalSpecs -\u003e Tree -\u003e A](#traverseobj--extendedtraversalspecs---tree---a)\n- [Tests](#tests)\n- [Build](#build)\n- [Install](#install)\n- [Examples of lenses](#examples-of-lenses)\n\n# Motivation\nThere is no shortage of libraries for manipulating trees in javascript. Because we seek to \nfocus on general multi-way trees, we have excluded those libraries focusing on specialized trees \n(i.e. binary search trees, red-black trees, etc.). Also we did not consider the libraries which \nlook at trees from a visualization perspective (for instance [jstree](https://www.jstree.com/)), as \nwe focus here on handling the data structure, and providing a few basic operations on it.\n\nSuch libraries include, among the most interesting subjects, in order of interest :\n\n- [tree-morph](https://github.com/ngryman/tree-morph) : maintained, API features traversal only, \nfree tree format, tree is immutable, allows partial traversal (node skipping), iterative \nalgorithms, incomplete documentation\n- [tree-crawl](https://github.com/ngryman/tree-crawl) : maintained, API features traversal only, \nfree tree format, tree is mutable, claims to be optimized for performance!, nice API for skipping\n nodes or canceling a traversal, iterative algorithms, incomplete documentation\n- [tree-model](http://jnuno.com/tree-model-js/) : maintained and contributed to, imperative \nobject-based API, basic operations (traversal, find) together with utility functions (`isRoot`, \netc.) supporting the imperative portion of the API, recursive algorithms, nice [demo site](http://jnuno.com/tree-model-js/)!\n- [arboreal](https://github.com/afiore/arboreal) : ancient, no longer maintained, imperative API, \nimposed tree format, only basic operations\n- [t-js](https://github.com/aaronj1335/t-js) : ancient, no longer maintained, semi-functional \nAPI,, basic but key functional operations (bfs/dfs/post-order traversals, map, filter, find), an \ninteresting addition (`stroll`) traversing two trees at the same time, imposed tree format\n- [DataStructures.Tree](https://github.com/stephen-james/DataStructures.Tree) ; ancient, \nundocumented, unmaintained\n\nIn practice, it seems that few people use a dedicated tree library for manipulating tree-like \ndata structure. Rather, what I saw in the wild is ad-hoc implementations of traversals, which are \nadjusted to the particular shape of the data at hand. This is understandable as tree traversal \nalgorithms, specially the recursive ones, are trivial to implement (5-10 lines of code). \n\nHowever :\n\n- iterative algorithms are almost mandatory to process large trees (to avoid exhausting the stack)\n- a generic traversal library fosters reuse (in my particular case, I have various \ntree formats to handle, and it would not be DRY to write the traversal at hand each time for each \nformat)\n- a functional library is also nice to guarantee that there is no destructive update of tree \nnodes, and at the same time allows natural composition and chaining of tree operations\n- a well-designed, tested library enhances readability and maintainability \n\nAs a conclusion, these are the design choices made for this library :\n- manipulation of tree data structure is based on ADT, i.e. not on a specific or concrete data \nstructure as the aforementioned libraries. Those three possible concrete data structures for a tree \nshould be handled by the library just as easily : \n  - `[root, [left, [middle, [midright, midleft]], right]]`, or more commonly \n  - `{label : 'root', children : [{label:'left'}, {label: 'right'}]}`.\n  - `{key0 : {key0.0 : {}, key0.1 : 'something'}, key1 : 2018}`\n- immutability of tree nodes\n- iterative traversal algorithms\n- basic operations available : bfs/dfs/post-order traversals, map/reduce/prune(~filter)/find \noperations\n- advanced operations in a future version : find common ancestor(would involve building a zipper), \nreplace, optional : tree diff(hard), some, every (not so useful), transducers (would be amazing)\n\nAt the current state of the library, only the basic operations are implemented.\n\nAs a bonus, lenses for object traversal are included and allow traversing and mapping over a \njavascript object (POJO).\n\n# Concepts\nIn computing, a multi-way tree or rose tree is a tree data structure with a variable and \nunbounded number of branches per node[^1]. The name rose tree for this structure is prevalent in \nthe functional programming community, so we use it here. For instance, a rose tree can be defined\n in Haskell as follows : `data RoseTree a = RoseTree a [RoseTree a]`.\n\nThere is a distinction between a tree as an abstract data type and as a concrete data structure, \nanalogous to the distinction between a list and a linked list. As a data type, a tree has a value\n (`:: a`) and children (`:: [RoseTree a]`), and the children are themselves trees. \n A linked tree is an example of specific data structure, implementing \n the abstract data type and is a group of nodes, where  each node has a value and a list of \n references to other nodes (its children).  There is also the requirement that no two \"downward\" \n  references point to the same node.\n\nAs an ADT, the abstract tree type T with values of some type E is defined, using the abstract forest type F (list of trees), by the functions:\n\n- value: T → E\n- children: T → F\n- nil: () → F\n- node: E × F → T\n\nwith the axioms:\n\n- value(node(e, f)) = e\n- children(node(e, f)) = f\n\nIn our API, we will use a parameter `lenses` which will provide an implementation as necessary of\n the relevant functions :\n \n - `getLabel :: T -\u003e E`\n - `getChildren :: T -\u003e F`\n - `constructTree :: E x F -\u003e T` \n - `nil` (the empty forest) will be taken by default to be the empty list (`[]`). A forest being \n a list of trees, it is convenient that the empty forest be an empty list.\n\nThese functions are gathered into the `lenses` parameter, as, just like lenses, they allow to \nfocus on a portion of a composite data structure. `constructTree` can be viewed both as a constructor, \nand as the setter part of a lens on the tree.\n\nFor instance, the tree-like object `{label : 'root', children : [{label:'left'}, {label: \n'right'}]}` can be described by the following lenses :\n\n- `getLabel = tree =\u003e tree.label`\n- `getChildren = tree =\u003e tree.children || []`\n- `constructTree = (label, children) =\u003e ({label, children})`\n\n[^1]: Bird, Richard (1998). Introduction to Functional Programming using Haskell. Hemel Hempstead, Hertfordshire, UK: Prentice Hall Europe. p. 195. ISBN 0-13-484346-0.\n\nThe flexibility offered by the abstract data type comes in handy when interpreting abstract \nsyntax trees, whose format is imposed by the parser, and which may vary widely according to the \ntarget language and specific parser. The ADT technique also allows for higher reusability.\n\n**NOTE** : All functions are provided without currying. We paid attention to the order of parameters to \nfacilitate currying for those who will find it convenient. The `ramda` functional library can be \nused easily to curry any relevant provided function. \n**NOTE** : This API style could also be called [interface-passing style](https://common-lisp.net/~frideau/lil-ilc2012/lil-ilc2012.html) \n\n# Key contracts\n## Key types\n- `Traversal :: BFS | PRE_ORDER | POST_ORDER`\n- `Lenses :: {{getLabel :: T -\u003e E, getChildren :: T -\u003e F, constructTree :: ExF -\u003e T}}`\n- `State :: {{isAdded :: Boolean, isVisited :: Boolean, path :: Array\u003cNumber\u003e, ...}}` (extensible\n record)\n- `TraversalState :: HashMap\u003cT, State\u003e` (the hashmap exposes `get` and `set` methods to read and \nupdate itself)\n- `Reducer\u003cA, T, TraversalState\u003e :: A -\u003e TraversalState -\u003e T -\u003e A` (reducer may additionally \nupdate the traversal state if necessary to implement a given traversal logic)\n- `TraverseSpecs :: {{strategy :: Optional\u003cTraversal\u003e, seed : A, visit :: Reducer\u003cA, T, TraversalState\u003e }}`\n\nThose types can be slightly modified depending on the specific function executed. The meaning of \nthose types is pretty straight-forward. Let's just notice that `TraversalState` is a map which \nassociates to each node being traversed the state of the traversal, and possibly any extra state \nthat the API consumer might want to add, while traversing. As a matter of fact, the `visit` \nfunction could mutate `TraversalState` if that would make sense for the situation at end. That \nmutation would be invisible from outside of the API, as long as none of the mutated state is \nexported (\"If a tree falls in a forest and no one is around to hear it, does it make a sound?\").\n\n## No node repetition\nIt is important to note that **no tree can repeat the same nodes** with sameness defined by \nreferential equality. It is easy to inadvertently repeat the same node :\n\n```javascript\nconst tree1  ...;\nconst tree2 = {label : ..., children : [tree1, tree1]}\n```\n\nWhile `tree2` is a well-formed tree, our library will bug in that case, for reasons due to our \nspecific implementation (nodes are used as keys to keep the traversal state, and keys must be \nunique). It gets really tricky if your tree somehow can be a javascript primitive, in which case,\n this contract would mean that all such primitives should have a different value! Tests so far \n seems to show that 'normal' tree structures do not have this primitive-value-duplication problem.\n\n# API\n## breadthFirstTraverseTree :: Lenses -\u003e TraverseSpecs -\u003e Tree -\u003e A\n### Description\nTraverse a tree breadth-first, applying a reducer while traversing the tree, and returning the \nfinal accumulated reduction.\n\n### Types\n- `Tree :: T`\n- `Traversal :: BFS | PRE_ORDER | POST_ORDER`\n- `State :: {{isAdded :: Boolean, isVisited :: Boolean, path :: Array\u003cNumber\u003e, ...}}` (extensible\n record)\n- `TraversalState :: Map\u003cT, State\u003e`\n- `Lenses :: {{getLabel :: T -\u003e E, getChildren :: T -\u003e F, constructTree :: ExF -\u003e T}}`\n- `Reducer\u003cA, T, TraversalState\u003e :: A -\u003e TraversalState -\u003e T -\u003e A`\n- `TraverseSpecs :: {{strategy :: Optional\u003cTraversal\u003e, seed : A, visit :: Reducer\u003cA, T, TraversalState\u003e }}`\n\n### Other contracts\n- a seed **must** be a JSON object or a function returning a constructor (e.g `() =\u003e Map`) which \nexecuted will produce a seed value\n\n### Examples\n**NOTE** : for bfs/pre/post-order traversals, we only need the `getChildren` lens. It is a good \nhabit however to define and pass the full`lenses` once and for all.\n \n```ecmascript 6\nconst tree = {\n  label: \"root\",\n  children: [\n    { label: \"left\" },\n    {\n      label: \"middle\",\n      children: [{ label: \"midleft\" }, { label: \"midright\" }]\n    },\n    { label: \"right\" }\n  ]\n};\n\nconst lenses = {\n  getChildren: tree =\u003e tree.children || []\n};\n\nconst traverse = {\n  seed: [],\n  visit: (result, traversalState, tree) =\u003e {\n    result.push(tree.label);\n    return result;\n  }\n};\n\nQUnit.test(\"main case - breadthFirstTraverseTree\", function exec_test(assert) {\n  const actual = breadthFirstTraverseTree(lenses, traverse, tree);\n  const expected = [\n    \"root\",\n    \"left\",\n    \"middle\",\n    \"right\",\n    \"midleft\",\n    \"midright\"\n  ];\n\n  assert.deepEqual(actual, expected, `Works!`);\n});\n```\n\n## preorderTraverseTree :: Lenses -\u003e TraverseSpecs -\u003e Tree -\u003e A\n### Description\nTraverse a tree pre=order depth-first, applying a reducer while traversing the tree, and returning \nthe final accumulated reduction.\n\n### Types\n- `Tree :: T`\n- `Traversal :: BFS | PRE_ORDER | POST_ORDER`\n- `State :: {{isAdded :: Boolean, isVisited :: Boolean, path :: Array\u003cNumber\u003e, ...}}` (extensible\n record)\n- `TraversalState :: Map\u003cT, State\u003e`\n- `Lenses :: {{getLabel :: T -\u003e E, getChildren :: T -\u003e F, constructTree :: ExF -\u003e T}}`\n- `Reducer\u003cA, T, TraversalState\u003e :: A -\u003e TraversalState -\u003e T -\u003e A`\n- `TraverseSpecs :: {{strategy :: Optional\u003cTraversal\u003e, seed : A, visit :: Reducer\u003cA, T, TraversalState\u003e }}`\n\n### Examples\n```ecmascript 6\nQUnit.test(\"main case - preorderTraverseTree\", function exec_test(assert) {\n  const actual = preorderTraverseTree(lenses, traverse, tree);\n  const expected = [\n    \"root\",\n    \"left\",\n    \"middle\",\n    \"midleft\",\n    \"midright\",\n    \"right\"\n  ];\n\n  assert.deepEqual(actual, expected, `Works!`);\n});\n```\n\n## postOrderTraverseTree :: Lenses -\u003e TraverseSpecs -\u003e Tree -\u003e A\n### Description\nTraverse a tree post=order depth-first, applying a reducer while traversing the tree, and returning \nthe final accumulated reduction.\n\n### Types\n- `Tree :: T`\n- `Traversal :: BFS | PRE_ORDER | POST_ORDER`\n- `State :: {{isAdded :: Boolean, isVisited :: Boolean, path :: Array\u003cNumber\u003e, ...}}` (extensible\n record)\n- `TraversalState :: Map\u003cT, State\u003e`\n- `Lenses :: {{getLabel :: T -\u003e E, getChildren :: T -\u003e F, constructTree :: ExF -\u003e T}}`\n- `Reducer\u003cA, T, TraversalState\u003e :: A -\u003e TraversalState -\u003e T -\u003e A`\n- `TraverseSpecs :: {{strategy :: Optional\u003cTraversal\u003e, seed : A, visit :: Reducer\u003cA, T, TraversalState\u003e }}`\n\n### Other contracts\n- a seed **must** be a JSON object or a function returning a constructor (e.g `() =\u003e Map`) which \nexecuted will produce a seed value\n\n### Examples\n```ecmascript 6\nQUnit.test(\"main case - postOrderTraverseTree\", function exec_test(assert) {\n  const actual = postOrderTraverseTree(lenses, traverse, tree);\n  const expected = [\n    \"left\",\n    \"midleft\",\n    \"midright\",\n    \"middle\",\n    \"right\",\n    \"root\"\n  ];\n\n  assert.deepEqual(actual, expected, `Works!`);\n});\n```\n\n## reduceTree :: Lenses -\u003e TraverseSpecs -\u003e Tree -\u003e A\n**NOTE** : the  `strategy` property is this time mandatory as part of the traversal specs.\n\n### Description\nTraverse a tree according to the parameterized traversal stratgy, applying a reducer while \ntraversing the tree, and returning the final accumulated reduction.\n\n### Types\n- `Tree :: T`\n- `Traversal :: BFS | PRE_ORDER | POST_ORDER`\n- `State :: {{isAdded :: Boolean, isVisited :: Boolean, path :: Array\u003cNumber\u003e, ...}}` (extensible\n record)\n- `TraversalState :: Map\u003cT, State\u003e`\n- `Lenses :: {{getLabel :: T -\u003e E, getChildren :: T -\u003e F, constructTree :: ExF -\u003e T}}`\n- `Reducer\u003cA, T, TraversalState\u003e :: A -\u003e TraversalState -\u003e T -\u003e A`\n- `TraverseSpecs :: {{strategy :: Traversal, seed : A, visit :: Reducer\u003cA, T, TraversalState\u003e }}`\n\n### Other contracts\n- a seed **must** be a JSON object or a function returning a constructor (e.g `() =\u003e Map`) which \nexecuted will produce a seed value\n\n### Examples\n```ecmascript 6\nQUnit.test(\"main case - reduceTree\", function exec_test(assert) {\n  const reduceTraverse = assoc(\"strategy\", BFS, traverse);\n  const actual = reduceTree(lenses, reduceTraverse, tree);\n  const expected = [\n    \"root\",\n    \"left\",\n    \"middle\",\n    \"right\",\n    \"midleft\",\n    \"midright\"\n  ];\n\n  assert.deepEqual(actual, expected, `Works!`);\n});\n```\n\n## forEachInTree :: Lenses -\u003e TraverseSpecs -\u003e Tree -\u003e A\n### Description\nTraverse a tree according to the parameterized traversal strategy, applying a reducer while \ntraversing the tree, and returning the final accumulated reduction. Note that, as the action may \nperform effects, the order of the traversal is particularly relevant.\n\n**NOTE** : the traversal specs require this time an `action` property defining the action to \nexecute on each traversed portion of the tree. The same stands for the `strategy` property.\n\n### Types\n- `Tree :: T`\n- `Traversal :: BFS | PRE_ORDER | POST_ORDER`\n- `State :: {{isAdded :: Boolean, isVisited :: Boolean, path :: Array\u003cNumber\u003e, ...}}` (extensible\n record)\n- `TraversalState :: Map\u003cT, State\u003e`\n- `Lenses :: {{getLabel :: T -\u003e E, getChildren :: T -\u003e F, constructTree :: ExF -\u003e T}}`\n- `Action :: T -\u003e traversalState -\u003e ()`\n- `TraverseSpecs :: {{strategy :: Traversal, action :: Action }}`\n\n### Examples\n```ecmascript 6\nQUnit.test(\"main case - forEachInTree\", function exec_test(assert) {\n  const traces = [];\n  const traverse = {\n    strategy: POST_ORDER,\n    action: (tree, traversalState) =\u003e {\n      traces.push(traversalState.get(tree))\n      traces.push(tree.label)\n    }\n  }\n\n  forEachInTree(lenses, traverse, tree);\n  const actual = traces;\n  const expected = [\n    {\n      \"isAdded\": true,\n      \"isVisited\": false,\n      \"path\": [\n        0,\n        0\n      ]\n    },\n    \"left\",\n    {\n      \"isAdded\": true,\n      \"isVisited\": false,\n      \"path\": [\n        0,\n        1,\n        0\n      ]\n    },\n    \"midleft\",\n    {\n      \"isAdded\": true,\n      \"isVisited\": false,\n      \"path\": [\n        0,\n        1,\n        1\n      ]\n    },\n    \"midright\",\n    {\n      \"isAdded\": true,\n      \"isVisited\": true,\n      \"path\": [\n        0,\n        1\n      ]\n    },\n    \"middle\",\n    {\n      \"isAdded\": true,\n      \"isVisited\": false,\n      \"path\": [\n        0,\n        2\n      ]\n    },\n    \"right\",\n    {\n      \"isAdded\": true,\n      \"isVisited\": true,\n      \"path\": [\n        0\n      ]\n    },\n    \"root\"\n  ];\n\n  assert.deepEqual(actual, expected, `Works!`);\n});\n```\n\n## mapOverTree :: Lenses -\u003e MapFn -\u003e Tree -\u003e Tree'\n### Description \nTraverse a tree, applying a mapping function, while, and returning a mapped tree, containing the  \nmapped nodes. The `constructTree` lens is mandatory here to rebuild the tree from its nodes. The\norigin tree is traversed from the leaves (post-order traversal) upwards. As the traversal progress,\n`constructTree` maps first the leaves, and recursively maps the mapped children together with each \ntraversed compound node. We signal that with the signature `constructTree :: E'xF' -\u003e T'` \nfor `constructTree`, where `E'` is a mapped label, and `F` are mapped children.\n\n### Types\n- `Tree :: T`\n- `Tree' :: T'`\n- `Traversal :: BFS | PRE_ORDER | POST_ORDER`\n- `State :: {{isAdded :: Boolean, isVisited :: Boolean, path :: Array\u003cNumber\u003e, ...}}` (extensible record)\n- `TraversalState :: Map\u003cT, State\u003e`\n- `Lenses :: {{getLabel :: T -\u003e E, getChildren :: T -\u003e F, constructTree :: E'xF' -\u003e T'}}`\n- `MapFn :: E -\u003e E'`\n\n### Examples\n```ecmascript 6\nQUnit.test(\"main case - mapOverTree\", function exec_test(assert) {\n  const getChildren = tree =\u003e tree.children || [];\n  const getLabel = tree =\u003e tree.label || '';\n  const constructTree = (label, trees) =\u003e ({label, children : trees});\n  const mapFn = label =\u003e addPrefix('Map:')(label)\n  const lenses = { getChildren, constructTree, getLabel };\n\n  const actual = mapOverTree(lenses, mapFn, tree);\n  const expected = {\n    \"children\": [\n      {\n        \"children\": [],\n        \"label\": \"Map:left\"\n      },\n      {\n        \"children\": [\n          {\n            \"children\": [],\n            \"label\": \"Map:midleft\"\n          },\n          {\n            \"children\": [],\n            \"label\": \"Map:midright\"\n          }\n        ],\n        \"label\": \"Map:middle\"\n      },\n      {\n        \"children\": [],\n        \"label\": \"Map:right\"\n      }\n    ],\n    \"label\": \"Map:root\"\n  };\n\n  assert.deepEqual(actual, expected, `Works!`);\n});\n\n```\n\n### Real-life example\nIn this example, we map `.graphml` XML file describing a compound graph to the corresponding state \nhierarchy expressed as an object. Given the following graph:\n\n![Example graph](https://imgur.com/9KpIiSR.png) \n\nwith the following graphml:\n\n```xml\n\u003c?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"no\"?\u003e\n\u003cgraphml ...\u003e\n  \u003c!--Created by yEd 3.16.1--\u003e\n  \u003ckey attr.name=\"Description\" attr.type=\"string\" for=\"graph\" id=\"d0\"/\u003e\n  (...)\n  \u003ckey for=\"edge\" id=\"d10\" yfiles.type=\"edgegraphics\"/\u003e\n  \u003cgraph edgedefault=\"directed\" id=\"G\"\u003e\n    \u003cdata key=\"d0\"/\u003e\n    \u003cnode id=\"n0\"\u003e\n      \u003cdata key=\"d6\"\u003e\n        \u003cy:ShapeNode\u003e\n          \u003cy:Geometry height=\"30.0\" width=\"30.0\" x=\"405.42301587301586\" y=\"467.0\"/\u003e\n          \u003cy:Fill color=\"#FF6600\" transparent=\"false\"/\u003e\n          \u003cy:BorderStyle color=\"#000000\" raised=\"false\" type=\"line\" width=\"1.0\"/\u003e\n          \u003cy:NodeLabel alignment=\"center\" autoSizePolicy=\"content\" fontFamily=\"Dialog\" fontSize=\"12\" fontStyle=\"bold\" hasBackgroundColor=\"false\" hasLineColor=\"false\" height=\"18.701171875\" horizontalTextPosition=\"center\" iconTextGap=\"4\" modelName=\"internal\" modelPosition=\"c\" textColor=\"#000000\" verticalTextPosition=\"bottom\" visible=\"true\" width=\"21.994140625\" x=\"4.0029296875\" y=\"5.6494140625\"\u003einit\u003c/y:NodeLabel\u003e\n          \u003cy:Shape type=\"ellipse\"/\u003e\n        \u003c/y:ShapeNode\u003e\n      \u003c/data\u003e\n    \u003c/node\u003e\n    \u003cnode id=\"n1\" yfiles.foldertype=\"group\"\u003e\n      \u003cdata key=\"d4\"/\u003e\n      \u003cdata key=\"d5\"/\u003e\n      \u003cdata key=\"d6\"\u003e\n        \u003cy:ProxyAutoBoundsNode\u003e\n          \u003cy:Realizers active=\"0\"\u003e\n            \u003cy:GroupNode\u003e\n             (...)\n              \u003cy:NodeLabel alignment=\"right\" autoSizePolicy=\"node_width\" backgroundColor=\"#EBEBEB\" borderDistance=\"0.0\" fontFamily=\"Dialog\" fontSize=\"15\" fontStyle=\"plain\" hasLineColor=\"false\" height=\"22.37646484375\" horizontalTextPosition=\"center\" iconTextGap=\"4\" modelName=\"internal\" modelPosition=\"t\" textColor=\"#000000\" verticalTextPosition=\"bottom\" visible=\"true\" width=\"96.0\" x=\"0.0\" y=\"0.0\"\u003eGroup 1\u003c/y:NodeLabel\u003e\n             (...)\n            \u003c/y:GroupNode\u003e\n            \u003cy:GenericGroupNode configuration=\"PanelNode\"\u003e\n             (...)\n            \u003c/y:GenericGroupNode\u003e\n          \u003c/y:Realizers\u003e\n        \u003c/y:ProxyAutoBoundsNode\u003e\n      \u003c/data\u003e\n      \u003cgraph edgedefault=\"directed\" id=\"n1:\"\u003e\n        \u003cnode id=\"n1::n0\"\u003e\n          \u003cdata key=\"d6\"\u003e\n            \u003cy:ShapeNode\u003e\n              \u003cy:Geometry height=\"61.0\" width=\"64.0\" x=\"223.39523809523808\" y=\"161.0\"/\u003e\n              \u003cy:Fill color=\"#FFCC00\" transparent=\"false\"/\u003e\n              \u003cy:BorderStyle color=\"#000000\" raised=\"false\" type=\"line\" width=\"1.0\"/\u003e\n              \u003cy:NodeLabel alignment=\"center\" autoSizePolicy=\"content\" fontFamily=\"Dialog\" fontSize=\"12\" fontStyle=\"bold\" hasBackgroundColor=\"false\" hasLineColor=\"false\" height=\"33.40234375\" horizontalTextPosition=\"center\" iconTextGap=\"4\" modelName=\"internal\" modelPosition=\"c\" textColor=\"#000000\" verticalTextPosition=\"bottom\" visible=\"true\" width=\"53.9921875\" x=\"5.00390625\" y=\"13.798828125\"\u003eShowing\nmini UI\u003c/y:NodeLabel\u003e\n              \u003cy:Shape type=\"roundrectangle\"/\u003e\n            \u003c/y:ShapeNode\u003e\n          \u003c/data\u003e\n        \u003c/node\u003e\n        \u003cnode id=\"n1::n1\"\u003e\n             (...)\n        \u003c/node\u003e\n        (...)\n      \u003c/graph\u003e\n    \u003c/node\u003e\n   (...)\n    \u003cedge id=\"e0\" source=\"n0\" target=\"n1\"\u003e\n      \u003cdata key=\"d9\"/\u003e\n      \u003cdata key=\"d10\"\u003e\n        \u003cy:PolyLineEdge\u003e\n          \u003cy:Path sx=\"0.0\" sy=\"7.5\" tx=\"0.0\" ty=\"239.688232421875\"/\u003e\n          \u003cy:LineStyle color=\"#000000\" type=\"line\" width=\"1.0\"/\u003e\n          \u003cy:Arrows source=\"none\" target=\"standard\"/\u003e\n          \u003cy:EdgeLabel alignment=\"center\" configuration=\"AutoFlippingLabel\" distance=\"2.0\" fontFamily=\"Dialog\" fontSize=\"12\" fontStyle=\"plain\" hasBackgroundColor=\"false\" hasLineColor=\"false\" height=\"33.40234375\" horizontalTextPosition=\"center\" iconTextGap=\"4\" modelName=\"side_slider\" preferredPlacement=\"right\" ratio=\"0.0\" textColor=\"#000000\" verticalTextPosition=\"bottom\" visible=\"true\" width=\"52.0234375\" x=\"-64.1347844199529\" y=\"2.0\"\u003e^K\n/ activate\u003cy:PreferredPlacementDescriptor angle=\"0.0\" angleOffsetOnRightSide=\"0\" angleReference=\"absolute\" angleRotationOnRightSide=\"co\" distance=\"-1.0\" placement=\"anywhere\" side=\"right\" sideReference=\"relative_to_edge_flow\"/\u003e\n          \u003c/y:EdgeLabel\u003e\n          \u003cy:BendStyle smoothed=\"false\"/\u003e\n        \u003c/y:PolyLineEdge\u003e\n      \u003c/data\u003e\n    \u003c/edge\u003e\n    (...)\n  \u003c/graph\u003e\n  (...)\n\u003c/graphml\u003e\n\n``` \n\nwe extract the state hierarchy as:\n\n```json\n{ \"ღ\":\n   { \"n0ღinit\": \"\",\n     \"n1ღGroup 1\":\n      { \"n1::n0ღShowing\\nmini UI\": \"\",\n        \"n1::n1ღinit\": \"\",\n        \"n1::n2ღShowing\\nbig UI\": \"\",\n        \"n1::n3ღH*\": \"\" \n      },\n     \"n2ღupdating\": \"\" \n   } \n}\n```\n\nand the mapping between the yEd node naming and the user-generated node names as follows:\n\n```json\n{ \"n0\": \"init\",\n  \"n1::n0\": \"Showing\\nmini UI\",\n  \"n1::n1\": \"init\",\n  \"n1::n2\": \"Showing\\nbig UI\",\n  \"n1::n3\": \"H*\",\n  \"n2\": \"updating\" \n}\n\n``` \n\nThe code is the following: \n\n```js\nconst parser = require('fast-xml-parser');\nconst {mapOverTree} = require('fp-rosetree');\nconst {lensPath, view, mergeAll} = require('ramda');\nconst STATE_LABEL_SEP='ღ';\n\n// Lenses for traversing the syntax tree\nfunction isCompoundState(graphObj){\n  return graphObj['@_yfiles.foldertype']==='group'\n}\n\nconst atomicStateLens = lensPath(['y:ShapeNode', 'y:NodeLabel', '#text']);\nconst compoundStateLens = lensPath(['y:ProxyAutoBoundsNode', 'y:Realizers', 'y:GroupNode', 'y:NodeLabel', '#text']);\n\nconst getLabel = graphObj =\u003e {\n  const graphData = graphObj.data;\n  const lens = isCompoundState(graphObj) ? compoundStateLens : atomicStateLens;\n  const dataKeys = Array.isArray(graphData)\n    ? graphData.reduce((acc, dataItem) =\u003e {\n      return Object.assign(acc, {[dataItem['@_key']]: view(lens, dataItem)})\n    },{})\n    : graphData['@_key'] === 'd6'\n      ? {d6: view(lens, graphData)}\n      : {};\n  const stateLabel = dataKeys.d6 || \"\";\n\n  return [graphObj['@_id'], stateLabel]\n};\nconst getChildren = graphObj =\u003e graphObj.graph ? graphObj.graph.node : [];\nconst constructStateHierarchy= (label, children) =\u003e {\n  const _label = label.join(STATE_LABEL_SEP);\n  return children \u0026\u0026 children.length === 0\n    ? {[_label]: \"\"}\n    : {[_label]: mergeAll(children)}\n};\nconst constructStateYed2KinglyMap = (label, children) =\u003e {\n  return children \u0026\u0026 children.length === 0\n    ? {[label[0]]: label[1]}\n    : mergeAll(children)\n};\nconst stateHierarchyLens = {\n  getLabel,\n  getChildren,\n  constructTree: constructStateHierarchy,\n};\nconst stateYed2KinglyLens = {\n  getLabel,\n  getChildren,\n  constructTree: constructStateYed2KinglyMap,\n};\nconst {graphml: graphObj} = parser.parse(yedString, {ignoreAttributes: false});\nconst stateHierarchy = mapOverTree(stateHierarchyLens, x=\u003ex, graphObj);\nconst stateYed2KinglyMap = mapOverTree(stateYed2KinglyLens, x=\u003ex, graphObj);\n\n```\n\n### Contracts\n- `mapFn` must be a pure function. In particular, `mapFn` receives the label (i.e. the structure \nreturned by `lenses.getLabel`), and canoot modify in-place this label. This ensures the \n`mapOverTree` function is also pure.\n\n## pruneWhen :: Lenses -\u003e Predicate -\u003e Tree -\u003e Tree\n### Description \nTraverse a tree, applying a predicate, which when failed leads to discarding any descendant \nnodes of the node failing that predicate. The failing node itself remains in the result tree. \n Note that the `constructTree` lens is mandatory here to rebuild the tree from its nodes. Note also \n that the predicate is passed the traversal state, together with the node. This allows to \n implement stop conditions by modifying directly the traversal state (adding for instance a \n `isTraversalStopped` flag).\n\n### Types\n- `Tree :: T`\n- `State :: {{isAdded :: Boolean, isVisited :: Boolean, path :: Array\u003cNumber\u003e, ...}}` (extensible record)\n- `TraversalState :: Map\u003cT, State\u003e`\n- `Lenses :: {{getLabel :: T -\u003e E, getChildren :: T -\u003e F, constructTree :: ExF -\u003e T}}`\n- `Predicate :: T -\u003e TraversalState -\u003e Boolean`\n\n### Examples\n```javascript\nQUnit.test(\"main case - pruneWhen\", function exec_test(assert) {\n  const getChildren = tree =\u003e tree.children || [];\n  const getLabel = tree =\u003e tree.label || '';\n  const constructTree = (label, trees) =\u003e ({label, children : trees});\n  const predicate = (tree, traversalState) =\u003e traversalState.get(tree).path.length \u003e 1;\n  const lenses = { getChildren, constructTree, getLabel };\n\n  const actual = pruneWhen(lenses, predicate, tree);\n  const expected = {\n    \"children\": [\n      {\n        \"children\": [],\n        \"label\": \"left\"\n      },\n      {\n        \"children\": [],\n        \"label\": \"middle\"\n      },\n      {\n        \"children\": [],\n        \"label\": \"right\"\n      }\n    ],\n    \"label\": \"root\"\n  };\n\n  assert.deepEqual(actual, expected, `Works!`);\n});\n```\n## visitTree :: ExtendedTraversalSpecs -\u003e Tree -\u003e A\n### Description \nThis is the generic tree traversal algorithm that all traversals use as their core. \n\n- The tree is traversed starting from the root, \n- for each traversed node its children are generating traversal tasks, \n- a store is used to keep track of the pending traversal tasks to execute, \n- each task involves the application of a visiting function which builds iteratively the result of\n the traversal, taking inputs from the traversal state, and the traversed node \n- the traversal state includes flags (`isAdded`, `isVisited`) and relevant information (`path`) \nto the traversal\n- the traversal state is passed to the `getChildren` lens, and the visitor function, for those \ncases where the traversal tasks to generate, or visits to undertake, depend on the traversal state \n  - that is for instance the case for iterative post-order traversal, where we traverse a parent \n  node twice, but only visit it once, after its children have been visited)\n  - that is also the case for incomplete traversals (`pruneWhen`), where we discard traversing \n  and visiting some nodes, based on some predicate \n\n### Types\n- `Tree :: T`\n- `Traversal :: BFS | PRE_ORDER | POST_ORDER`\n- `EmptyStore :: *`\n- `Store :: {{empty :: EmptyStore, add :: [T] -\u003e Store -\u003e (), takeAndRemoveOne :: Store -\u003e \nMaybe\u003cT\u003e, isEmpty :: Store -\u003e Boolean}}`\n- `State :: {{isAdded :: Boolean, isVisited :: Boolean, path :: Array\u003cNumber\u003e, ...}}` (extensible\n record)\n- `TraversalState :: Map\u003cT, State\u003e`\n- `Lenses :: {{getLabel :: T -\u003e E, getChildren :: T -\u003e F, constructTree :: ExF -\u003e T}}`\n- `Reducer\u003cA, T, TraversalState\u003e :: A -\u003e TraversalState -\u003e T -\u003e A`\n- `TraverseSpecs :: {{seed : A, visit :: Reducer\u003cA, T, TraversalState\u003e }}`\n- `ExtendedTraversalSpecs :: {{store :: Store, lenses :: Lenses, traverse :: TraverseSpecs}}`\n\n### Other contracts\n- an empty store **must** be a JSON object or a function returning a constructor (e.g `() =\u003e \nArray`) which executed will produce the `empty` value\n- a seed **must** be a JSON object or a function returning a constructor (e.g `() =\u003e Map`) which \nexecuted will produce a seed value\n\n### Examples\nBreadth-first traversal requires a stack store...\n \n```ecmascript 6\nexport function breadthFirstTraverseTree(lenses, traverse, tree) {\n  const { getChildren } = lenses;\n  const traversalSpecs = {\n    store: {\n      empty: [],\n      takeAndRemoveOne: store =\u003e store.shift(),\n      isEmpty: store =\u003e store.length === 0,\n      add: (subTrees, store) =\u003e store.push.apply(store, subTrees)\n    },\n    lenses: { getChildren: (traversalState, subTree) =\u003e getChildren(subTree) },\n    traverse\n  };\n\n  return visitTree(traversalSpecs, tree);\n}\n```\n\nwhile a depth-first traversal requires a queue store. Additionally, a custom lens adds children \n nodes for visit only under some conditions corresponding to post-order traversal (i.e. parent \n  must be visited only after children).\n  \n```ecmascript 6\nexport function postOrderTraverseTree(lenses, traverse, tree) {\n  const { getChildren } = lenses;\n  const isLeaf = (tree, traversalState) =\u003e getChildren(tree, traversalState).length === 0;\n  const { seed, visit } = traverse;\n  const predicate = (tree, traversalState) =\u003e traversalState.get(tree).isVisited || isLeaf(tree, traversalState)\n  const decoratedLenses = {\n    // For post-order, add the parent at the end of the children, that simulates the stack for the recursive function\n    // call in the recursive post-order traversal algorithm\n    getChildren: (traversalState, tree) =\u003e\n      predicate(tree, traversalState)\n        ? []\n        : getChildren(tree, traversalState).concat(tree)\n  };\n  const traversalSpecs = {\n    store: {\n      empty: [],\n      takeAndRemoveOne: store =\u003e store.shift(),\n      isEmpty: store =\u003e store.length === 0,\n      add: (subTrees, store) =\u003e store.unshift(...subTrees)\n    },\n    lenses: decoratedLenses,\n    traverse: {\n      seed: seed,\n      visit: (result, traversalState, tree) =\u003e {\n        // Cases :\n        // 1. label has been visited already : visit\n        // 2. label has not been visited, and there are no children : visit\n        // 3. label has not been visited, and there are children : don't visit, will do it later\n        if (predicate(tree, traversalState)) {\n          visit(result, traversalState, tree);\n        }\n\n        return result;\n      }\n    }\n  };\n\n  return visitTree(traversalSpecs, tree);\n}\n```\n\n## switchTreeDataStructure :: Lenses -\u003e Lenses -\u003e Tree\n### Description\nAllows to convert between concrete tree data structures. Note that for the conversion to be \npossible, the lenses must be very well behaved. It is for instance not always possible to convert\n to an object tree data structure.\n\n### Types\nThe types have been introduced previously. \n\n### Other contracts\nGood behaviour of the lenses. If you are unsure of your lenses behaviour, try out a few conversions.\nIf it works for a non-trivial tree, then it will always work.   \n\n### Examples\nCf. tests. \n\nFor instance, this tree :\n\n```javascript\nconst tree = {\n  label: \"root\",\n  children: [\n    { label: \"left\" },\n    {\n      label: \"middle\",\n      children: [{ label: \"midleft\" }, { label: \"midright\" }]\n    },\n    { label: \"right\" }\n  ]\n};\n```\n\nbecomes this equivalent tree :\n\n```javascript\n[\n    \"root\",\n    [\n      \"left\",\n      [\n        \"middle\",\n        [\n          \"midleft\",\n          \"midright\"\n        ]\n      ],\n      \"right\"\n    ]\n  ]\n``` \n\nIt is however impossible to convert any of those tree data structure towards an object tree.\n\n## traverseObj :: ExtendedTraversalSpecs -\u003e Tree -\u003e A\n### Description\nAllows to traverse an object (POJO), applying a visiting function to every property. Traversal \nstrategy can be specified (pre-order, post-order, or breadth-first).\n  \n### Types\nTypes are as introduced previously. \n\n### Other contracts\nOld same old.\n\n### Examples\nCf. tests\n\n# Tests\n- `npm run test`\n\n# Build\n- `npm run build`\n- `npm run dist` \n\n# Install\n- `npm fp-rosetree`\n\n# Examples of lenses\n## Object traversal\nAn object can be traversed and mapped over with the following lenses : \n\n```javascript\nexport const objectTreeLenses = {\n  getLabel: tree =\u003e {\n    if (typeof tree === 'object' \u0026\u0026 !Array.isArray(tree) \u0026\u0026 Object.keys(tree).length === 1) {\n      return tree;\n    }\n    else {\n      throw `getLabel \u003e unexpected object tree value`\n    }\n  },\n  getChildren: tree =\u003e {\n    if (typeof tree === 'object' \u0026\u0026 !Array.isArray(tree) \u0026\u0026 Object.keys(tree).length === 1) {\n      let value = Object.values(tree)[0];\n      if (typeof value === 'object' \u0026\u0026 !Array.isArray(value)) {\n        return Object.keys(value).map(prop =\u003e ({ [prop]: value[prop] }))\n      }\n      else {\n        return []\n      }\n    }\n    else {\n      throw `getChildren \u003e unexpected value`\n    }\n  },\n  constructTree: (label, children) =\u003e {\n    const labelKey = Object.keys(label)[0];\n    return children.length === 0\n      ? label\n      : {\n      [labelKey]: Object.assign.apply(null, children)\n    }\n  },\n};\n```\n\nCf. tests for examples with mapping over object keys and properties and traversing objects. \n\n## Hash-stored tree\nWe mean by hash-stored tree (by lack of a better name) a tree whose content is mapped to its \nlocation path through a hash map. That is the data structure is : `Record {cursor :: Cursor, hash \n:: HashMap\u003cCursor, Tree\u003e}`. The cursor is usually a string which allows to point at a specific \nsubtree. \n\nAn example of such concrete data structure is as follows :\n\n```javascript\n  const hash = {\n    \"0\": \"root\",\n    \"0.0\": \"combinatorName\",\n    \"0.1\": \"componentName\",\n    \"0.2\": \"emits\",\n    \"0.3\": \"id\",\n    \"0.2.0\": \"identifier\",\n    \"0.2.1\": \"notification\",\n    \"0.2.2\": \"type\",\n    \"0.2.1.0\": \"kind\",\n    \"0.2.1.1\": \"value\",\n    \"0.2.1.1.0\": \"key\"\n  };\n  const obj  { cursor : \"0\", hash};\n```\n\nThe corresponding lenses would be as follows :\n\n```javascript\n  const sep = '.';\n\n  function makeChildCursor(parentCursor, childIndex, sep) {\n    return [parentCursor, childIndex].join(sep)\n  }\n\n  const lenses = {\n    getLabel: tree =\u003e {\n      const { cursor, hash } = tree;\n      return { label: hash[cursor], hash, cursor }\n    },\n    getChildren: tree =\u003e {\n      const { cursor, hash } = tree;\n      let childIndex = 0;\n      let children = [];\n\n      while ( makeChildCursor(cursor, childIndex, sep) in hash ) {\n        children.push({ cursor: makeChildCursor(cursor, childIndex, sep), hash })\n        childIndex++;\n      }\n\n      return children\n    },\n    constructTree: (label, children) =\u003e {\n      const { label: value, hash, cursor } = label;\n\n      return {\n        cursor: cursor,\n        hash: merge(\n          children.reduce((acc, child) =\u003e merge(acc, child.hash), {}),\n          { [cursor]: value }\n        )\n      }\n    },\n  };\n```\n\nCf. tests for concrete examples. \n\n## Array-stored trees\nSelf-evident from the lenses definitions, a tree is `label || [label, children]` :\n\n```javascript\nexport const arrayTreeLenses = {\n  getLabel: tree =\u003e {\n    return Array.isArray(tree) ? tree[0] : tree\n  },\n  getChildren: tree =\u003e {\n    return Array.isArray(tree) ? tree[1] : []\n  },\n  constructTree: (label, children) =\u003e {\n    return children \u0026\u0026 Array.isArray(children) ? [label, children] : label\n  },\n}\n```\n\n# Conversion\n**TODO**\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fbrucou%2Ffunctional-rose-tree","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fbrucou%2Ffunctional-rose-tree","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fbrucou%2Ffunctional-rose-tree/lists"}