diff --git a/404.html b/404.html index 5ca5737..a763dd6 100644 --- a/404.html +++ b/404.html @@ -4,7 +4,7 @@ Page Not Found | RefTree - + diff --git a/assets/images/reftree-56916a9f376d970aaefc3069020aee4a.png b/assets/images/reftree-56916a9f376d970aaefc3069020aee4a.png new file mode 100644 index 0000000..cf7dc07 Binary files /dev/null and b/assets/images/reftree-56916a9f376d970aaefc3069020aee4a.png differ diff --git a/assets/images/reftree-db3d1866df4838172ab51dd6b7a592c1.png b/assets/images/reftree-db3d1866df4838172ab51dd6b7a592c1.png deleted file mode 100644 index 5dd1453..0000000 Binary files a/assets/images/reftree-db3d1866df4838172ab51dd6b7a592c1.png and /dev/null differ diff --git a/assets/js/22c5de9a.091aecae.js b/assets/js/22c5de9a.091aecae.js new file mode 100644 index 0000000..c27f8e0 --- /dev/null +++ b/assets/js/22c5de9a.091aecae.js @@ -0,0 +1 @@ +"use strict";(self.webpackChunkreftree=self.webpackChunkreftree||[]).push([[672],{8598:(e,n,i)=>{i.r(n),i.d(n,{assets:()=>d,contentTitle:()=>l,default:()=>g,frontMatter:()=>o,metadata:()=>c,toc:()=>p});var t=i(4848),s=i(8453),a=i(3554),r=i.n(a);const o={sidebar_position:1,description:"A journey deep inside reftree\u2019s animations feature, showing how some of functional programming techniques and concepts can be applied to produce visualizations of themselves."},l="Visualize your data structures!",c={id:"talks/Visualize",title:"Visualize your data structures!",description:"A journey deep inside reftree\u2019s animations feature, showing how some of functional programming techniques and concepts can be applied to produce visualizations of themselves.",source:"@site/../site-gen/target/mdoc/talks/Visualize.md",sourceDirName:"talks",slug:"/talks/Visualize",permalink:"/reftree/docs/talks/Visualize",draft:!1,unlisted:!1,editUrl:"https://github.com/stanch/reftree/tree/main/docs/../site-gen/target/mdoc/talks/Visualize.md",tags:[],version:"current",sidebarPosition:1,frontMatter:{sidebar_position:1,description:"A journey deep inside reftree\u2019s animations feature, showing how some of functional programming techniques and concepts can be applied to produce visualizations of themselves."},sidebar:"mainSidebar",previous:{title:"Unzipping Immutability",permalink:"/reftree/docs/talks/Immutability"}},d={},p=[{value:"Introducing reftree",id:"introducing-reftree",level:2},{value:"Inside reftree",id:"inside-reftree",level:2},{value:"Functional animation",id:"functional-animation",level:2},{value:"Zipping it up",id:"zipping-it-up",level:2}];function h(e){const n={a:"a",code:"code",em:"em",h1:"h1",h2:"h2",img:"img",li:"li",p:"p",pre:"pre",ul:"ul",...(0,s.R)(),...e.components},{Details:a}=n;return a||function(e,n){throw new Error("Expected "+(n?"component":"object")+" `"+e+"` to be defined: you likely forgot to import, pass, or provide it.")}("Details",!0),(0,t.jsxs)(t.Fragment,{children:[(0,t.jsx)(n.h1,{id:"visualize-your-data-structures",children:"Visualize your data structures!"}),"\n",(0,t.jsx)(n.p,{children:"This page contains the materials for my talk \u201cVisualize your data structures!\u201d."}),"\n","\n",(0,t.jsx)(r(),{controls:!0,style:{marginBottom:"1em"},url:"https://www.youtube.com/watch?v=yoayLNPTESk"}),"\n",(0,t.jsxs)(a,{children:[(0,t.jsx)("summary",{children:"Older videos"}),(0,t.jsxs)(n.ul,{children:["\n",(0,t.jsxs)(n.li,{children:["ScalaDays Chicago, April 2017: ",(0,t.jsx)(n.a,{href:"https://www.youtube.com/watch?v=6mWaqGHeg3g",children:"https://www.youtube.com/watch?v=6mWaqGHeg3g"})]}),"\n"]})]}),"\n",(0,t.jsx)(n.p,{children:"You can use this page in two ways:"}),"\n",(0,t.jsxs)(n.ul,{children:["\n",(0,t.jsx)(n.li,{children:"as a reference/refresher on the material covered in the talk;"}),"\n",(0,t.jsx)(n.li,{children:"as an interactive playground where you can try the same commands I presented."}),"\n"]}),"\n",(0,t.jsx)(n.p,{children:"Throughout this page we will assume the following\ndeclarations (each section might add its own):"}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-scala",children:"import reftree.core._\nimport reftree.diagram._\nimport reftree.render._\nimport reftree.geometry._\nimport reftree.svg.animation.Frame\nimport reftree.svg.XmlSvgApi\nimport reftree.svg.XmlSvgApi.svgUnzip\nimport reftree.contrib.XmlInstances._\nimport reftree.contrib.OpticInstances._\nimport reftree.contrib.ZipperInstances._\nimport reftree.contrib.ShapelessInstances._\nimport reftree.util.Optics\nimport reftree.demo.Data\nimport reftree.demo.Shortcuts\nimport scala.collection.immutable._\nimport java.nio.file.Paths\nimport Diagram.{sourceCodeCaption => diagram}\n"})}),"\n",(0,t.jsx)(n.p,{children:"To start an interactive session, just run"}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{children:"$ sbt demo\n@ render(List(1, 2, 3))\n"})}),"\n",(0,t.jsxs)(n.p,{children:["and open ",(0,t.jsx)(n.code,{children:"diagram.png"})," in your favorite image viewer (hopefully one that\nreloads images automatically on file change). You will also need to have\n",(0,t.jsx)(n.a,{href:"http://www.graphviz.org/",children:"GraphViz"})," installed. ",(0,t.jsx)(n.em,{children:"The interactive session\nalready has all the necessary imports in scope."})]}),"\n",(0,t.jsxs)(n.h2,{id:"introducing-reftree",children:["Introducing ",(0,t.jsx)(n.code,{children:"reftree"})]}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-scala",children:'// extra declarations for this section\nval renderer = Renderer(\n renderingOptions = RenderingOptions(density = 100),\n directory = Paths.get(ImagePath, "visualize")\n)\nimport renderer._\n'})}),"\n",(0,t.jsxs)(n.p,{children:[(0,t.jsx)(n.a,{href:"https://stanch.github.io/reftree",children:"reftree"})," is a library for visualizing Scala data structures."]}),"\n",(0,t.jsx)(n.p,{children:"Let\u2019s look at a quick usage example:"}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-scala",children:'case class Person(firstName: String, age: Int)\n\nval bob = Person("Bob", 42)\n// bob: Person = Person(firstName = "Bob", age = 42)\n\ndiagram(bob).render("bob")\n'})}),"\n",(0,t.jsx)(n.p,{children:(0,t.jsx)(n.img,{alt:"bob",src:i(108).A+"",width:"313",height:"363"})}),"\n",(0,t.jsx)(n.p,{children:"That\u2019s it! You can configure the visualization as you like:"}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-scala",children:"// render strings as a single box\nimport reftree.contrib.SimplifiedInstances.string\n"})}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-scala",children:'// rename the firstName field (pun not intended)\nimplicit val personConfig: ToRefTree.DerivationConfig[Person] =\n ToRefTree.DerivationConfig[Person]\n .tweakField("firstName", _.withName("name"))\n\ndiagram(bob).render("bob-simplified")\n'})}),"\n",(0,t.jsx)(n.p,{children:(0,t.jsx)(n.img,{alt:"bob-simplified",src:i(9341).A+"",width:"238",height:"364"})}),"\n",(0,t.jsxs)(n.p,{children:["There are various ways you can use ",(0,t.jsx)(n.code,{children:"reftree"}),":"]}),"\n",(0,t.jsxs)(n.ul,{children:["\n",(0,t.jsx)(n.li,{children:"improving the documentation of your projects;"}),"\n",(0,t.jsx)(n.li,{children:"live-coding demos;"}),"\n",(0,t.jsx)(n.li,{children:"exploring how things work;"}),"\n",(0,t.jsx)(n.li,{children:"anywhere you need diagrams of your Scala data structures."}),"\n"]}),"\n",(0,t.jsxs)(n.p,{children:["(",(0,t.jsx)(n.em,{children:"Incidentally, this talk is an example of all of the above."}),")"]}),"\n",(0,t.jsxs)(n.p,{children:["My previous ",(0,t.jsx)(n.code,{children:"reftree"}),"-powered ",(0,t.jsx)(n.a,{href:"/reftree/docs/talks/Immutability",children:"talk"})," focused on\nimmutable data and various ways it can be manipulated (I do recommend it)."]}),"\n",(0,t.jsxs)(n.p,{children:["Today I would like to take you on a journey deep inside ",(0,t.jsx)(n.code,{children:"reftree"})," itself,\nso that we can see how some of these techniques and concepts can be applied...\nto produce visualizations of themselves \u2014 using one of my favorite ",(0,t.jsx)(n.code,{children:"reftree"}),"\nfeatures: animations."]}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-scala",children:'Animation\n .startWith(Queue(1, 2, 3))\n .repeat(3)(_.iterate(2)(q => q :+ (q.max + 1)).iterate(2)(_.tail))\n .build(Diagram.toStringCaption(_).withAnchor("queue"))\n .render("queue")\n'})}),"\n",(0,t.jsx)(n.p,{children:(0,t.jsx)(n.img,{alt:"queue",src:i(9696).A+"",width:"540",height:"922"})}),"\n",(0,t.jsxs)(n.h2,{id:"inside-reftree",children:["Inside ",(0,t.jsx)(n.code,{children:"reftree"})]}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-scala",children:"// extra declarations for this section\nimport reftree.contrib.SimplifiedInstances.{option, seq, list}\n"})}),"\n",(0,t.jsxs)(n.p,{children:["First, we need to grasp the basics of ",(0,t.jsx)(n.code,{children:"reftree"}),"."]}),"\n",(0,t.jsxs)(n.p,{children:["To visualize a value of some type ",(0,t.jsx)(n.code,{children:"A"}),", ",(0,t.jsx)(n.code,{children:"reftree"})," converts it into a data structure\ncalled ",(0,t.jsx)(n.code,{children:"RefTree"})," (surprise!), using a typeclass ",(0,t.jsx)(n.code,{children:"ToRefTree[A]"}),"."]}),"\n",(0,t.jsxs)(n.p,{children:["For case classes this is done automagically, using\n",(0,t.jsx)(n.a,{href:"https://github.com/milessabin/shapeless/wiki/Feature-overview:-shapeless-2.0.0#generic-representation-of-sealed-families-of-case-classes",children:(0,t.jsx)(n.em,{children:"shapeless"})}),".\n(",(0,t.jsxs)(n.em,{children:["If you are curious about the magic, take a look at ",(0,t.jsx)(n.a,{href:"https://github.com/stanch/reftree/blob/master/core/shared/src/main/scala/reftree/core/GenericInstances.scala",children:"this file"}),"."]}),")\nGiven our friend ",(0,t.jsx)(n.code,{children:"bob"}),", ",(0,t.jsx)(n.em,{children:"shapeless"})," would provide a generic representation,\nwhich includes the field names (at the type level!) and the values (as a heterogeneous list):"]}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-scala",children:'Shortcuts.generic(bob)\n// res2: shapeless.::[String with shapeless.labelled.KeyTag[Symbol with shapeless.tag.Tagged["firstName"], String], shapeless.::[Int with shapeless.labelled.KeyTag[Symbol with shapeless.tag.Tagged["age"], Int], shapeless.HNil]] = "Bob" :: 42 :: HNil\n\ndiagram(Shortcuts.generic(bob)).render("generic")\n'})}),"\n",(0,t.jsx)(n.p,{children:(0,t.jsx)(n.img,{alt:"generic",src:i(2634).A+"",width:"417",height:"460"})}),"\n",(0,t.jsxs)(n.p,{children:["This information is enough to auto-generate a ",(0,t.jsx)(n.code,{children:"RefTree"}),".\nNow, what does it look like? The best way to find out is to visualize a ",(0,t.jsx)(n.code,{children:"RefTree"}),"\nof a ",(0,t.jsx)(n.code,{children:"RefTree"}),"!"]}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-scala",children:'diagram(Shortcuts.refTree(bob)).render("reftree")\n'})}),"\n",(0,t.jsx)(n.p,{children:(0,t.jsx)(n.img,{alt:"reftree",src:i(3342).A+"",width:"1581",height:"836"})}),"\n",(0,t.jsxs)(n.p,{children:["As you can see, it contains values (",(0,t.jsx)(n.code,{children:"Val"}),") and references (",(0,t.jsx)(n.code,{children:"Ref"}),")."]}),"\n",(0,t.jsxs)(n.p,{children:["How do we get from ",(0,t.jsx)(n.code,{children:"RefTree"})," to an image though?\nThis is where ",(0,t.jsx)(n.a,{href:"http://www.graphviz.org/",children:"GraphViz"})," comes in.\nFrom a ",(0,t.jsx)(n.code,{children:"RefTree"})," we can obtain a graph definition that can be rendered by GraphViz:"]}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-scala",children:'Shortcuts.graph(bob).encode\n// res5: String = """digraph "Diagram" {\n// graph [ ranksep=0.8 bgcolor="#ffffff00" ]\n// node [ shape="plaintext" fontname="Source Code Pro" fontcolor="#000000ff" ]\n// edge [ arrowsize=0.7 color="#000000ff" ]\n// "-repl.MdocSession$MdocApp$Person1385763504" [ id="-repl.MdocSession$MdocApp$Person1385763504" label=<
Personnameage
·42
> ] [ color="#104e8bff" fontcolor="#104e8bff" ]\n// "-java.lang.String78928860" [ id="-java.lang.String78928860" label=<
"Bob"
> ] [ color="#104e8bff" fontcolor="#104e8bff" ]\n// "-repl.MdocSession$MdocApp$Person1385763504":"0":"s" -> "-java.lang.String78928860":"n":"n" [ id="-repl.MdocSession$MdocApp$Person1385763504-0-java.lang.String78928860" ] [ color="#104e8bff" ]\n// }"""\n'})}),"\n",(0,t.jsxs)(n.p,{children:["Going even further, we can ask GraphViz for an ",(0,t.jsx)(n.a,{href:"https://en.wikipedia.org/wiki/Scalable_Vector_Graphics",children:"SVG"})," output:"]}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-scala",children:'Shortcuts.svg(bob)\n// res6: xml.Node = Diagram\x3c!-- -repl.MdocSession$MdocApp$Person1385763504 --\x3e-repl.MdocSession$MdocApp$Person1385763504Personnameage\xb742\x3c!-- -java.lang.String78928860 --\x3e-java.lang.String78928860"Bob"\x3c!-- -repl.MdocSession$MdocApp$Person1385763504->-java.lang.String78928860 --\x3e-repl.MdocSession$MdocApp$Person1385763504:s->-java.lang.String78928860:n\n'})}),"\n",(0,t.jsx)(n.p,{children:"At this point you might be guessing how we can use this as a basis for our animation approach.\nEvery state of a data structure will be a separate frame in the SVG format.\nHowever, an animation consisting of these frames alone would be too jumpy.\nWe need to add intermediate frames to smoothly \u201cmorph\u201d one frame into another.\nWith SVG being a vector format, this sounds simple.\nWe just have to individually morph different aspects of the image:"}),"\n",(0,t.jsxs)(n.ul,{children:["\n",(0,t.jsx)(n.li,{children:"graph node positions;"}),"\n",(0,t.jsx)(n.li,{children:"graph edges and their shapes;"}),"\n",(0,t.jsx)(n.li,{children:"colors;"}),"\n",(0,t.jsx)(n.li,{children:"stroke thickness;"}),"\n",(0,t.jsx)(n.li,{children:"transparency."}),"\n"]}),"\n",(0,t.jsx)(n.p,{children:"Ouch! A sane functional approach would definitely help here :)"}),"\n",(0,t.jsx)(n.h2,{id:"functional-animation",children:"Functional animation"}),"\n",(0,t.jsxs)(n.p,{children:["Let\u2019s start by introducing an abstraction for morphing, or, in other words,\ninterpolating things of type ",(0,t.jsx)(n.code,{children:"A"}),":"]}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-scala",children:"trait Interpolation[A] {\n def apply(left: A, right: A, time: Double): A\n def sample(left: A, right: A, n: Int, inclusive: Boolean = true): Seq[A]\n}\n"})}),"\n",(0,t.jsxs)(n.p,{children:["(",(0,t.jsxs)(n.em,{children:["If you are curious, ",(0,t.jsx)(n.a,{href:"https://github.com/stanch/reftree/blob/master/core/shared/src/main/scala/reftree/geometry/Interpolation.scala",children:"here is the actual implementation"}),"."]}),")"]}),"\n",(0,t.jsxs)(n.p,{children:["Once we have an instance of ",(0,t.jsx)(n.code,{children:"Interpolation[xml.Node]"}),", we can generate\nas many intermediate frames as we want! But how do we construct this instance?"]}),"\n",(0,t.jsxs)(n.p,{children:["Consider a lowly floating point number (it can represent an ",(0,t.jsx)(n.em,{children:"x"})," coordinate of some element in our SVG, for example).\nThere is an obvious way to implement ",(0,t.jsx)(n.code,{children:"Interpolation[Double]"}),", which ",(0,t.jsx)(n.code,{children:"reftree"})," already defines as ",(0,t.jsx)(n.code,{children:"Interpolation.double"}),":"]}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-scala",children:'val numbers = Interpolation.double.sample(0, 10, 5).toList\n// numbers: List[Double] = List(0.0, 2.5, 5.0, 7.5, 10.0)\n\ndiagram(numbers).render("numbers")\n'})}),"\n",(0,t.jsx)(n.p,{children:(0,t.jsx)(n.img,{alt:"numbers",src:i(4447).A+"",width:"376",height:"193"})}),"\n",(0,t.jsx)(n.p,{children:"Now if you think about a point in 2D space, it\u2019s just two numbers joined together:"}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-scala",children:'val point = Point(0, 10)\n// point: Point = Point(x = 0.0, y = 10.0)\n\ndiagram(point).render("point")\n'})}),"\n",(0,t.jsx)(n.p,{children:(0,t.jsx)(n.img,{alt:"point",src:i(4967).A+"",width:"226",height:"231"})}),"\n",(0,t.jsx)(n.p,{children:"Can we use the number interpolation to interpolate these two numbers?\nTo answer this question, let\u2019s introduce more abstraction\n(in a great tradition of functional programming)."}),"\n",(0,t.jsxs)(n.p,{children:["A lens ",(0,t.jsx)(n.code,{children:"Lens[A, B]"})," is something that can \u201cfocus\u201d on a piece of data of type ",(0,t.jsx)(n.code,{children:"B"}),"\ninside a data structure of type ",(0,t.jsx)(n.code,{children:"A"})," and provide read-write access to it.\nWe will use the excellent ",(0,t.jsxs)(n.a,{href:"https://github.com/julien-truffaut/Monocle",children:[(0,t.jsx)(n.em,{children:"Monocle"})," library"]}),"\nto create lenses and other optics along the way:"]}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-scala",children:'import monocle.macros.GenLens\n\nval x = GenLens[Point](_.x)\n// x: monocle.package.Lens[Point, Double] = repl.MdocSession$MdocApp$$anon$1@6f44eadb\nval y = GenLens[Point](_.y)\n// y: monocle.package.Lens[Point, Double] = repl.MdocSession$MdocApp$$anon$2@f82e5b8\n\n(diagram(OpticFocus(x, point)).toNamespace("x") +\n diagram(OpticFocus(y, point)).toNamespace("y")).render("x+y")\n'})}),"\n",(0,t.jsx)(n.p,{children:(0,t.jsx)(n.img,{alt:"x+y",src:i(9221).A+"",width:"580",height:"231"})}),"\n",(0,t.jsx)(n.p,{children:"Lenses provide several methods to manipulate data:"}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-scala",children:"x.get(point)\n// res10: Double = 0.0\ny.set(20)(point)\n// res11: Point = Point(x = 0.0, y = 20.0)\ny.modify(_ + 20)(point)\n// res12: Point = Point(x = 0.0, y = 30.0)\n"})}),"\n",(0,t.jsxs)(n.p,{children:["If we can read and write each coordinate field, we can interpolate them separately\nand update the point field by field.\nWe do this by piping ",(0,t.jsx)(n.code,{children:"Interpolation.double"})," through ",(0,t.jsx)(n.code,{children:"x"})," and ",(0,t.jsx)(n.code,{children:"y"})," lenses\nand combining the resulting interpolations:"]}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-scala",children:'val pointInterpolation = (\n x.interpolateWith(Interpolation.double) +\n y.interpolateWith(Interpolation.double))\n// pointInterpolation: Interpolation[Point] = reftree.geometry.Interpolation$$anonfun$apply$4@2c57a744\n\nval points = pointInterpolation.sample(Point(0, 0), Point(10, 20), 5).toList\n// points: List[Point] = List(\n// Point(x = 0.0, y = 0.0),\n// Point(x = 2.5, y = 5.0),\n// Point(x = 5.0, y = 10.0),\n// Point(x = 7.5, y = 15.0),\n// Point(x = 10.0, y = 20.0)\n// )\n\ndiagram(points).render("points")\n'})}),"\n",(0,t.jsx)(n.p,{children:(0,t.jsx)(n.img,{alt:"points",src:i(7692).A+"",width:"1176",height:"363"})}),"\n",(0,t.jsxs)(n.p,{children:["Of course, ",(0,t.jsx)(n.code,{children:"reftree"})," already defines this as ",(0,t.jsx)(n.code,{children:"Point.interpolation"}),"."]}),"\n",(0,t.jsx)(n.p,{children:"Using the same approach, we can build a polyline interpolator\n(assuming the polylines being interpolated consist of equal number of points):"}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-scala",children:'Data.polyline1\n// res14: Polyline = Polyline(\n// points = List(Point(x = 0.0, y = 10.0), Point(x = 10.0, y = 20.0))\n// )\nData.polyline2\n// res15: Polyline = Polyline(\n// points = List(Point(x = 20.0, y = 30.0), Point(x = 40.0, y = 50.0))\n// )\n\nval polylineInterpolation = (GenLens[Polyline](_.points)\n .interpolateEachWith(Point.interpolation))\n// polylineInterpolation: Interpolation[Polyline] = reftree.geometry.Interpolation$$anonfun$apply$4@1e08a7e1\n\nval polylines = polylineInterpolation.sample(Data.polyline1, Data.polyline2, 3).toList\n// polylines: List[Polyline] = List(\n// Polyline(points = List(Point(x = 0.0, y = 10.0), Point(x = 10.0, y = 20.0))),\n// Polyline(points = List(Point(x = 10.0, y = 20.0), Point(x = 25.0, y = 35.0))),\n// Polyline(points = List(Point(x = 20.0, y = 30.0), Point(x = 40.0, y = 50.0)))\n// )\n\ndiagram(polylines).render("polylines")\n'})}),"\n",(0,t.jsx)(n.p,{children:(0,t.jsx)(n.img,{alt:"polylines",src:i(5846).A+"",width:"1491",height:"664"})}),"\n",(0,t.jsxs)(n.p,{children:["We are finally ready to implement our first substantial interpolator: one that morphs graph edges.\n",(0,t.jsxs)(n.em,{children:["The following approach is inspired by Mike Bostock\u2019s ",(0,t.jsx)(n.a,{href:"https://bl.ocks.org/mbostock/3916621",children:"path tween"}),",\nhowever ",(0,t.jsx)(n.code,{children:"reftree"})," puts more emphasis on types and even includes its own\n",(0,t.jsx)(n.a,{href:"https://github.com/stanch/reftree/blob/master/core/shared/src/main/scala/reftree/geometry/Path.scala",children:"SVG path parser and simplification algorithm"}),"."]})]}),"\n",(0,t.jsx)(n.p,{children:"The resulting animation should look like this:"}),"\n",(0,t.jsx)(n.p,{children:(0,t.jsx)(n.img,{alt:"edges-100",src:i(8914).A+"",width:"361",height:"194"})}),"\n",(0,t.jsxs)(n.p,{children:["An edge is drawn with an ",(0,t.jsx)(n.a,{href:"https://developer.mozilla.org/en-US/docs/Web/SVG/Tutorial/Paths",children:"SVG path"}),",\nwhich consists of several commands, e.g. \u201cmove to\u201d, \u201cline to\u201d, \u201cbezier curve to\u201d.\nHere is a minimized SVG snippet for an actual edge:"]}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-scala",children:'Data.edge1\n// res17: xml.Node = \n\ndiagram(Data.edge1).render("edge")\n'})}),"\n",(0,t.jsx)(n.p,{children:(0,t.jsx)(n.img,{alt:"edge",src:i(1842).A+"",width:"1365",height:"703"})}),"\n",(0,t.jsxs)(n.p,{children:["As you can see, the commands themselves are given in the ",(0,t.jsx)(n.code,{children:"d"})," attribute inside the ",(0,t.jsx)(n.code,{children:"path"})," element\nin a rather obscure format. Luckily, we have lenses and other optics at our disposal\nto plumb through this mess."]}),"\n",(0,t.jsxs)(n.p,{children:["First, let\u2019s get to the ",(0,t.jsx)(n.code,{children:"path"})," element. ",(0,t.jsx)(n.code,{children:"reftree"})," implements a few things that will help us:"]}),"\n",(0,t.jsxs)(n.ul,{children:["\n",(0,t.jsxs)(n.li,{children:[(0,t.jsx)(n.code,{children:"XmlSvgApi"}),", an implementation of several useful SVG operations for ",(0,t.jsx)(n.em,{children:"scala-xml"}),".\nIn particular, if offers a CSS selector-like method for matching elements of certain type and/or class."]}),"\n",(0,t.jsxs)(n.li,{children:["An optic that focuses on an element deep inside XML or any other recursive data structure: ",(0,t.jsx)(n.code,{children:"Optics.collectFirst"}),".\nIt is actually an ",(0,t.jsx)(n.code,{children:"Optional"}),", not a ",(0,t.jsx)(n.code,{children:"Lens"}),", since the element might be missing."]}),"\n"]}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-scala",children:'val edgePathElement = Optics.collectFirst(XmlSvgApi.select("path"))\n// edgePathElement: monocle.package.Optional[xml.Node, xml.Node] = monocle.Optional$$anon$6@56d284c9\n\ndiagram(OpticFocus(edgePathElement, Data.edge1)).render("edgePathElement")\n'})}),"\n",(0,t.jsx)(n.p,{children:(0,t.jsx)(n.img,{alt:"edgePathElement",src:i(3135).A+"",width:"1408",height:"703"})}),"\n",(0,t.jsxs)(n.p,{children:["Next, we need to \u201cdescend\u201d to the ",(0,t.jsx)(n.code,{children:"d"})," attribute. Here is where optics really shine:\nwe can compose ",(0,t.jsx)(n.code,{children:"Optional[A, B]"})," with ",(0,t.jsx)(n.code,{children:"Optional[B, C]"})," to get an ",(0,t.jsx)(n.code,{children:"Optional[A, C]"}),":"]}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-scala",children:'val d = XmlSvgApi.attr("d")\n// d: monocle.package.Optional[xml.Node, String] = monocle.POptional$$anon$1@4ee5148e\nval edgePathString = edgePathElement composeOptional d\n// edgePathString: monocle.POptional[xml.Node, xml.Node, String, String] = monocle.POptional$$anon$1@5f99f791\n\ndiagram(OpticFocus(edgePathString, Data.edge1)).render("edgePathString")\n'})}),"\n",(0,t.jsx)(n.p,{children:(0,t.jsx)(n.img,{alt:"edgePathString",src:i(8908).A+"",width:"1403",height:"703"})}),"\n",(0,t.jsx)(n.p,{children:"Next, we will use an isomorphism, another kind of optic, to view\nthe string as a nice case class:"}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-scala",children:'Path.stringIso\n// res21: monocle.package.Iso[String, Path] = monocle.PIso$$anon$9@4f5642c\n\nval edgePath = edgePathString composeIso Path.stringIso\n// edgePath: monocle.POptional[xml.Node, xml.Node, Path, Path] = monocle.POptional$$anon$1@46435cb7\n\ndiagram(edgePath.getOption(Data.edge1)).render("edgePath")\n'})}),"\n",(0,t.jsx)(n.p,{children:(0,t.jsx)(n.img,{alt:"edgePath",src:i(3959).A+"",width:"1435",height:"701"})}),"\n",(0,t.jsxs)(n.p,{children:["And finally, another isomorphism takes us from a ",(0,t.jsx)(n.code,{children:"Path"})," to its sampled representation\nas a ",(0,t.jsx)(n.code,{children:"Polyline"}),". (",(0,t.jsx)(n.em,{children:"Purists will say that this is not really an isomorphism because\nit\u2019s not reversible, but with a lot of points you can get pretty close ;)"}),")"]}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-scala",children:'Path.polylineIso(points = 4)\n// res23: monocle.package.Iso[Path, Polyline] = monocle.PIso$$anon$9@20647195\n\ndef edgePolyline(points: Int) = edgePath composeIso Path.polylineIso(points)\n\ndiagram(edgePolyline(4).getOption(Data.edge1)).render("edgePolyline")\n'})}),"\n",(0,t.jsx)(n.p,{children:(0,t.jsx)(n.img,{alt:"edgePolyline",src:i(7274).A+"",width:"1727",height:"532"})}),"\n",(0,t.jsx)(n.p,{children:"Let\u2019s interpolate!"}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-scala",children:'def edgeInterpolation(points: Int) = edgePolyline(points).interpolateWith(Polyline.interpolation)\n\ndef edges(points: Int, frames: Int) = (Data.edge1 +:\n edgeInterpolation(points).sample(Data.edge1, Data.edge2, frames, inclusive = false) :+\n Data.edge2)\n\nAnimatedGifRenderer.renderFrames(\n edges(4, 4).map(Frame(_)),\n Paths.get(ImagePath, "visualize", "edges-4.gif"),\n RenderingOptions(density = 200),\n AnimationOptions(framesPerSecond = 1)\n)\n\nAnimatedGifRenderer.renderFrames(\n edges(100, 32).map(Frame(_)),\n Paths.get(ImagePath, "visualize", "edges-100.gif"),\n RenderingOptions(density = 200),\n AnimationOptions(framesPerSecond = 8)\n)\n'})}),"\n",(0,t.jsx)(n.p,{children:"With 4 points and 4 frames:"}),"\n",(0,t.jsx)(n.p,{children:(0,t.jsx)(n.img,{alt:"edges-4",src:i(5047).A+"",width:"361",height:"194"})}),"\n",(0,t.jsx)(n.p,{children:"With 100 points and 32 frames:"}),"\n",(0,t.jsx)(n.p,{children:(0,t.jsx)(n.img,{alt:"edges-100",src:i(8914).A+"",width:"361",height:"194"})}),"\n",(0,t.jsx)(n.p,{children:(0,t.jsxs)(n.em,{children:["Interpolating the entire image is left as an exercise for the reader,\nalthough the impatient will find the complete implementation\n",(0,t.jsx)(n.a,{href:"https://github.com/stanch/reftree/blob/master/core/shared/src/main/scala/reftree/svg/animation/GraphInterpolation.scala",children:"here"}),"."]})}),"\n",(0,t.jsxs)(n.p,{children:["Notice that we never touched XML directly.\nIn fact, equipped with the same set of optics for another format or representation,\nwe would be able to operate on it without changing the code too much.\nCase in point: ",(0,t.jsx)(n.code,{children:"reftree"})," supports both\n",(0,t.jsx)(n.a,{href:"https://github.com/stanch/reftree/blob/master/core/jvm/src/main/scala/reftree/svg/XmlSvgApi.scala",children:(0,t.jsx)(n.em,{children:"scala-xml"})})," and\n",(0,t.jsx)(n.a,{href:"https://github.com/stanch/reftree/blob/master/core/js/src/main/scala/reftree/svg/DomSvgApi.scala",children:(0,t.jsx)(n.em,{children:"scala-js-dom"})})," (for Scala.js),\nwith only 50 lines of implementation-specific code for each backend.\nThis goes to show the flexibility and usefulness of optics."]}),"\n",(0,t.jsx)(n.h2,{id:"zipping-it-up",children:"Zipping it up"}),"\n",(0,t.jsxs)(n.p,{children:["In the previous section we saw ",(0,t.jsx)(n.code,{children:"Optics.collectFirst"})," \u2014 an optic that is able to perform\nmodifications deep inside SVG. How do we go about implementing something like this,\nor, more generally, how do we edit recursive data structures such as XML?"]}),"\n",(0,t.jsxs)(n.p,{children:["This solution is called a \u201cZipper\u201d, and was introduced by G\xe9rard Huet in 1997.\nIt consists of a \u201ccursor\u201d pointing to a location anywhere in a tree \u2014 \u201ccurrent focus\u201d.\nThe cursor can be moved freely with operations like ",(0,t.jsx)(n.code,{children:"moveDownLeft"}),", ",(0,t.jsx)(n.code,{children:"moveRight"}),", ",(0,t.jsx)(n.code,{children:"moveUp"}),", etc.\nCurrent focus can be updated, deleted, or new nodes can be inserted to its left or right.\nZippers are immutable, and every operation returns a new Zipper.\nAll the changes made to the tree can be committed, yielding a new modified version of the original tree."]}),"\n",(0,t.jsxs)(n.p,{children:["My ",(0,t.jsx)(n.a,{href:"https://github.com/stanch/zipper#zipper--an-implementation-of-huets-zipper",children:"zipper library"}),"\nprovides a few useful movements and operations. Just like optics, it\u2019s rather generic and flexible.\nThe zipper can operate on any type, as long as an instance of the ",(0,t.jsx)(n.code,{children:"Unzip"})," typeclass is available,\nwhich can be automatically derived in many cases.\n(",(0,t.jsxs)(n.em,{children:["Note that the derivation of ",(0,t.jsx)(n.code,{children:"Unzip"})," for SVG can be found\n",(0,t.jsx)(n.a,{href:"https://github.com/stanch/reftree/blob/master/core/shared/src/main/scala/reftree/svg/api/BaseSvgApi.scala",children:"here"}),"."]}),")"]}),"\n",(0,t.jsx)(n.p,{children:"Consider a simple XML tree:"}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-scala",children:'Data.simpleXml\n// res27: xml.Node = \n\ndiagram(Data.simpleXml).render("simpleXml")\n'})}),"\n",(0,t.jsx)(n.p,{children:(0,t.jsx)(n.img,{alt:"simpleXml",src:i(4792).A+"",width:"1277",height:"703"})}),"\n",(0,t.jsx)(n.p,{children:"When we wrap a Zipper around this tree, it does not look very interesting yet:"}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-scala",children:'import zipper.Zipper\n\nval zipper1 = Zipper(Data.simpleXml)\n// zipper1: Zipper[xml.Node] = Zipper(List(),,List(),None)\n\n(diagram(Data.simpleXml) + diagram(zipper1)).render("zipper1")\n'})}),"\n",(0,t.jsx)(n.p,{children:(0,t.jsx)(n.img,{alt:"zipper1",src:i(3618).A+"",width:"1321",height:"872"})}),"\n",(0,t.jsx)(n.p,{children:"We can see that it just points to the original tree.\nIn this case the focus is the root of the tree, which has no siblings,\nand the parent zipper does not exist, since we are at the top level."}),"\n",(0,t.jsx)(n.p,{children:"To move down the tree, we \u201cunzip\u201d it, separating the child nodes into\nthe focused node and its left and right siblings:"}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-scala",children:'val zipper2 = zipper1.moveDownLeft\n// zipper2: Zipper[xml.Node] = Zipper(List(),,List(, , ),Some(Zipper(List(),,List(),None)))\n\n(diagram(zipper1) + diagram(zipper2)).render("zipper1+2")\n'})}),"\n",(0,t.jsx)(n.p,{children:(0,t.jsx)(n.img,{alt:"zipper1+2",src:i(2385).A+"",width:"1366",height:"1042"})}),"\n",(0,t.jsx)(n.p,{children:"The new Zipper links to the old one,\nwhich will allow us to return to the root of the tree when we are done applying changes.\nThis link however prevents us from seeing the picture clearly.\nLet\u2019s look at the second zipper alone:"}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-scala",children:'diagram(zipper2).render("zipper2b")\n'})}),"\n",(0,t.jsx)(n.p,{children:(0,t.jsx)(n.img,{alt:"zipper2b",src:i(5823).A+"",width:"1178",height:"872"})}),"\n",(0,t.jsxs)(n.p,{children:["Great! We have ",(0,t.jsx)(n.code,{children:"2"})," in focus and ",(0,t.jsx)(n.code,{children:"3, 4, 5"})," as right siblings. What happens if we move right a bit?"]}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-scala",children:'val zipper3 = zipper2.moveRightBy(2)\n// zipper3: Zipper[xml.Node] = Zipper(List(, ),,List(),Some(Zipper(List(),,List(),None)))\n\ndiagram(zipper3).render("zipper3")\n'})}),"\n",(0,t.jsx)(n.p,{children:(0,t.jsx)(n.img,{alt:"zipper3",src:i(1248).A+"",width:"1113",height:"872"})}),"\n",(0,t.jsx)(n.p,{children:"This is interesting! Notice that the left siblings are \u201cinverted\u201d.\nThis allows to move left and right in constant time, because the sibling\nadjacent to the focus is always at the head of the list."}),"\n",(0,t.jsx)(n.p,{children:"This also allows us to insert new siblings easily:"}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-scala",children:'val zipper4 = zipper3.insertLeft()\n// zipper4: Zipper[xml.Node] = Zipper(List(, , ),,List(),Some(Zipper(List(),,List(),None)))\n\ndiagram(zipper4).render("zipper4")\n'})}),"\n",(0,t.jsx)(n.p,{children:(0,t.jsx)(n.img,{alt:"zipper4",src:i(4095).A+"",width:"1235",height:"872"})}),"\n",(0,t.jsx)(n.p,{children:"And, as you might know, we can delete nodes and update the focus:"}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-scala",children:'val zipper5 = zipper4.deleteAndMoveRight.set()\n// zipper5: Zipper[xml.Node] = Zipper(List(, , ),,List(),Some(Zipper(List(),,List(),None)))\n\ndiagram(zipper5).render("zipper5")\n'})}),"\n",(0,t.jsx)(n.p,{children:(0,t.jsx)(n.img,{alt:"zipper5",src:i(5078).A+"",width:"556",height:"667"})}),"\n",(0,t.jsx)(n.p,{children:"Finally, when we move up, the siblings at the current level are \u201czipped\u201d\ntogether and their parent node is updated:"}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-scala",children:'val zipper6 = zipper5.moveUp\n// zipper6: Zipper[xml.Node] = Zipper(List(),,List(),None)\n\ndiagram(zipper6).render("zipper6")\n'})}),"\n",(0,t.jsx)(n.p,{children:(0,t.jsx)(n.img,{alt:"zipper6",src:i(6317).A+"",width:"794",height:"703"})}),"\n",(0,t.jsxs)(n.p,{children:["When we are done editing, the ",(0,t.jsx)(n.code,{children:".commit"})," shorthand can be used for going\nall the way up (applying all the changes) and returning the focus.\nNotice how all the unchanged nodes are shared between the old and the new XML."]}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-scala",children:'val notSoSimpleXml = zipper6.commit\n// notSoSimpleXml: xml.Node = \n\n(diagram(Data.simpleXml) + diagram(notSoSimpleXml)).render("notSoSimpleXml")\n'})}),"\n",(0,t.jsx)(n.p,{children:(0,t.jsx)(n.img,{alt:"notSoSimpleXml",src:i(4259).A+"",width:"1560",height:"703"})}),"\n",(0,t.jsx)(n.p,{children:(0,t.jsxs)(n.em,{children:["Using an XML zipper, a determined reader can easily implement advanced lenses,\nsuch as ",(0,t.jsx)(n.code,{children:"Optics.collectFirst"}),", ",(0,t.jsx)(n.code,{children:"Optics.collectLeftByKey"}),", etc, all found\n",(0,t.jsx)(n.a,{href:"https://github.com/stanch/reftree/blob/master/core/shared/src/main/scala/reftree/util/Optics.scala",children:"here"}),"."]})}),"\n",(0,t.jsx)(n.p,{children:"To conclude, here is an animation of a zipper and the tree it operates on\n(from my previous talk), produced (as we know now) not without zippers\u2019 help:"}),"\n",(0,t.jsx)(n.p,{children:(0,t.jsx)(n.img,{alt:"tree+zipper",src:i(1864).A+"",width:"1241",height:"690"})}),"\n",(0,t.jsxs)(n.p,{children:["That\u2019s all! Thank you for reading this far.\nI hope you are leaving this page with some great ",(0,t.jsx)(n.code,{children:"reftree"})," use-cases in mind :)"]})]})}function g(e={}){const{wrapper:n}={...(0,s.R)(),...e.components};return n?(0,t.jsx)(n,{...e,children:(0,t.jsx)(h,{...e})}):h(e)}},9696:(e,n,i)=>{i.d(n,{A:()=>t});const t=i.p+"assets/images/queue-668a708310e8e78475fbc9b757c69eb3.gif"},1864:(e,n,i)=>{i.d(n,{A:()=>t});const t=i.p+"assets/images/tree+zipper-b1cf73da287eda26f64b667ce8dcf311.gif"},9341:(e,n,i)=>{i.d(n,{A:()=>t});const t="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAO4AAAFsCAYAAADL4YVFAAAABmJLR0QA/wD/AP+gvaeTAAAgAElEQVR4nO3de3wcdb3/8ddndpOUXpJNaQERBUXxggVKEUEEerweCtm0HOMFPUc9hxaVI1qahIuiqwhINi0XQaSgB0QuNkjbBOhBRKvgTzlIQUFRBEWs0NJLdpO2NE12Pr8/mpRtmtI22WR3mvfz8fABzHznO5+p++53dnbmOyAiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiJSwvY76cxb9z9p9lNR61tkqIJiFzAkFkxz/OHI9S0yRJEN7uTpnx+P+5uxoODhGs6+RQohssHFt0wFgjCk8OEazr5FCiBe7AIGK3A72qEzV8Gzk0+cc4kFfqo5rw+NX8Us9oVVy7/7XP9tJp045zWB+VeBE4CDgT+62YI1v1jYMtS+RUZSZEdcd6YB68q3+G/Mwtc6dgluF5tzchjm7qGuLpbffr8TZ38osPAJ4Fh3W4jxacyeMfdF+508+/ND6VtkpEV2xMWYBlS789GXHrzxvr7Fk0+c023m3z5gTdWJq2A5wH7vO3N/urkNs59Vjh3zyWeWfburt/ni/U6ePcacbx5+eN2Nf/hDy5Y97VukGCI54h44bc5Y4C3mnnrpwRvuy18XxHLLATzk0L5l3sM3Acri5Z/NC+3WdaH/yKF6XSLx5sH0LVIMkQxuzzg/Cog5/kj/dSFBHMA9WNe3zNymY7T+84Fr1/VvbwFZAIJw3GD6FimGSAbX3Y4GQttny+P91wWhvR0gDHuegt6fduBQQv/jQH1ZGLwDyAWbYk/uad8ixRLJ4Frg0zCeWv2TWzb2Xxe6z8Z5du2Bnc8ArOns6QJyBEzYsadU4Ob/ATzywqMLN+1p3yLFEsng4hyNEx50/Nx98hfvd+Lsz5ox3fHzaWnJAfDowm7g97j9W/+rwZNP+ufXgSmBcc6g+hYpEit2AXvqkOmfHrMpLOsENuM8HgT21TD0iQQ2E/dP4ix46cEb5uVvs//02e/3kPtwfob5dZiNM+djDu/DbO5Lv1h43WD7FimGyI24m3OxI4BYLrB3Yrwcuj9gxg3mXu1mHxkoWKuX3/BTN5/uEDfsRtxS7mRxP7YvtIPtW0QGoeo9n6vuvQAVqb5FRERERERERERERERERERERERERERERERERERERERERERERERERF7NqHzr3H4nz/782DccU7HpuUefL3YtIoMRuelZC8K5wELeX+wyRAZrdAZXJOIUXJEIUnBFIkjBFYkgBVckghRckQhScEUiSMEViSAFVySCFFyRCFJwRSJIwRWJIAVXJIIUXJEIUnBFIkjBFYkgBVckghRckQhScEUiSMEViSAFVySCFFyRCFJwRSJIwRWJIAVXJIIUXJEIUnBFIkjBFYkgBVckghRckQhScEUiSMEViSAFVySCFFyRCFJwRSJIwRWJIAVXJIIUXJEIUnBFIkjBFYkgBVckghRckQhScEUiSMEViSAFVySCFFyRCFJwRSJIwRWJIAVXJIIUXJEIUnBFIkjBFYkgBVckghRckQhScEUiSMEViSAFVySCFFyRCFJwRSJIwRWJIAVXJIIUXJEIUnBFIkjBFYkgBVckghRckQhScEUiSMEViSAFVySCFFyRCFJwRSJIwRWJIAVXJIIUXJEIsmIXMBL2O3H2v7lxNr71LyqDN2De4W7rAMx8iwflp69Z/p0Nxa1UZPfEi13ASPBY+DsLg3/Z/q8pwwzA3bGHFFqJklFxqrxm+feecfwxnHCg9YFxy0jXJDIUoyK4AIEHt7j1+2bgABbG4+V3FaMmkcEaNcHNwR3mvv1Co8eNZf984Np1xalKZHBGTXDXPrjwRTceBHJ5i+NByK3FqklksEZNcAECtx+y/TFvDmNldxerHpHBGlXB7YptudOhB8CxHtwX62qyRNGoCm5m+U0ZsP/FCQ2PW+C3F7smkcEYVcEFCAhvxQgcOiat7biv2PWIDMaI3zk1seZbh7vFkiGcZPAOYH+gbMQKcKe84xlyZZXkxu4/YruVvUoWeMZgReh+3zg2LXuhLbVpJAsYseBW1zbNcLcvA+8GOnGWe+BPBM5KzLKhe/dI1VLe8fwXcuXjH8iNmfjHkdrnYJnbFVj4uGM3F7uWYjDsZvAWh5K4iGgwASxh8FbH3g1+OJAB/15YEb+0o+Xc9SNUx/CakLz8LYEHC8yYgbMc7KrMmA330pLaMtz73plJ7zlz2tqHDnoMUgPeSVVKqmrSfzT8fzNtjecWu5ZiSCTTHUAq09qwoNi1DKTq1CveSKznTIPPAz2GX9RecchCWj6S2+XGQzCs9ypX1zad7W4LMP6BMzPT1rB0OPe3u9Y+dOOjxa5B9g7Ze+b+Fbhw/Kz0lfEev8TNrqnu+vunYqdfMWvtXXNfHK79Ds/FqbpFsURt89Xudg3u12a6Kw4vldCKDIcNixteyrQ1zg6D8HiHA3t6eh5OnDb/yOHaX+GDW7coltjy3J24f9axszJtjeey7Jyugu9HpAR1LDnv/+Lx+LuAVQThQ9W1TScOx34KHtzEluevwO1Uc6vNttYvLHT/IqVu7V1zXxzrG6cD/+dud1XNbDq00PsoaHCrapvPxP0Lhn+xva1+WSH7FomSF9pSm4Luillgqwnt7qpTL6suZP8FC+6E5OVvMfdrcb+ivbXxukL1KxJV65ed0xESzDRnP4uVNRey74IFNyC4AnxlpmfMBYXqUyTqOlrPfcYDLgT/dOXMy48tVL8FCW51bdMMg1Nw04UokX6y5QffCPzOPLgSvCD3ThQkuCH2FZzl+slHZAAtH8lBOM+c46tOW/DeQnQ55OBWn5p+hznHg11ViIJE9kaZ1vN+jvEkQXhmIfob+ogbeBLYkOkp11VkkVfj9iODGdSlyofa1ZCDG5qdCLZc321FXl3o3AdUVm4eP+Q7qoZ8r7LBO9x9VD65UmqqatLfDALe2b604UNVNU0fNLPzgGOADWBNmdb6Hb7OVNYtmBhs7rkcowYs7vCIG6nAme342Gxr4xkTa+YfF1r4y8CCQ0PPXQN2PM6lZTm/oztu3wVOwP07mbbGr23XeV2qPNE1vhH8X4Ej3FlpZrdlDphwOQvPGpanwXbneLb7Mzv1smqLxb4GvBfsQIdH3O2rZuF/AlXbtR/i8VSOCZ7c0JXLBRZOAR4ZynEW4uLU/oH5PwvQjwyRGUe582IimU5hdhVmdzvW4NABfkVVTfoN+e0nzJp/WNCVexizfwW7FmO2OU+bsxz4CPA4gJsfBWzJeW4RFizH7W8YjT1xW4LZL4C/YzY3/4ppYkbzwYmusb/F/EzDbzX8lCDwa8HrE6s75w/H8e/u8fSpTC54k8XivwGrg2AR2JzA+Xtg/qBhZ+S3L8TxrGw592VgvWFDfhB8aCNuXaqcLsrc2DjUQqQgpgIJx3+Qrdh0ZN+jk9U1zZ1ufhsevBH429ambrFc8x3AljjhyWtbz3uht4/Fidr0szhX4b0fXPejMAzn7Exr/Yqq2qZJ5lZvHpze3jrvN4lk+iDgMDDv65t48/fBQu/pmZq554L23r5/lahpCoArDqpbcF7vB7lA9uB4ehk9t4AFgQXHrV867x+9i+9KJJtXgl/8SvtCHo93hsb4oR7t0IK7YaJR1gVh4LtuLMNp/CmXTgYONHxhprXxc/nrwoBucwhi9lLfsupk8yccpoLlf8gBMGe9A7m4bf3gGkfhPJBta1wBEHjwZscfWt827zdbt/A3Ofyhb/tEcv4s4L1hEL6ro3Xbh7y3q+Axx2ObNm95I3nbDNUeHQ9QVdv8YXM/zsxPzQvt1vbmf3V/pX1hj8ccH/pz8KPi3UGjQSwenwpgFtvheoM5bwVy+4Qdz/Z96hw+DDyWaa3/Zf/2jh8D9sKGxQ0vkUoFvoIp4M356834cd4ejsZozevhBIAgDB5OJNP9+wagy8asHeShDmi3j6evYvda8L+2L228d4f2oR+D5bcf+ePZFQV3b2EcBYRlL4e/23GlTwX7U795kQ4H/t9OOnsn8BjAvo+OPyxnPpZg6+jTe7P8G3AehW0j/UGErMjb3xSwXwae+/yA3Qdx37j0S6v38Ah3ZbeOJ8/bMX4/cHM7xrc7rS7K8bwqBXfvcZTD06t/0jDA9QY7GvMH+y08EKxzh6bTU3FgqjsLAMKtF6bAerZ+8OPxaTj0xIIVkD/SB9uCawQZxw9b33Z+wU6Fd8NuHU+eAzy0P+3QPpUKWMFUAr+6b1GRjudVjbrpWfdeNhW8/6jSO0L6IduPiAA8B358/oLJddeOT0wYdxewT98I2zuSZzJLLngOIMCPATZ0Htn5DEAQcDSwJVPRmfeh9seAKYmZTUf1ryeRbD5p0If46p7breN5xd/MOJa6RbFtS+pS5YkV4/4HGA9B3p9lUY7nVWnE3QscWJMauwkOc+z7O6wMto6ImPUP7veA+Yma9Pct8DvD0KZ0b9n0SYy/bG0fbv3gOkflX411t2nAY6R6J9pzmwo8mT/5X4zwmh6Csz20ZVW1zRfFQnsyJPdWsNPB31tVk56SbWv4WwH/CHb/eLaxG8Bvqu56/naS6ZsdexNd/in63nThwbZjLtLxvCqNuHuBDcG4KUCA7fA9DjObCnjQXb7diJOpOPgq4KsY093tBxb4ceb+WTe7DejMLmn4K4DjR2Hb/YwyDXzbXwIOR9Pv++Pa1vM644THGvzU3C8KLbwPs9kYf4nH428ejg/57h7Ptvat834A9iXHj3K4FcJTMS7GuQ3o7Gid+2wxj2d4nXJ1RSKZ9kRN878Xu5S9VVVN+o+JmqaSnJp0JCSS6Y5EMj1iU9MmkukliWT6gWHs/5mqZPrSofajEVdGIbftvtv2qprZPA04zYzvFaGoPaLvuDLqVCebz/YuvkgyfV1o9geAwMP3EPo5wOL27MZFRS5xlxRcGXXiFWNv6u7aVO14XeBcBLjDnzGbl81uuInlqZ5i17grCq6MOmtazt4AXNz7v0jSd1yRCFJwRSJo5E+VT7m6IlHWtXm7Zc56jJUGd1CR+057y/nZEa9LJEKKNuIa3IczE+N0D+w84HGHb/jm2JLe+0tFZCeKFpAQ/1u2rTF/Otcbq2qanjCzdPWE8R9oB00+J7ITJTWyhfFYaywXpgnCI+kX3OraBVOc3Ndw3glMAP7g+NXZ1saW/HYTa751eGixJ81iR7TvP+5PVauyc3H7tBmvB/5sbl9sb6t/COCQ6akx2Qnjz/XAP45zCJBxeMLM78iUH3LrQC8nTiTnJ/FwLsaRQMadXwfkLmhvO//5vjZVM5unWei/jbm9DSBnpMFPAjoNvydeMW5e75VNkUEpqYtT8S1hOYBj+c+NUl2bPsM9twJnMu5fAzvT8ScNuz2RTF80UF8huf0Tqzp+ZlgjAT/HfT7mz4dhbNvsCJnKcTeE5hcZ3GFuHwH7imF/xe266q7n5vTvs6q26RIIl7qx2bALDW6wgBPcYk8O9HqJHvyTPeY/d3wD8HXwRx2b07154xVD/sOSUa2kRlzifBKHgOC3fYsSM5oPdvfvGtzV3trw0bzWdyWS6VXAVyaefvn/rL/rvJX5XZlzHc5KG5M7NDPQxa6tz13WGdzRvrThkrw1N0+oaU51Ttu4Pn9Oh4k1848LPTwf9xuzbY2z+5ZPmHXpwliu7LHAg+8x5/qj82f7M+Os0DitY2nDw72LFiSS6V9g9u+kUmdte8JGZA8VLbgB9oZETbqWgMDc9nf8g+7MBG5ev3TeKzMZlHktzoRc6F/ZoQ8Lvh96mApzwTuB7YKLURYnTK5tOX/Hh6sBUqmQZNPTYDMSNc0zM6+ZcE9f6Drb6tfStn3z0MJPANYTty/nL+9cfOG6RG26Geeqyhc7j+rIm3bTsIs6ltY/vF1H7ssxO2nf31a+Zh3senZMM8NJVCYXvGmXbfdKoTk+ae85/lyZwz5D7aVowXX4EMaHcHB8jePPGpyZ6dj0g+1b2lRwgsCe7j/fT+hbByxzXrvjHuyqta3nDRzavu1D/i0IuAnzxYlVHZusNv2QhywLguDH/ScQA6YAK/PnLdq2p9BWuDmGH0FecEP3AebatXaALUH3bs7052UYnwnIfWb32u9ljE5zLjBye81bIM1511D7KGJw/bvZfrMRDtzQM0DO3WcYDPi2hHiOP/dfZoRbBmqbr+Puxr8AJ1Qn00eE8CFz3ufGZaGH30ok0xdmWhvyH6cLMAY8tTX3nBtgtsMTJzs2NofdnxQzwLqBFg/8O7u90d4k5B43vmNGy64blz4LudW3f755UErrO+6A/AmwmMVsS2ZJwy+GYw/trQ2/B34PpCfUNE+KBeEduDVPntF025p7G1cBuPGEOSdMmHXpvp2LL1y3XYW29azAzQeYqG1oQnc3fGVmSePyQvcdBYlkOmfOs5mlDcuLXUshJJLpl4HMUPspqavKA/FcbrHDKkKuPLAmNbb/+t5ZB/fY5Lprxydqmmf2f19pZ1v9WguDnwHWXZ4b07c8CO12wIKw7Bv57RMzr0i40eDwp47yjTvMQCEyHEp+xM3ec0F7dW3Tf7nbnZts3J+qatLXmtlf8PC1FthpofN68Le/Mov+7tnStfEUM7s1kWx+AtKtbv4nPFiP+9Fufr4792Z7J0gDaG+rf6g62ZzGvTGRbDrI3VrBqwm7Pwc2OebB+/PnXRIZTiU/4gK0L228153DgV8ExmfAbyGwOe62Mh4LT9/T0AJkWxtb6LE3g/8cZ7q5fdvwFjM+5nDxhDGxD+9QR2v9ebjNAqsyo9ksONvhYYKed7wyq7/I8Bv5EXfZOV0Z9vwVDL0Tcu1ybqveuW93q//MvfV/B+oH3N/OtmmrXwIsebV+s0vqH91ZDb1vzNNLwGVIIjHiisj2FFyRCFJwRSJIwRWJoJL/OUik0BI1TV/A7GqHO7KtDR/PXzcueeX+5Wz5hmMzgH0xnsVZlOjYmH5ueWrzTroccQqujCpVycuPcazJoL3/uupk0wlOd4tjneA3YfzFQnu3G1/LVI49BqgtQskDUnBl1EjMvCJB2LMIuBEGuNE/Z1mLeds+vmlu3ruEf5BINv8FaK6sTb8r7xHNolJwZfTw7v8BCys2c/7mCh7p/0t7+z0NTwJn9R+KPQiXWGjNhk0BSiK4ujglr6pqZtOhk5KXTyh2HUOVqG2ei9spENSt/knDRjPKdndbC/31AA79H/UsGgVXdiqRbPpPC+0vPQT/HD8rvV+x6xmsiTXzj8P9cje+mGmdt+cPglhsFrApvqX814WvbnAUXNkpd3sfW2/dnFAWDv3h72KorFswMbTwR47fmV3acP2ebt8b+s87pNYvO6djOGocDAVXdirArgOeAb+/Krvx/mLXs+fcgq7wZoOXyyvG7TD5366MS165f2i5W3F7NNtRWhP86eKU7FTvNLZvhgI8+V0E1cn0ux07zYHurk2d/ac+MnhTIpn+2ECzsVTWLZgYdG25H8x74l5Tam/wU3Blr5WriD8V68qdNtA6hxuAZwwuN4s/n7+u6tTLqq0rdx/YpJDYSRsWn7vDPGPFpuDKXquj5dz1wD0DrUsk0xsd/plpbdhu/eQZTQd0x/gJUB2GfnLH3ec+MxK17ikFV6RXYuZlh3SH9lPgIDOfZzGOTiSb353fJk7url3NHvrq3AbxOPoOhhbcZed0kUx3mzFuyJWIFJmHsfMMDgVwt2u2xmv7yVW6A3sIGEJwbXwwpO23GvKI67AO/ICh9iMykjKtDW/uv6z3AtWupwwerLpUOV1MDM3WDLWrIf8cZPAkcMRQ+xHZ2yW6x74diMewPw61r0L8jvsrnJOpW7TrycBFRjHP8X5g4/ps54qh9jXk4HpgbRgTqzY/976h9iWyNzOCOszvpwDP9Q45uL0zGj5u2H8PtS+RvVVlTfM7MT/WsO8Xor+C3PJocDlGTWJmenoh+hPZu7gF5vPBn2gvP/jeQvRYkOC2tzbcAfyS0K5hekq/DYvkqa6Z/zHgRCeYR8tHcoXos2APGbj7XPC3VU8Y21ioPkWibtLpV7zGzZuBO7Ot9QV7UKNgwc22Na4ALnazixO16VmF6lckqg6qW7BPT3duCRDG4/FzCtl3QR/ry7TWf91hEc4tlTMvP7aQfYtESt2i2Mau8GbMD4cgufauuS8WsvsCP49rnu3Y+Bng8SAMHkgk5ycL279I6Zt4ytWVVV1/b3N8Js4nBjXrxi4U/kH65anN4ytiHzBsGYSLq2qa6vu/g1Zkb1U1s+nQsLzrVwbHeRickmlrWDoc+xmWGTBWtpz7cnvrvI+6c5mZNVUnm3+tU2fZmx1YkxqbqGn6uoX2BE55LhYcl7173gPDtb9hnLrGPNvW8BXDT3QoC8Lg14lk0w8VYNmbVNd9q6o6mf7vTTbuKTdrBJrH+sapnYvnPT2c+x2ZU9hUKqhaMe6/DD8f7I3A3wy/PzRbEcALIbw8InVEUBD6mBxBzgLvLnYtxWDOUje/BezOYtcCgJsFziQ3f4sZx7tzMhA4fiduX+59j/OwG9nvnqlUUPXYuPeaUwucBLwNdn9+W5HS4avd7dHAWBbv8TvX3Nu4aiT3XtyLRnOuLxu/pqM6tiU+vqh1yM4FPb8yWORh/Kpil1IKzHO5wMvbS2mqVpEdJJLpfySS6VSx65DtaV5lkQhScEUiSMEViSAFVySCFFyRCFJwRSJIwRWJIAVXJIIUXJEIUnBFIkjBFYkgBVckghRckQhScEUiSMEViSAFVySCFFyRCFJwRSJIwRWJIAVXJIIUXJEIUnBFIkjBFYkgBVckghRckQhScEUiSMEViSAFVySCFFyRCFJwRSJIwRWJIAVXJIIUXJEIUnBFIkjBFYkgBVckghRckQhScEUiSMEViSAFVySCFFyRCFJwRSJIwRWJIAVXJIIUXJEIUnBFIkjBFYkgBVckghRckQhScEUiSMEViSAFVySCFFyRCFJwRSJIwRWJIAVXJIIUXJEIUnBFIkjBFYkgBVckghRckQhScEUiSMEViSAFVySCFFyRCFJwRSJIwRWJIAVXJIIUXJEIUnBFIkjBFYkgBVckghRckQhScEUiSMEViSAFVySCFFyRCFJwRSLIil2AlJbq2gVTQu956ytL7Dqwn0HY0rekvGLcsjUtZ28oRn2yVbzYBUiJCXsONrNF2y/0OrA6AIdVaw5f81paBtpYRopOlWU77a+pug/I7mR1j8FtpFLhSNYkO1JwZXsLz+o25w6gZ4C1cQ/stpEuSXak4MoOHL+dgb5Guf89u6T+0ZGvSPpTcGUHmbaGXwIv9P23b/1HD2Y3Faci6U/BlQGYm3EbvafLvT89xIMdLlpJsSi4MqAwzDtddhznd+uX1v+xuFVJHwVXBpRta1wBPIubuxE6/sNi1ySvUHDl1dyCuZkTxMr8jmIXI6/QnVMRM/6USyfH4mX/YvjhYAkzHzNsO3Oq3OyjDqsCvHXY9gOAZR1bTeCPZsoOfpCWj+SGd3/RpuBGQSoVVD02tg63Txl8gK3fPV8A1gLdw7lrg7cBax3WDPN+xju8FhgPvhoLFgXwXX2vHpiCW+ImnpY+Pgy4CjgG+KVjt4XOXZ1t9WtHYv/VyfR/98S6b+9cfOG6Yd9ZKhUkVkw40gg/7vBx4ACc73jYk8rec0H7sO8/QhTckuVWVdN8sRkXAr8NPDhnfdu834x4GdNTcZanBrqLanjNub4ssarzHPCLgE2hMatjacPDI15HiVJwS9BBdQv22diVu8nhww7nZ4/eOH+03h88eUbTAd1xawGmufPpbFuDfktGwS09qVSQeGzsj3H7AARnZFrnDfNFoQg45eqKRHnXDTifwDk909awtNglFZuCW2Kqa9Npd+aa+8z2tsa7i11P6XCrqmn+oRmzQreTO9rqHyl2RcWk33FLSHUyfao79QZfUmj7M58wJnYmbk8E5rczPTV8P4NFgIJbKupS5Q4LgJ+2tzZcU+xyStHKlnNf9lh4BnBQddW4ecWup5gU3BKR2DL+bOANsVh4TrFrKWXZJY3PmnGlOxeMS165f7HrKRYFt1S4nwncvm7xeU8Vu5SSV567zCFWZj1nFLuUYlFwS0AiOX8q8HZ3v7XYtURBe8v5WYO7cf94sWspFgW3JISnAeuynZt+VuxKosKdFuCdk06/4jXFrqUYNMtjaTgC88f35A6l6tqmE93tve7+ZLat8cfglkg2fxXAKnJXtrecv7MJ3wqmambzNAv9t+6cmW1r+N4I1/wIQHd39xTgxcEdQXQpuKXAmQS2ck82CZ3jDVJm9kPgx2AO6a8BFrxcfiO9MzVOmHXpvrFcWf59zSGwGvgDzjUjeTPD7ta8O7I9FS8kyroIzPYbtoJLmE6VS4AHVABde7KNEWxt7688HeS9fXSV5QZ6YugesE+72VmYfRc4EGNJdW16xC7wDKLmnVt2ThdAaDYqf8/ViBtRbuEqc8ONVX3LDFsF/roN8YN3eJLH3Z7IttXf3Pff+38wPb9rDE97yGxgRKZc3dOaZecU3IgKnJUOBJ5/iu0rgdjuPIS++icNGxO1TU+BHTzQ+kRyfhIP52IcCWTc+XVA7oL2tvOf36EWs81Vtc0fNufz4NOAZ8FuzrTWX1XImuUVOlWOKg//AUDg/9i2CP8H8I+dbZKvsm7BRNyOxOyh/uuqapsugXCpG5sNu9DgBgs4wS32ZOXMy4/doRS4wNwXuIW/NvMLgXbwK6uS6UsLWbO8QiNuRLV3bn4hUTkudA+2jV7B1gtcsYHam/GO6mTzJ5ywAux1dPX8h8MzuYDz8ttNrJl/XOjh+bjfmG1rnN23fMKsSxfGcmWPBR58jznXH83Cs/K+k4YveC534raH3VOp6xKPjf2JuX1p/Kz0lRsWN7w0mJpl5xTcqFqe6sn0+8C3tzY07nwDP83htFceCLOMwa1s7vb8VqGFnwCsJ25fzl/eufjCdYnadDPOVZUvdh7V0ftzDIC7/Wi7GWwoCUwAAAQPSURBVCpSqdCTTdcbvC/ew/HA0sHVLDujU+VRwt2+lWltsExrfZCLdU8K8I8DyXhZ/Il+9/xOAVb2jZL5LLQVAIYfsesd8uzWjeyQApQv/WjEHXXMOxezDvjfytOang0Ce7rcus/cCJf0NggwBpxtw9xzboDZLk9tzYLx4ODhsN8IMhppxB3F9tliLwDuzqS+ZW48gfO6CbMu3bd/ezebuvWf/rvd6H4qQBiYHpoYBgruKLZ5DGcAZrDtDXxBaLcDFoRl38hvm5h5RcKNBoc/dZRvfCx/XRDw1v5tMZ/rzlMd2Y16u98w0KnyKLHtqrITgO+LcSJQCzzYXrFx2wRs7W31D1Unm9O4NyaSTQe5Wyt4NWH358Amxzx4Py2pLfl9u3NuIpk+CoJvQ/gmwp7PAgcAM4oyQ+QooBF31PDTHP8h5j/A+ApwMGYNFZs5pX8Q21vrz8NtFliVGc1mwdkODxP0vGOgKWI9DD7o0APhEuAig79CcHy2rUFPOw0Tjbh7ud6JzPd4UsBMW/0SYMmrtel9yXVf3w9U1i2YuE8217X6Jw0b97xS2RMKrhRMR8u56zuKXcQooVPlEmDOyzCML+/aCx1YkxoLEISMytFdwS0Bjq8GO6jYdURJZ3zCQQBuuVW7ars3UnBLw+PA1NE+V/CeiOX8eCDMxXK/L3YtxaDglgAnfhdQmagad0qxa4kKxz+K88sReYtgCVJwS0BH67nPAI/g/Huxa4mCyTOaDjD4gJvdXuxaikXBLRXGdcDMytr0u4pdSqnrids3gKwFsVH75j4Ft0Rkpm68GVgRhHY1qZT+f9mJqpnN0xz+y/CLMkvmZopdT7HoA1IqUqkwCPkC5tOqHh1/ya43GH0q6xZMtNBvBx5vrzhkYbHrKSbNPFBCXn76/pVjDvtghxnfHHPYB/6++en7Hy92TSVjzvVl+2S72oDXufPBrsVz1he7pGJScEvM5qfv/80+h33wNZh9bZ+3fmD95j/fP6rfAwtQdepl1fts6l7icELMghmZ1vonil1TsSm4JWjzGSfcO+bFskqwb455y4cmTzzs3cs7n16+Z/MO7yWqaxdMMbP7gDcGbqe2t9b/qtg1lQK9kb6EVdU2n4n7tw3WuFOfbatv2Tr7/96v6tTLqi1e9nXcPwf+lOfKZmbvmfvXYtdVKhTcEldVk36DGc3A6cDf3LkNC5dkK17+ff/H8aJuQk3zpBi8x+Bjbp4EtmD29cz+E67ZflZJUXAjYmLN/ONylvuUYXVA37Qy7a+2TcRUAGN7/30F2G093Vt+sGHZhWuKWVSpUnCjZs71ZZWrO44O4O0e+r6w64nbosDwTR4EL5p1/zaz5ILnil2PiIiIiIiIiIiIiIiIiIiIiIiIlJr/D04XNlEWPqivAAAAAElFTkSuQmCC"},108:(e,n,i)=>{i.d(n,{A:()=>t});const t=i.p+"assets/images/bob-6e8dad95c440d488af22193a37ae0d7f.png"},1842:(e,n,i)=>{i.d(n,{A:()=>t});const t=i.p+"assets/images/edge-0a8b76cae995e0a8c485063cb500de8a.png"},3959:(e,n,i)=>{i.d(n,{A:()=>t});const t=i.p+"assets/images/edgePath-e11472fa0a55e29fd9de9876ff597ceb.png"},3135:(e,n,i)=>{i.d(n,{A:()=>t});const t=i.p+"assets/images/edgePathElement-8070f24cd6faf257dbf2357bbc26ab75.png"},8908:(e,n,i)=>{i.d(n,{A:()=>t});const t=i.p+"assets/images/edgePathString-e95d3068168658ba325a9e873dbf2612.png"},7274:(e,n,i)=>{i.d(n,{A:()=>t});const t=i.p+"assets/images/edgePolyline-7c4044c84e38ce242eefda017e1e188c.png"},8914:(e,n,i)=>{i.d(n,{A:()=>t});const t=i.p+"assets/images/edges-100-9cbc68a0debf543244c33b7d26b22709.gif"},5047:(e,n,i)=>{i.d(n,{A:()=>t});const t=i.p+"assets/images/edges-4-788922eab8ebbf8f1d0ffaa9a071132b.gif"},2634:(e,n,i)=>{i.d(n,{A:()=>t});const t=i.p+"assets/images/generic-2c235d68ea9ba3134644dfbbfcfc8d0e.png"},4259:(e,n,i)=>{i.d(n,{A:()=>t});const t=i.p+"assets/images/notSoSimpleXml-d176756e1aae1946e6d6fe906c9d9bbc.png"},4447:(e,n,i)=>{i.d(n,{A:()=>t});const t="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAXgAAADBCAYAAADFL6uvAAAABmJLR0QA/wD/AP+gvaeTAAAY+UlEQVR4nO3deXxU9b3/8dfnTEiAAJmwheJ+XdqfVi+LtmoF89NeLdYM6BWtv9vepULcuikErL96TW2tkonYa60LoNe61Za6kGitSxVBq31U0WulWrdqa1UWSQIEIZk5n/tHCMQQIDAbHN7PxyOPB3OWz/d75st5z5kzZ86AiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiBTc8PFT7qoYP/XVQvdDRKRQgkJ3IGcsGOv47wvdDRGRQolkwA+rvGAA7gdjgQJeRPZYkQx4vG00EIQhCngR2WMVZbPYsPFTfmjYUcsXzT15+Pjqk8BnAkcCaw3qli2a+1+dyw4fX30vcMDyRXPGdK8zfPyUleZ297LFc78FUFE55WgPbVERqQNTFF0PHGPuPwpjxfdY2H4T8AWcG5Yvnns5QOA2xmFNuoS3ho2rvtIC/7I5+4bGMzGLffPDhTe9073Nww6bXLxySHyGw5eAIxzeC+DuZa02ixfmtH9iO8dNOcPMfrp80dyKT51wzn7plP0IOBnACepXLJpzdeeyI46v/r9p98sCGOWw3rHnDR5avmjOHMAzftJFRLYiq0fwATYK/IPh46trDf8vc3vQzGsMVjtcWzF+ygGbl/Yxji/pXmPk+P/YB2xIaLy0acm0jQLaUhT90twWAn9xsxkWtj9gbk+Z8S7GRYABuDMW+Ki4zZ8zC/dy7ErcfmDO8WGYfojJk2Nd2/zUCefst2JI/Hl3pmB2Fx5OAP+pw/SK/n7NFhtqjALeHjquekw6FTzvBAPd7PsON0H4cudiFZXnfDZ0f9jgdQLONPfzzXgNvH74uKl3Zfh0i4hsU1aP4B1Gg8XBbx/6UfM/Ll06vw2g4vgpa3C7G4/9A/CXvY85Z3Ab7G/GFgGfpmg0gLttCviNgWqh24UrF89ZMmxc9VAzn26Bn75s4dznho+bujfGIXQeERtjgXJ3zlq+eN4jnWWGjatuN/OfjFhRNu5DWNhZPZ0KbsUIN4RFo1ueurFp4/Rnhh0/NQCu3fuYi2a+9+y1H3fWCbDRDqsD83sN/8byRXN/0ePzEcZOBg+XL5p7XpfJC0YeV319m6f67eDTKyKyQ7IW8CPG/cewEEaCzVm+aM75y7vMC0PazQDz5QDtRUWjIcRsyyN4x0eDpeIDSpau3Dx5lGO/Xbl4zhKAwPxgh6eXLZz3HADGQcBSgJFjq/un8E+b+/RlXcIdIIilF3oY4CEHsjHgh4+fchpwQhCGn295elO4dzB7EfdYKlj7D531O/rIKGAk7l9btnhej+EO4MZfzek3fFz1ZcNWNc3qfMF7/+k5f932sykikrmsBXxofUZ3HED7z7rPC8w+45COtfIWgJMeA5aOrQle3qIQNhr405sP/2RDx+PaAP5+uLnXdy7hHef17+2y0higASBV6qOAmON/2KKPBEUGuAcfbZro9gUMQgt+P3z81G4rdLwhCPv6pteaimPPG+6kRwK3L188785tPScrnprzq4rxU6938ytWDIl/Y/j4KXc4sZtXLLr5jW2tJyKSDdk7B99xGiW0vuv/p/ssx0ZjvPb+C3PWAXhgY4A/dz7uZjT4i50PhlS+fwjQn6DjnHzZceeXAweY8wJ0vHMA9raN5/PdbQwQWr+2l7oXDkI7FCAMU5u/AGUcDr4oSPPZHv88OGz5b+ct27QtReGojvWCm3rxrPiyRXO/GcMPdfg52L8Z4dKKcVO+04t1RUQykr1z8KGPMuP1ZY/e0brlTB9DyOLOR+b2aeC17kuNOH7qUaGzr/nmD1iL0j7KDQKLvQjQtyg11kNIe8f5+853DuHGgLfAxwKv9tSP0H2qwVsrR655c1NfoNnNDvnwmblLuy/fI2MUTmgl63p499GzDxbNexX4zt7HXPTdtj5rb3Kz2SMrq+98f+GcldtdWURkJ2XtCN6M0e682H36xiPu/e0TV8z4cDcv7rrc8HHVk0L3hwEsRvcPWJs7L20MQzsSWLvy6b3eBPCO0zNtwz9a3RHQzhiccO9jLur3yfpTzzOj0vFLmD8/3Tk9dF7EOXxY5dRR3fs+bPy547tPc3y0w597fiHbrGLcuScOHzf15K7T3nv22o+t4xSWeZAu3db6IiKZysoR/MYPNg8Bv7X7vL5B+2jHCC3W9QPVt805qWL81O9h/pY7p+K+L3Ax8LP1pDZfIgmjvMsRvZmPxXkRakOAoCNwX1m6dH7b/pX/3nddyKHA+raitY+OOL76P8PQBxPYJNy/ijN7xeJ5v+raPw/T11ssdqGF/nDFuOrLiIWvhGn7DGanG+EJFeOnHL5s0by/dC4fOKN6urxzC0F4Is7MivFTbwN/NB3E3grC9KGOfRv4/QdP3PJub59fEZGdkZUj+HQ/DgcCi9kWR/BOMBrw9IbizaEd8C3gFYdLneByd3tl2KrmE82DYuCd5oW3NXcpMQrrcsmkM9bZfHmlwxin453D+nTsCCCWDuwojI9D99+aMdfcy93szOWL507r3r+Vz9y6JkynP4cFj7v5ZR7aI2ZMNfc3QreDu4b7yLHV/R0OgS23s7tlT8291AJODrFSx34YhOFisFrHFhW3h6dsb30REdmGsuPOLx9WecGAQvdDREREREREREREREREREREREREREREREREREREREREREREREREREREREREREREREREREREREREREREREREti1W6A7kWsXxU84esM/Y/Vv/uuT1QvdFRCSfgkJ3INfC0Ko9ZmcVuh8iIvkW+YAXEdlTKeBFRCJKAS8iElEKeBGRiFLAi4hElAJeRCSiFPAiIhGlgBcRiSgFvIhIRCngRUQiSgEvIhJRCngRkYhSwIuIRJQCXkQkohTwIiIRpYAXEYkoBbyISEQp4EVEIkoBLyISUQp4EZGIUsCLiESUAl5EJKIU8CIiEaWAFxGJKAW8iEhEKeBFRCJKAS8iElEKeBGRiFLAi4hElAJeRCSiFPAiIhGlgBcRiSgFvIhIRCngRUQiSgEvIhJRCngRkYhSwIuIRJQCXkQkohTwIiIRpYAXEYkoBbyISEQp4EVEIkoBLyISUQp4EZGIUsCLiESUAl5EJKIU8CIiEaWAFxGJKAW8iEhEKeBFRCJKAS8iElEKeBGRiFLAi4hElAJeRCSiFPAiIhGlgBcRiSgFvIhIRCngRUQiSgEvIhJRCngRkYhSwIuIRJQCXkQkohTwIiIRpYAXEYkoBbyISEQp4EVEIkoBLyISUQp4EZGIUsCLiESUAl5EJKIU8CIiEaWAFxGJKAW8iEhEKeBFRCKqqNAdyLaDJnyzZMWaVP/NU9JF7lZcdtz55Z1TWj5Or+WFOe2F6J+ISL5ELuBbWts+XxKET31yqlMShGd2PhheZgcvh7fy3jkRkTyK3CmaFYs+9bRjH25ltruxZPkTcxTuIhJ5kQt4qA3NuMOxVPc57jjY7YXolYhIvkUw4ME9/XPDtzj9ZObmqdSvCtEnEZF8i2TAr1h0y4sOrwPeOc0hDfbkymdufb+AXRMRyZtIBjyAGXcB4abHTuDmdxawSyIieRXZgCdmd9Fl+xxS7Za6v4A9EhHJq8gG/PIn5rzl+Es4IZAy7KHmhbc1F7pfIiL5EtmABwg8uANzA4o84O5C90dEJJ+s0B0YeNqPhhSliyeHeAI4wGA/oF9Wiocpile/DRbQNuhAsIJvrojsmdLA+8C77jwVI/3zVY2XLM11owVLvKGJWSNTBD8C/h+Aw+OBsdThb+7+QbbaKWl5u9aD2Mq2gftdn62aO8Q4i9CWYb6wIO1nkWGHY4xz9xsK3ZdMOWEsILgAeNThz4XuT6bMOcnNBoDfV+i+ZMoC24vQT3cLbsfDlkL3JxsMBoLtC34Q2JeAIcAfzHxa04IZi3PYbr65xSde8x3crwBvxbkyXZS6e839l36Ui9YqxlVPcfxvyxfPfSQX9bcnnkgucXiupaHmgkK0n03lE+sudLfZzQ01JYXuS8YmXFcS77NhPW7/2tw4/Y5CdydT8UTdnWB7NzfUVBa6L5mKT0pWEvJkSOzg1Q0Xv1no/mRd9c19yj9cfVJo/H9zjnb8HtLpC1se+m5TtpvK771oJlxXEu9TPw/nX3D/sfUNv980/5KcvkK3lTC/KbVXay7bEBHptTnntjfBQ+C/jldd81UzZlus6NlBidmnZvsFLX8BX1nbN95nw2+Azzl+VkvjjPn5aLbp8TmReIsnIlFj3tzIHWWT6n7naWsMLP1ceSJ5QlNDzcvZaiFPV9G4lQ0svQU4KrDgiy0N+Ql3EZFdXcsDM96yWNGxwDsOjcNOqRuRrdp5CfiyRLLGjK+4+7+uWjDtd/loU0Rkd9H8wEXNRYQJINZeZPdSW5uVbM55wA+eeM0+hl1uWH1L44x7c92eiMjuaGXDzPcDD84Aji5bUnpONmrmPODTHtYBTUUl/X6Q67ZERHZnqxqnPYfzM4Mr45OujWdaL6cBPzQxa6TBmQ7fXzH/wrW5bEtEJAr6pP1SoMy8/V8yrZXTgE8RO9uhLShJ/zKX7YiIRMWKX8/4EPw37nZ2prVyfIrGE2Y8lOtr3UVEosQ8uAc4NtMranJ9Dv4gsKxd0ykisifwNL8DLF3EqEzq5C7gJ9cWAyNw3s1ZG12UVdWNiU9MvhOvqpub1cLVN/ehsja/3/gVkT1ac+m+7wEeOhkdwecsuAakSuNAANbr+yuUTUyea85NhMGo5gen/c8ONRjYUTj7YZYArwbz7a+0ffEPVy/0Qf1fboHzs1FvewZNmvW5IIxdBn60wUchPNGHcObKhplrduXaPSlN/LiimLYrHDsFGILxFs4v46tbk+8srF2fSe3yicmr3ZnZ81z/YXPDjMsyqZ/v9vI1NvFE8g7gq9taxt3P2NlLmvM9LgB7T57dr7UtdWQY2rFmfB04pGQ9A5Y9WtPjLUp2i31s/plpEsk2CyjNpD+ROTIdWBy7fe2GVLs7L2Ur3AHcKc/XLdnKTr3mRAvDBvA/uHOJBT7M3C5KERxTPvnqykw+y8hl7Z6UJ+q+4LTPd2wN+G0Yb1hox7pxefOg/kcCEzNqwL0crNmd6VvMiwUvZVQ7z+3ldWwCbvE0C3uaZebfBhvsfYue3On6+R4XYO368GTM7th4N/Bt3ggvSvtYb0Qm4N+bf/HHwK1ZL2yUZ73m1poKwnrgxeaS1pOYX9sGMHhifUPo/lK4PpgK1O+KtXuUthaLeWM/X3fR+4216zZOvT2eqH8DqB80Mfn51Qtqfr/zDQTlmL/X0lBzSza6W8j28jk2zQ/ULIQtA76sKnkC2KEQ/tPq+TWrdr6FfI8LNDdOfwAYCFA+MZns8cVlo0jtY72w2wZ82anXnGhB+HgPsx5vbqj5p+4T96+s7dsycMDFHvjZOPsDzQ5/NPN7mov3v4v5Z6Y7l41X1c3F7AygP1AMdl48kTyvaz3Dj2tqmPFMtran44iXUWD/3PmfA2DVgul/iieSjRbYN9jJ/yC5rL01TQ/VvAKc2/38nAfhAxZavWGHAzsd8E4Yx4MVGXVyF2ivEGPT3dDErIEp41aD+qYFM3f+6J38j8uOiNo+1hu77U/2pdMbXsaZ1PXP4cOtLd88qHRuaH6ZwT3mdibY9wx7G7cbyze8U9112ZBgDs6/45wJYPBI97bSJUWvZnmTjgAoIv1Y9xmGPYazX/nkq8t2wdo7xELfF8DhbxlWKjdn+f6VtX2HVNV/ZvDps/YGz+HJtJy1V/CxSWEzcAYVFff/YebV8j0uO2SP2Me62m2P4Nc+fOkKYEHXafFEsudXyNragCVMNrinaUHNlV3m/GxgVX3tmrGtq2jYPHF14/Q/dKlJiP+lpXHGJ9rKthDbx2DdyoaZa0oTP67oQ/si3Bc1N86Y6u4fYMC62D7ADp/Hy2XtHWax03BfV9RW/GyGlcpD8+ObB5V+BN6fVEC8qn6VW92sloaaZDY/h8lle4Uem2GTfzqgff26CxxuyNK3zfM9Lr22x+xjXey2R/A7pLY2BH8dOCVeVT+J6pv7dM5a0zh9Zcf8wgrwIUAzQJG1Hw4cglnHB5HWcSWS92HorlZ7RwyuuuZo3C9wqF318LdWZ1LLzOfh3JImHNPfW0stzeFmPG7YrLKq5LRs9TnX7RV6bNrb1n0doyxdxHXZqJfvcdkRe8I+1t1uewS/o8KQfw4CbsP8/viHq9fZxOTTHvJwEAT3rlowLcPTBZlzbBkdv9NIS0vrwvig0mmO/XHj7KEAsbRt9RRUoWr3VmnixxUhbXfhwQsta9Zem2m9pgUzru7898brz16htvbs+JLSA83sYrL9mUKO2iv42DgTgZfX3l+zPBvl8j0uOyLq+1hP9piAX/3gjDeAL5QnkkeEcLI5J7pxVejh1fFE8tLmhprZheyfY+8ZXlL25avKWx76blMzbOqP4SMdCPr2e29Xq90bgybPHhxsaHsMzFNFXsXC2lROGqqtDW1i8nF3Zg6sqh+6pnH6ypy0k8X2Cjo2E64rcTYcazAvJ/U75XtctiLK+9jW7BmnaLpoaqh5uaWhJtnUUPOl0G0fzJ8G6rdxz4fVhg3Iecc8fB4gKCqa0H1W6ExweG1b50jLJtUdODQxa2Auamei7MtXlQcb0o+ADQ2Jfak3R4rb3Jbt8JDhwMdr1qxt3pn1s93edrelgGNTXrz+cwZ9bQeuZtrZscn3uGylE5Hcx7Zljwj4YZN/OiBeVT+p+6f5axqnr7QweAKw9uJ0362s/jZwdLZ+YWVrWhpnLHHjWQ/t20y4btOXNeKT6kaZcXJgfv3W1o0n6r5uob2RIvj7gNOSw7NZOxPDTqkbYbHYU8CIMPTje/ODwtvblo5lrknEE/Wnd58+MDHr0xiTHZ7K5ruEnW2vN9tSqLEBcDgAIHTe783y29uefI/LjoriPrY9u+YpmiBMxBP1Pdxkx15ubpj24o6Wa9vQOsHM7oon6v8IyQY3fw0PVuE+xs0vcefXLQ98952e1nXjJnNuKnuhtNEm1v3CCYpxH4Pzq5bGmid2eNu2IXCvcePheJ8Nj1JV999uNoyQGoPniz+227a2nrudaIYBA/uEfB5ozFbtnRWfdNX+7aE9Duxt5tMsxph4ov7YrssUkb6v+1e4e7MtZuGx7syIT6x7ArfFhr0ZOp81/AJgfeDpc7O5LTvbXm+2BfI/Npv6F9qnzCAIYr26bn1725PvcYGO2xSsXZ8+aXMfORigrS9V8arkxwDt1ue51obvLINo7WO9sasewV8BftuWf+ktjg56o6VhxnxSdjD4kziV5vYTw+eb8RWHHwzsGztjq+suqLnZ3C40Yy/cbjT3Kw0OCwLf2hH/TmtqmPGMu1eCN2NWB3wd+EU/bz1ha/fVAAiwG4E3wR8ra2nd4jrcTGrvLA9jM4EDgRJ3u97c7uk+nu2BbXEk1KttWVBziTtfxG0tcK7jtwTmp4HdkYpxWFPjJX/N5rbsbHu92RbI/9h0Muu4kVUsZr06J7697cn3uACsbQ2HYzyw6W/jLTAcft45rZj2Izf1MUL7WEENmjx7cDyR9PjE5GmF7kshxRPJJWWJ5A2F7kc2lE+suzCeSG4odD+yYsJ1JfFE0uNV9V8rdFeyIZ6ouzOeSC4sdD+yIT4pWRlPJH1QYvZBhe5LIcUTybbyRF1GNznM2RH86vkXNQEfm7NXrtoQEYmiipOSpUAfzDL6YlQOT9GY0/F19H1z14aISPSsL/FPA6ThrUzq5PQcvOMvOHwxl22IiESNWXAKsHp1S+uO/S5GNzkNeCN2DzB68MT6Q3PZjohIxHwFs/vI8IdxchrwzSVrfgOsTHv4zVy2IyISFWUTk18EP8yduzOtldvLJOfXtuF+hWHVZYlZR25/BRGRPVhlbZG5zwaebmmY1tPvXeyQnF8H37xm3Y0YfzKCG6iszfq14yIiUREfVPo9sP9jFrsgG7dWzv0XnRbWpjz0fwMOKxvU/9Zd6Ob/IiK7jLKq5JnAf5r75U0LLv7jdlfohbx8k7WlccYSsK8ZdlY8cc1PmPzLWD7aFRHZHcQT1yTMuA3jzqbGmquyVTdvtypobph+n5udC15dtuHdxsETrhuUr7ZFRHZNbmVVddMhvN+wB5vbSqZm81ev8novmpYF0+d5GEwwODrss+G1skR9da7v0igisiuKT6obFU/UP2VmSZzrm8as/QoPfyurtwLJ+90kWx6c9tvBp886Ip0KkobfFF9SegGJ+juCovQvVt03M+83xBcRyZeRVbX9W4P+VeZ2NiGnYryKhyc0N858sud7jWamoB94llfVHwd8w80TQD/gPYO/+6Zf+4oA90FmlnJYV+iuZMqgxKE/0FTovmRJOdAKtBW6I5kypxQjiMS+4xRhDKTjB6oL/nvJWdLXnSFmHAzEgGcdbmtZ3frfubxH/i5xRcvQxKyB7W4nmdk/GowAixe6TyIi2eL4OuAjh9csSD3WvJXfnxARERERERERERERERERERERERERERERERERERER2T38Lw24DnJ90If/AAAAAElFTkSuQmCC"},4967:(e,n,i)=>{i.d(n,{A:()=>t});const t="data:image/png;base64,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"},7692:(e,n,i)=>{i.d(n,{A:()=>t});const t=i.p+"assets/images/points-5c9c6c00db5175c0d948a09163a40637.png"},5846:(e,n,i)=>{i.d(n,{A:()=>t});const t=i.p+"assets/images/polylines-00e6358bcbdecbf9807c0bc2ade2d3f9.png"},3342:(e,n,i)=>{i.d(n,{A:()=>t});const t=i.p+"assets/images/reftree-56916a9f376d970aaefc3069020aee4a.png"},4792:(e,n,i)=>{i.d(n,{A:()=>t});const t=i.p+"assets/images/simpleXml-33e8a5f26cea7dd70c3808d86690488b.png"},9221:(e,n,i)=>{i.d(n,{A:()=>t});const t=i.p+"assets/images/x+y-c7fbf58ef886ad0cf3d32f18259c15be.png"},2385:(e,n,i)=>{i.d(n,{A:()=>t});const t=i.p+"assets/images/zipper1+2-bc931dc2a9a17d909a27f5ae5d1e095b.png"},3618:(e,n,i)=>{i.d(n,{A:()=>t});const t=i.p+"assets/images/zipper1-5f0e8ccac0548193671b75d4f96125e4.png"},5823:(e,n,i)=>{i.d(n,{A:()=>t});const t=i.p+"assets/images/zipper2b-b4d938aec16be4bc5d0601737ed9bd53.png"},1248:(e,n,i)=>{i.d(n,{A:()=>t});const t=i.p+"assets/images/zipper3-25b272311b7ec6ce2e1b0ce993d16055.png"},4095:(e,n,i)=>{i.d(n,{A:()=>t});const t=i.p+"assets/images/zipper4-cbb2cf5c5de815041892e7f3b09703f8.png"},5078:(e,n,i)=>{i.d(n,{A:()=>t});const t=i.p+"assets/images/zipper5-56ccb910b4b659418162c70c7d32c364.png"},6317:(e,n,i)=>{i.d(n,{A:()=>t});const t=i.p+"assets/images/zipper6-5e74a23116f43ca88bf512b027cbdd9e.png"}}]); \ No newline at end of file diff --git a/assets/js/22c5de9a.773cf82f.js b/assets/js/22c5de9a.773cf82f.js deleted file mode 100644 index c87d415..0000000 --- a/assets/js/22c5de9a.773cf82f.js +++ /dev/null @@ -1 +0,0 @@ -"use strict";(self.webpackChunkreftree=self.webpackChunkreftree||[]).push([[672],{8598:(e,n,i)=>{i.r(n),i.d(n,{assets:()=>d,contentTitle:()=>l,default:()=>g,frontMatter:()=>o,metadata:()=>c,toc:()=>p});var t=i(4848),s=i(8453),a=i(3554),r=i.n(a);const o={sidebar_position:1,description:"A journey deep inside reftree\u2019s animations feature, showing how some of functional programming techniques and concepts can be applied to produce visualizations of themselves."},l="Visualize your data structures!",c={id:"talks/Visualize",title:"Visualize your data structures!",description:"A journey deep inside reftree\u2019s animations feature, showing how some of functional programming techniques and concepts can be applied to produce visualizations of themselves.",source:"@site/../site-gen/target/mdoc/talks/Visualize.md",sourceDirName:"talks",slug:"/talks/Visualize",permalink:"/reftree/docs/talks/Visualize",draft:!1,unlisted:!1,editUrl:"https://github.com/stanch/reftree/tree/main/docs/../site-gen/target/mdoc/talks/Visualize.md",tags:[],version:"current",sidebarPosition:1,frontMatter:{sidebar_position:1,description:"A journey deep inside reftree\u2019s animations feature, showing how some of functional programming techniques and concepts can be applied to produce visualizations of themselves."},sidebar:"mainSidebar",previous:{title:"Unzipping Immutability",permalink:"/reftree/docs/talks/Immutability"}},d={},p=[{value:"Introducing reftree",id:"introducing-reftree",level:2},{value:"Inside reftree",id:"inside-reftree",level:2},{value:"Functional animation",id:"functional-animation",level:2},{value:"Zipping it up",id:"zipping-it-up",level:2}];function h(e){const n={a:"a",code:"code",em:"em",h1:"h1",h2:"h2",img:"img",li:"li",p:"p",pre:"pre",ul:"ul",...(0,s.R)(),...e.components},{Details:a}=n;return a||function(e,n){throw new Error("Expected "+(n?"component":"object")+" `"+e+"` to be defined: you likely forgot to import, pass, or provide it.")}("Details",!0),(0,t.jsxs)(t.Fragment,{children:[(0,t.jsx)(n.h1,{id:"visualize-your-data-structures",children:"Visualize your data structures!"}),"\n",(0,t.jsx)(n.p,{children:"This page contains the materials for my talk \u201cVisualize your data structures!\u201d."}),"\n","\n",(0,t.jsx)(r(),{controls:!0,style:{marginBottom:"1em"},url:"https://www.youtube.com/watch?v=yoayLNPTESk"}),"\n",(0,t.jsxs)(a,{children:[(0,t.jsx)("summary",{children:"Older videos"}),(0,t.jsxs)(n.ul,{children:["\n",(0,t.jsxs)(n.li,{children:["ScalaDays Chicago, April 2017: ",(0,t.jsx)(n.a,{href:"https://www.youtube.com/watch?v=6mWaqGHeg3g",children:"https://www.youtube.com/watch?v=6mWaqGHeg3g"})]}),"\n"]})]}),"\n",(0,t.jsx)(n.p,{children:"You can use this page in two ways:"}),"\n",(0,t.jsxs)(n.ul,{children:["\n",(0,t.jsx)(n.li,{children:"as a reference/refresher on the material covered in the talk;"}),"\n",(0,t.jsx)(n.li,{children:"as an interactive playground where you can try the same commands I presented."}),"\n"]}),"\n",(0,t.jsx)(n.p,{children:"Throughout this page we will assume the following\ndeclarations (each section might add its own):"}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-scala",children:"import reftree.core._\nimport reftree.diagram._\nimport reftree.render._\nimport reftree.geometry._\nimport reftree.svg.animation.Frame\nimport reftree.svg.XmlSvgApi\nimport reftree.svg.XmlSvgApi.svgUnzip\nimport reftree.contrib.XmlInstances._\nimport reftree.contrib.OpticInstances._\nimport reftree.contrib.ZipperInstances._\nimport reftree.contrib.ShapelessInstances._\nimport reftree.util.Optics\nimport reftree.demo.Data\nimport reftree.demo.Shortcuts\nimport scala.collection.immutable._\nimport java.nio.file.Paths\nimport Diagram.{sourceCodeCaption => diagram}\n"})}),"\n",(0,t.jsx)(n.p,{children:"To start an interactive session, just run"}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{children:"$ sbt demo\n@ render(List(1, 2, 3))\n"})}),"\n",(0,t.jsxs)(n.p,{children:["and open ",(0,t.jsx)(n.code,{children:"diagram.png"})," in your favorite image viewer (hopefully one that\nreloads images automatically on file change). You will also need to have\n",(0,t.jsx)(n.a,{href:"http://www.graphviz.org/",children:"GraphViz"})," installed. ",(0,t.jsx)(n.em,{children:"The interactive session\nalready has all the necessary imports in scope."})]}),"\n",(0,t.jsxs)(n.h2,{id:"introducing-reftree",children:["Introducing ",(0,t.jsx)(n.code,{children:"reftree"})]}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-scala",children:'// extra declarations for this section\nval renderer = Renderer(\n renderingOptions = RenderingOptions(density = 100),\n directory = Paths.get(ImagePath, "visualize")\n)\nimport renderer._\n'})}),"\n",(0,t.jsxs)(n.p,{children:[(0,t.jsx)(n.a,{href:"https://stanch.github.io/reftree",children:"reftree"})," is a library for visualizing Scala data structures."]}),"\n",(0,t.jsx)(n.p,{children:"Let\u2019s look at a quick usage example:"}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-scala",children:'case class Person(firstName: String, age: Int)\n\nval bob = Person("Bob", 42)\n// bob: Person = Person(firstName = "Bob", age = 42)\n\ndiagram(bob).render("bob")\n'})}),"\n",(0,t.jsx)(n.p,{children:(0,t.jsx)(n.img,{alt:"bob",src:i(108).A+"",width:"313",height:"363"})}),"\n",(0,t.jsx)(n.p,{children:"That\u2019s it! You can configure the visualization as you like:"}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-scala",children:"// render strings as a single box\nimport reftree.contrib.SimplifiedInstances.string\n"})}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-scala",children:'// rename the firstName field (pun not intended)\nimplicit val personConfig: ToRefTree.DerivationConfig[Person] =\n ToRefTree.DerivationConfig[Person]\n .tweakField("firstName", _.withName("name"))\n\ndiagram(bob).render("bob-simplified")\n'})}),"\n",(0,t.jsx)(n.p,{children:(0,t.jsx)(n.img,{alt:"bob-simplified",src:i(9341).A+"",width:"238",height:"364"})}),"\n",(0,t.jsxs)(n.p,{children:["There are various ways you can use ",(0,t.jsx)(n.code,{children:"reftree"}),":"]}),"\n",(0,t.jsxs)(n.ul,{children:["\n",(0,t.jsx)(n.li,{children:"improving the documentation of your projects;"}),"\n",(0,t.jsx)(n.li,{children:"live-coding demos;"}),"\n",(0,t.jsx)(n.li,{children:"exploring how things work;"}),"\n",(0,t.jsx)(n.li,{children:"anywhere you need diagrams of your Scala data structures."}),"\n"]}),"\n",(0,t.jsxs)(n.p,{children:["(",(0,t.jsx)(n.em,{children:"Incidentally, this talk is an example of all of the above."}),")"]}),"\n",(0,t.jsxs)(n.p,{children:["My previous ",(0,t.jsx)(n.code,{children:"reftree"}),"-powered ",(0,t.jsx)(n.a,{href:"/reftree/docs/talks/Immutability",children:"talk"})," focused on\nimmutable data and various ways it can be manipulated (I do recommend it)."]}),"\n",(0,t.jsxs)(n.p,{children:["Today I would like to take you on a journey deep inside ",(0,t.jsx)(n.code,{children:"reftree"})," itself,\nso that we can see how some of these techniques and concepts can be applied...\nto produce visualizations of themselves \u2014 using one of my favorite ",(0,t.jsx)(n.code,{children:"reftree"}),"\nfeatures: animations."]}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-scala",children:'Animation\n .startWith(Queue(1, 2, 3))\n .repeat(3)(_.iterate(2)(q => q :+ (q.max + 1)).iterate(2)(_.tail))\n .build(Diagram.toStringCaption(_).withAnchor("queue"))\n .render("queue")\n'})}),"\n",(0,t.jsx)(n.p,{children:(0,t.jsx)(n.img,{alt:"queue",src:i(9696).A+"",width:"540",height:"922"})}),"\n",(0,t.jsxs)(n.h2,{id:"inside-reftree",children:["Inside ",(0,t.jsx)(n.code,{children:"reftree"})]}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-scala",children:"// extra declarations for this section\nimport reftree.contrib.SimplifiedInstances.{option, seq, list}\n"})}),"\n",(0,t.jsxs)(n.p,{children:["First, we need to grasp the basics of ",(0,t.jsx)(n.code,{children:"reftree"}),"."]}),"\n",(0,t.jsxs)(n.p,{children:["To visualize a value of some type ",(0,t.jsx)(n.code,{children:"A"}),", ",(0,t.jsx)(n.code,{children:"reftree"})," converts it into a data structure\ncalled ",(0,t.jsx)(n.code,{children:"RefTree"})," (surprise!), using a typeclass ",(0,t.jsx)(n.code,{children:"ToRefTree[A]"}),"."]}),"\n",(0,t.jsxs)(n.p,{children:["For case classes this is done automagically, using\n",(0,t.jsx)(n.a,{href:"https://github.com/milessabin/shapeless/wiki/Feature-overview:-shapeless-2.0.0#generic-representation-of-sealed-families-of-case-classes",children:(0,t.jsx)(n.em,{children:"shapeless"})}),".\n(",(0,t.jsxs)(n.em,{children:["If you are curious about the magic, take a look at ",(0,t.jsx)(n.a,{href:"https://github.com/stanch/reftree/blob/master/core/shared/src/main/scala/reftree/core/GenericInstances.scala",children:"this file"}),"."]}),")\nGiven our friend ",(0,t.jsx)(n.code,{children:"bob"}),", ",(0,t.jsx)(n.em,{children:"shapeless"})," would provide a generic representation,\nwhich includes the field names (at the type level!) and the values (as a heterogeneous list):"]}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-scala",children:'Shortcuts.generic(bob)\n// res2: shapeless.::[String with shapeless.labelled.KeyTag[Symbol with shapeless.tag.Tagged["firstName"], String], shapeless.::[Int with shapeless.labelled.KeyTag[Symbol with shapeless.tag.Tagged["age"], Int], shapeless.HNil]] = "Bob" :: 42 :: HNil\n\ndiagram(Shortcuts.generic(bob)).render("generic")\n'})}),"\n",(0,t.jsx)(n.p,{children:(0,t.jsx)(n.img,{alt:"generic",src:i(2634).A+"",width:"417",height:"460"})}),"\n",(0,t.jsxs)(n.p,{children:["This information is enough to auto-generate a ",(0,t.jsx)(n.code,{children:"RefTree"}),".\nNow, what does it look like? The best way to find out is to visualize a ",(0,t.jsx)(n.code,{children:"RefTree"}),"\nof a ",(0,t.jsx)(n.code,{children:"RefTree"}),"!"]}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-scala",children:'diagram(Shortcuts.refTree(bob)).render("reftree")\n'})}),"\n",(0,t.jsx)(n.p,{children:(0,t.jsx)(n.img,{alt:"reftree",src:i(3342).A+"",width:"1520",height:"836"})}),"\n",(0,t.jsxs)(n.p,{children:["As you can see, it contains values (",(0,t.jsx)(n.code,{children:"Val"}),") and references (",(0,t.jsx)(n.code,{children:"Ref"}),")."]}),"\n",(0,t.jsxs)(n.p,{children:["How do we get from ",(0,t.jsx)(n.code,{children:"RefTree"})," to an image though?\nThis is where ",(0,t.jsx)(n.a,{href:"http://www.graphviz.org/",children:"GraphViz"})," comes in.\nFrom a ",(0,t.jsx)(n.code,{children:"RefTree"})," we can obtain a graph definition that can be rendered by GraphViz:"]}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-scala",children:'Shortcuts.graph(bob).encode\n// res5: String = """digraph "Diagram" {\n// graph [ ranksep=0.8 bgcolor="#ffffff00" ]\n// node [ shape="plaintext" fontname="Source Code Pro" fontcolor="#000000ff" ]\n// edge [ arrowsize=0.7 color="#000000ff" ]\n// "-repl.MdocSession$MdocApp$Person560852288" [ id="-repl.MdocSession$MdocApp$Person560852288" label=<
Personnameage
·42
> ] [ color="#104e8bff" fontcolor="#104e8bff" ]\n// "-java.lang.String1983877187" [ id="-java.lang.String1983877187" label=<
"Bob"
> ] [ color="#104e8bff" fontcolor="#104e8bff" ]\n// "-repl.MdocSession$MdocApp$Person560852288":"0":"s" -> "-java.lang.String1983877187":"n":"n" [ id="-repl.MdocSession$MdocApp$Person560852288-0-java.lang.String1983877187" ] [ color="#104e8bff" ]\n// }"""\n'})}),"\n",(0,t.jsxs)(n.p,{children:["Going even further, we can ask GraphViz for an ",(0,t.jsx)(n.a,{href:"https://en.wikipedia.org/wiki/Scalable_Vector_Graphics",children:"SVG"})," output:"]}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-scala",children:'Shortcuts.svg(bob)\n// res6: xml.Node = Diagram\x3c!-- -repl.MdocSession$MdocApp$Person560852288 --\x3e-repl.MdocSession$MdocApp$Person560852288Personnameage\xb742\x3c!-- -java.lang.String1983877187 --\x3e-java.lang.String1983877187"Bob"\x3c!-- -repl.MdocSession$MdocApp$Person560852288->-java.lang.String1983877187 --\x3e-repl.MdocSession$MdocApp$Person560852288:s->-java.lang.String1983877187:n\n'})}),"\n",(0,t.jsx)(n.p,{children:"At this point you might be guessing how we can use this as a basis for our animation approach.\nEvery state of a data structure will be a separate frame in the SVG format.\nHowever, an animation consisting of these frames alone would be too jumpy.\nWe need to add intermediate frames to smoothly \u201cmorph\u201d one frame into another.\nWith SVG being a vector format, this sounds simple.\nWe just have to individually morph different aspects of the image:"}),"\n",(0,t.jsxs)(n.ul,{children:["\n",(0,t.jsx)(n.li,{children:"graph node positions;"}),"\n",(0,t.jsx)(n.li,{children:"graph edges and their shapes;"}),"\n",(0,t.jsx)(n.li,{children:"colors;"}),"\n",(0,t.jsx)(n.li,{children:"stroke thickness;"}),"\n",(0,t.jsx)(n.li,{children:"transparency."}),"\n"]}),"\n",(0,t.jsx)(n.p,{children:"Ouch! A sane functional approach would definitely help here :)"}),"\n",(0,t.jsx)(n.h2,{id:"functional-animation",children:"Functional animation"}),"\n",(0,t.jsxs)(n.p,{children:["Let\u2019s start by introducing an abstraction for morphing, or, in other words,\ninterpolating things of type ",(0,t.jsx)(n.code,{children:"A"}),":"]}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-scala",children:"trait Interpolation[A] {\n def apply(left: A, right: A, time: Double): A\n def sample(left: A, right: A, n: Int, inclusive: Boolean = true): Seq[A]\n}\n"})}),"\n",(0,t.jsxs)(n.p,{children:["(",(0,t.jsxs)(n.em,{children:["If you are curious, ",(0,t.jsx)(n.a,{href:"https://github.com/stanch/reftree/blob/master/core/shared/src/main/scala/reftree/geometry/Interpolation.scala",children:"here is the actual implementation"}),"."]}),")"]}),"\n",(0,t.jsxs)(n.p,{children:["Once we have an instance of ",(0,t.jsx)(n.code,{children:"Interpolation[xml.Node]"}),", we can generate\nas many intermediate frames as we want! But how do we construct this instance?"]}),"\n",(0,t.jsxs)(n.p,{children:["Consider a lowly floating point number (it can represent an ",(0,t.jsx)(n.em,{children:"x"})," coordinate of some element in our SVG, for example).\nThere is an obvious way to implement ",(0,t.jsx)(n.code,{children:"Interpolation[Double]"}),", which ",(0,t.jsx)(n.code,{children:"reftree"})," already defines as ",(0,t.jsx)(n.code,{children:"Interpolation.double"}),":"]}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-scala",children:'val numbers = Interpolation.double.sample(0, 10, 5).toList\n// numbers: List[Double] = List(0.0, 2.5, 5.0, 7.5, 10.0)\n\ndiagram(numbers).render("numbers")\n'})}),"\n",(0,t.jsx)(n.p,{children:(0,t.jsx)(n.img,{alt:"numbers",src:i(4447).A+"",width:"376",height:"193"})}),"\n",(0,t.jsx)(n.p,{children:"Now if you think about a point in 2D space, it\u2019s just two numbers joined together:"}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-scala",children:'val point = Point(0, 10)\n// point: Point = Point(x = 0.0, y = 10.0)\n\ndiagram(point).render("point")\n'})}),"\n",(0,t.jsx)(n.p,{children:(0,t.jsx)(n.img,{alt:"point",src:i(4967).A+"",width:"226",height:"231"})}),"\n",(0,t.jsx)(n.p,{children:"Can we use the number interpolation to interpolate these two numbers?\nTo answer this question, let\u2019s introduce more abstraction\n(in a great tradition of functional programming)."}),"\n",(0,t.jsxs)(n.p,{children:["A lens ",(0,t.jsx)(n.code,{children:"Lens[A, B]"})," is something that can \u201cfocus\u201d on a piece of data of type ",(0,t.jsx)(n.code,{children:"B"}),"\ninside a data structure of type ",(0,t.jsx)(n.code,{children:"A"})," and provide read-write access to it.\nWe will use the excellent ",(0,t.jsxs)(n.a,{href:"https://github.com/julien-truffaut/Monocle",children:[(0,t.jsx)(n.em,{children:"Monocle"})," library"]}),"\nto create lenses and other optics along the way:"]}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-scala",children:'import monocle.macros.GenLens\n\nval x = GenLens[Point](_.x)\n// x: monocle.package.Lens[Point, Double] = repl.MdocSession$MdocApp$$anon$1@270b030c\nval y = GenLens[Point](_.y)\n// y: monocle.package.Lens[Point, Double] = repl.MdocSession$MdocApp$$anon$2@2fe9f8a4\n\n(diagram(OpticFocus(x, point)).toNamespace("x") +\n diagram(OpticFocus(y, point)).toNamespace("y")).render("x+y")\n'})}),"\n",(0,t.jsx)(n.p,{children:(0,t.jsx)(n.img,{alt:"x+y",src:i(9221).A+"",width:"580",height:"231"})}),"\n",(0,t.jsx)(n.p,{children:"Lenses provide several methods to manipulate data:"}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-scala",children:"x.get(point)\n// res10: Double = 0.0\ny.set(20)(point)\n// res11: Point = Point(x = 0.0, y = 20.0)\ny.modify(_ + 20)(point)\n// res12: Point = Point(x = 0.0, y = 30.0)\n"})}),"\n",(0,t.jsxs)(n.p,{children:["If we can read and write each coordinate field, we can interpolate them separately\nand update the point field by field.\nWe do this by piping ",(0,t.jsx)(n.code,{children:"Interpolation.double"})," through ",(0,t.jsx)(n.code,{children:"x"})," and ",(0,t.jsx)(n.code,{children:"y"})," lenses\nand combining the resulting interpolations:"]}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-scala",children:'val pointInterpolation = (\n x.interpolateWith(Interpolation.double) +\n y.interpolateWith(Interpolation.double))\n// pointInterpolation: Interpolation[Point] = reftree.geometry.Interpolation$$anonfun$apply$4@53ecdb48\n\nval points = pointInterpolation.sample(Point(0, 0), Point(10, 20), 5).toList\n// points: List[Point] = List(\n// Point(x = 0.0, y = 0.0),\n// Point(x = 2.5, y = 5.0),\n// Point(x = 5.0, y = 10.0),\n// Point(x = 7.5, y = 15.0),\n// Point(x = 10.0, y = 20.0)\n// )\n\ndiagram(points).render("points")\n'})}),"\n",(0,t.jsx)(n.p,{children:(0,t.jsx)(n.img,{alt:"points",src:i(7692).A+"",width:"1176",height:"363"})}),"\n",(0,t.jsxs)(n.p,{children:["Of course, ",(0,t.jsx)(n.code,{children:"reftree"})," already defines this as ",(0,t.jsx)(n.code,{children:"Point.interpolation"}),"."]}),"\n",(0,t.jsx)(n.p,{children:"Using the same approach, we can build a polyline interpolator\n(assuming the polylines being interpolated consist of equal number of points):"}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-scala",children:'Data.polyline1\n// res14: Polyline = Polyline(\n// points = List(Point(x = 0.0, y = 10.0), Point(x = 10.0, y = 20.0))\n// )\nData.polyline2\n// res15: Polyline = Polyline(\n// points = List(Point(x = 20.0, y = 30.0), Point(x = 40.0, y = 50.0))\n// )\n\nval polylineInterpolation = (GenLens[Polyline](_.points)\n .interpolateEachWith(Point.interpolation))\n// polylineInterpolation: Interpolation[Polyline] = reftree.geometry.Interpolation$$anonfun$apply$4@762f7035\n\nval polylines = polylineInterpolation.sample(Data.polyline1, Data.polyline2, 3).toList\n// polylines: List[Polyline] = List(\n// Polyline(points = List(Point(x = 0.0, y = 10.0), Point(x = 10.0, y = 20.0))),\n// Polyline(points = List(Point(x = 10.0, y = 20.0), Point(x = 25.0, y = 35.0))),\n// Polyline(points = List(Point(x = 20.0, y = 30.0), Point(x = 40.0, y = 50.0)))\n// )\n\ndiagram(polylines).render("polylines")\n'})}),"\n",(0,t.jsx)(n.p,{children:(0,t.jsx)(n.img,{alt:"polylines",src:i(5846).A+"",width:"1491",height:"664"})}),"\n",(0,t.jsxs)(n.p,{children:["We are finally ready to implement our first substantial interpolator: one that morphs graph edges.\n",(0,t.jsxs)(n.em,{children:["The following approach is inspired by Mike Bostock\u2019s ",(0,t.jsx)(n.a,{href:"https://bl.ocks.org/mbostock/3916621",children:"path tween"}),",\nhowever ",(0,t.jsx)(n.code,{children:"reftree"})," puts more emphasis on types and even includes its own\n",(0,t.jsx)(n.a,{href:"https://github.com/stanch/reftree/blob/master/core/shared/src/main/scala/reftree/geometry/Path.scala",children:"SVG path parser and simplification algorithm"}),"."]})]}),"\n",(0,t.jsx)(n.p,{children:"The resulting animation should look like this:"}),"\n",(0,t.jsx)(n.p,{children:(0,t.jsx)(n.img,{alt:"edges-100",src:i(8914).A+"",width:"361",height:"194"})}),"\n",(0,t.jsxs)(n.p,{children:["An edge is drawn with an ",(0,t.jsx)(n.a,{href:"https://developer.mozilla.org/en-US/docs/Web/SVG/Tutorial/Paths",children:"SVG path"}),",\nwhich consists of several commands, e.g. \u201cmove to\u201d, \u201cline to\u201d, \u201cbezier curve to\u201d.\nHere is a minimized SVG snippet for an actual edge:"]}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-scala",children:'Data.edge1\n// res17: xml.Node = \n\ndiagram(Data.edge1).render("edge")\n'})}),"\n",(0,t.jsx)(n.p,{children:(0,t.jsx)(n.img,{alt:"edge",src:i(1842).A+"",width:"1365",height:"703"})}),"\n",(0,t.jsxs)(n.p,{children:["As you can see, the commands themselves are given in the ",(0,t.jsx)(n.code,{children:"d"})," attribute inside the ",(0,t.jsx)(n.code,{children:"path"})," element\nin a rather obscure format. Luckily, we have lenses and other optics at our disposal\nto plumb through this mess."]}),"\n",(0,t.jsxs)(n.p,{children:["First, let\u2019s get to the ",(0,t.jsx)(n.code,{children:"path"})," element. ",(0,t.jsx)(n.code,{children:"reftree"})," implements a few things that will help us:"]}),"\n",(0,t.jsxs)(n.ul,{children:["\n",(0,t.jsxs)(n.li,{children:[(0,t.jsx)(n.code,{children:"XmlSvgApi"}),", an implementation of several useful SVG operations for ",(0,t.jsx)(n.em,{children:"scala-xml"}),".\nIn particular, if offers a CSS selector-like method for matching elements of certain type and/or class."]}),"\n",(0,t.jsxs)(n.li,{children:["An optic that focuses on an element deep inside XML or any other recursive data structure: ",(0,t.jsx)(n.code,{children:"Optics.collectFirst"}),".\nIt is actually an ",(0,t.jsx)(n.code,{children:"Optional"}),", not a ",(0,t.jsx)(n.code,{children:"Lens"}),", since the element might be missing."]}),"\n"]}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-scala",children:'val edgePathElement = Optics.collectFirst(XmlSvgApi.select("path"))\n// edgePathElement: monocle.package.Optional[xml.Node, xml.Node] = monocle.Optional$$anon$6@9430e9f\n\ndiagram(OpticFocus(edgePathElement, Data.edge1)).render("edgePathElement")\n'})}),"\n",(0,t.jsx)(n.p,{children:(0,t.jsx)(n.img,{alt:"edgePathElement",src:i(3135).A+"",width:"1408",height:"703"})}),"\n",(0,t.jsxs)(n.p,{children:["Next, we need to \u201cdescend\u201d to the ",(0,t.jsx)(n.code,{children:"d"})," attribute. Here is where optics really shine:\nwe can compose ",(0,t.jsx)(n.code,{children:"Optional[A, B]"})," with ",(0,t.jsx)(n.code,{children:"Optional[B, C]"})," to get an ",(0,t.jsx)(n.code,{children:"Optional[A, C]"}),":"]}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-scala",children:'val d = XmlSvgApi.attr("d")\n// d: monocle.package.Optional[xml.Node, String] = monocle.POptional$$anon$1@698ffbcb\nval edgePathString = edgePathElement composeOptional d\n// edgePathString: monocle.POptional[xml.Node, xml.Node, String, String] = monocle.POptional$$anon$1@5c8c4b29\n\ndiagram(OpticFocus(edgePathString, Data.edge1)).render("edgePathString")\n'})}),"\n",(0,t.jsx)(n.p,{children:(0,t.jsx)(n.img,{alt:"edgePathString",src:i(8908).A+"",width:"1403",height:"703"})}),"\n",(0,t.jsx)(n.p,{children:"Next, we will use an isomorphism, another kind of optic, to view\nthe string as a nice case class:"}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-scala",children:'Path.stringIso\n// res21: monocle.package.Iso[String, Path] = monocle.PIso$$anon$9@512f2655\n\nval edgePath = edgePathString composeIso Path.stringIso\n// edgePath: monocle.POptional[xml.Node, xml.Node, Path, Path] = monocle.POptional$$anon$1@4009fd19\n\ndiagram(edgePath.getOption(Data.edge1)).render("edgePath")\n'})}),"\n",(0,t.jsx)(n.p,{children:(0,t.jsx)(n.img,{alt:"edgePath",src:i(3959).A+"",width:"1435",height:"701"})}),"\n",(0,t.jsxs)(n.p,{children:["And finally, another isomorphism takes us from a ",(0,t.jsx)(n.code,{children:"Path"})," to its sampled representation\nas a ",(0,t.jsx)(n.code,{children:"Polyline"}),". (",(0,t.jsx)(n.em,{children:"Purists will say that this is not really an isomorphism because\nit\u2019s not reversible, but with a lot of points you can get pretty close ;)"}),")"]}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-scala",children:'Path.polylineIso(points = 4)\n// res23: monocle.package.Iso[Path, Polyline] = monocle.PIso$$anon$9@56ce1770\n\ndef edgePolyline(points: Int) = edgePath composeIso Path.polylineIso(points)\n\ndiagram(edgePolyline(4).getOption(Data.edge1)).render("edgePolyline")\n'})}),"\n",(0,t.jsx)(n.p,{children:(0,t.jsx)(n.img,{alt:"edgePolyline",src:i(7274).A+"",width:"1727",height:"532"})}),"\n",(0,t.jsx)(n.p,{children:"Let\u2019s interpolate!"}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-scala",children:'def edgeInterpolation(points: Int) = edgePolyline(points).interpolateWith(Polyline.interpolation)\n\ndef edges(points: Int, frames: Int) = (Data.edge1 +:\n edgeInterpolation(points).sample(Data.edge1, Data.edge2, frames, inclusive = false) :+\n Data.edge2)\n\nAnimatedGifRenderer.renderFrames(\n edges(4, 4).map(Frame(_)),\n Paths.get(ImagePath, "visualize", "edges-4.gif"),\n RenderingOptions(density = 200),\n AnimationOptions(framesPerSecond = 1)\n)\n\nAnimatedGifRenderer.renderFrames(\n edges(100, 32).map(Frame(_)),\n Paths.get(ImagePath, "visualize", "edges-100.gif"),\n RenderingOptions(density = 200),\n AnimationOptions(framesPerSecond = 8)\n)\n'})}),"\n",(0,t.jsx)(n.p,{children:"With 4 points and 4 frames:"}),"\n",(0,t.jsx)(n.p,{children:(0,t.jsx)(n.img,{alt:"edges-4",src:i(5047).A+"",width:"361",height:"194"})}),"\n",(0,t.jsx)(n.p,{children:"With 100 points and 32 frames:"}),"\n",(0,t.jsx)(n.p,{children:(0,t.jsx)(n.img,{alt:"edges-100",src:i(8914).A+"",width:"361",height:"194"})}),"\n",(0,t.jsx)(n.p,{children:(0,t.jsxs)(n.em,{children:["Interpolating the entire image is left as an exercise for the reader,\nalthough the impatient will find the complete implementation\n",(0,t.jsx)(n.a,{href:"https://github.com/stanch/reftree/blob/master/core/shared/src/main/scala/reftree/svg/animation/GraphInterpolation.scala",children:"here"}),"."]})}),"\n",(0,t.jsxs)(n.p,{children:["Notice that we never touched XML directly.\nIn fact, equipped with the same set of optics for another format or representation,\nwe would be able to operate on it without changing the code too much.\nCase in point: ",(0,t.jsx)(n.code,{children:"reftree"})," supports both\n",(0,t.jsx)(n.a,{href:"https://github.com/stanch/reftree/blob/master/core/jvm/src/main/scala/reftree/svg/XmlSvgApi.scala",children:(0,t.jsx)(n.em,{children:"scala-xml"})})," and\n",(0,t.jsx)(n.a,{href:"https://github.com/stanch/reftree/blob/master/core/js/src/main/scala/reftree/svg/DomSvgApi.scala",children:(0,t.jsx)(n.em,{children:"scala-js-dom"})})," (for Scala.js),\nwith only 50 lines of implementation-specific code for each backend.\nThis goes to show the flexibility and usefulness of optics."]}),"\n",(0,t.jsx)(n.h2,{id:"zipping-it-up",children:"Zipping it up"}),"\n",(0,t.jsxs)(n.p,{children:["In the previous section we saw ",(0,t.jsx)(n.code,{children:"Optics.collectFirst"})," \u2014 an optic that is able to perform\nmodifications deep inside SVG. How do we go about implementing something like this,\nor, more generally, how do we edit recursive data structures such as XML?"]}),"\n",(0,t.jsxs)(n.p,{children:["This solution is called a \u201cZipper\u201d, and was introduced by G\xe9rard Huet in 1997.\nIt consists of a \u201ccursor\u201d pointing to a location anywhere in a tree \u2014 \u201ccurrent focus\u201d.\nThe cursor can be moved freely with operations like ",(0,t.jsx)(n.code,{children:"moveDownLeft"}),", ",(0,t.jsx)(n.code,{children:"moveRight"}),", ",(0,t.jsx)(n.code,{children:"moveUp"}),", etc.\nCurrent focus can be updated, deleted, or new nodes can be inserted to its left or right.\nZippers are immutable, and every operation returns a new Zipper.\nAll the changes made to the tree can be committed, yielding a new modified version of the original tree."]}),"\n",(0,t.jsxs)(n.p,{children:["My ",(0,t.jsx)(n.a,{href:"https://github.com/stanch/zipper#zipper--an-implementation-of-huets-zipper",children:"zipper library"}),"\nprovides a few useful movements and operations. Just like optics, it\u2019s rather generic and flexible.\nThe zipper can operate on any type, as long as an instance of the ",(0,t.jsx)(n.code,{children:"Unzip"})," typeclass is available,\nwhich can be automatically derived in many cases.\n(",(0,t.jsxs)(n.em,{children:["Note that the derivation of ",(0,t.jsx)(n.code,{children:"Unzip"})," for SVG can be found\n",(0,t.jsx)(n.a,{href:"https://github.com/stanch/reftree/blob/master/core/shared/src/main/scala/reftree/svg/api/BaseSvgApi.scala",children:"here"}),"."]}),")"]}),"\n",(0,t.jsx)(n.p,{children:"Consider a simple XML tree:"}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-scala",children:'Data.simpleXml\n// res27: xml.Node = \n\ndiagram(Data.simpleXml).render("simpleXml")\n'})}),"\n",(0,t.jsx)(n.p,{children:(0,t.jsx)(n.img,{alt:"simpleXml",src:i(4792).A+"",width:"1277",height:"703"})}),"\n",(0,t.jsx)(n.p,{children:"When we wrap a Zipper around this tree, it does not look very interesting yet:"}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-scala",children:'import zipper.Zipper\n\nval zipper1 = Zipper(Data.simpleXml)\n// zipper1: Zipper[xml.Node] = Zipper(List(),,List(),None)\n\n(diagram(Data.simpleXml) + diagram(zipper1)).render("zipper1")\n'})}),"\n",(0,t.jsx)(n.p,{children:(0,t.jsx)(n.img,{alt:"zipper1",src:i(3618).A+"",width:"1321",height:"872"})}),"\n",(0,t.jsx)(n.p,{children:"We can see that it just points to the original tree.\nIn this case the focus is the root of the tree, which has no siblings,\nand the parent zipper does not exist, since we are at the top level."}),"\n",(0,t.jsx)(n.p,{children:"To move down the tree, we \u201cunzip\u201d it, separating the child nodes into\nthe focused node and its left and right siblings:"}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-scala",children:'val zipper2 = zipper1.moveDownLeft\n// zipper2: Zipper[xml.Node] = Zipper(List(),,List(, , ),Some(Zipper(List(),,List(),None)))\n\n(diagram(zipper1) + diagram(zipper2)).render("zipper1+2")\n'})}),"\n",(0,t.jsx)(n.p,{children:(0,t.jsx)(n.img,{alt:"zipper1+2",src:i(2385).A+"",width:"1366",height:"1042"})}),"\n",(0,t.jsx)(n.p,{children:"The new Zipper links to the old one,\nwhich will allow us to return to the root of the tree when we are done applying changes.\nThis link however prevents us from seeing the picture clearly.\nLet\u2019s look at the second zipper alone:"}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-scala",children:'diagram(zipper2).render("zipper2b")\n'})}),"\n",(0,t.jsx)(n.p,{children:(0,t.jsx)(n.img,{alt:"zipper2b",src:i(5823).A+"",width:"1178",height:"872"})}),"\n",(0,t.jsxs)(n.p,{children:["Great! We have ",(0,t.jsx)(n.code,{children:"2"})," in focus and ",(0,t.jsx)(n.code,{children:"3, 4, 5"})," as right siblings. What happens if we move right a bit?"]}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-scala",children:'val zipper3 = zipper2.moveRightBy(2)\n// zipper3: Zipper[xml.Node] = Zipper(List(, ),,List(),Some(Zipper(List(),,List(),None)))\n\ndiagram(zipper3).render("zipper3")\n'})}),"\n",(0,t.jsx)(n.p,{children:(0,t.jsx)(n.img,{alt:"zipper3",src:i(1248).A+"",width:"1113",height:"872"})}),"\n",(0,t.jsx)(n.p,{children:"This is interesting! Notice that the left siblings are \u201cinverted\u201d.\nThis allows to move left and right in constant time, because the sibling\nadjacent to the focus is always at the head of the list."}),"\n",(0,t.jsx)(n.p,{children:"This also allows us to insert new siblings easily:"}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-scala",children:'val zipper4 = zipper3.insertLeft()\n// zipper4: Zipper[xml.Node] = Zipper(List(, , ),,List(),Some(Zipper(List(),,List(),None)))\n\ndiagram(zipper4).render("zipper4")\n'})}),"\n",(0,t.jsx)(n.p,{children:(0,t.jsx)(n.img,{alt:"zipper4",src:i(4095).A+"",width:"1235",height:"872"})}),"\n",(0,t.jsx)(n.p,{children:"And, as you might know, we can delete nodes and update the focus:"}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-scala",children:'val zipper5 = zipper4.deleteAndMoveRight.set()\n// zipper5: Zipper[xml.Node] = Zipper(List(, , ),,List(),Some(Zipper(List(),,List(),None)))\n\ndiagram(zipper5).render("zipper5")\n'})}),"\n",(0,t.jsx)(n.p,{children:(0,t.jsx)(n.img,{alt:"zipper5",src:i(5078).A+"",width:"556",height:"667"})}),"\n",(0,t.jsx)(n.p,{children:"Finally, when we move up, the siblings at the current level are \u201czipped\u201d\ntogether and their parent node is updated:"}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-scala",children:'val zipper6 = zipper5.moveUp\n// zipper6: Zipper[xml.Node] = Zipper(List(),,List(),None)\n\ndiagram(zipper6).render("zipper6")\n'})}),"\n",(0,t.jsx)(n.p,{children:(0,t.jsx)(n.img,{alt:"zipper6",src:i(6317).A+"",width:"794",height:"703"})}),"\n",(0,t.jsxs)(n.p,{children:["When we are done editing, the ",(0,t.jsx)(n.code,{children:".commit"})," shorthand can be used for going\nall the way up (applying all the changes) and returning the focus.\nNotice how all the unchanged nodes are shared between the old and the new XML."]}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-scala",children:'val notSoSimpleXml = zipper6.commit\n// notSoSimpleXml: xml.Node = \n\n(diagram(Data.simpleXml) + diagram(notSoSimpleXml)).render("notSoSimpleXml")\n'})}),"\n",(0,t.jsx)(n.p,{children:(0,t.jsx)(n.img,{alt:"notSoSimpleXml",src:i(4259).A+"",width:"1560",height:"703"})}),"\n",(0,t.jsx)(n.p,{children:(0,t.jsxs)(n.em,{children:["Using an XML zipper, a determined reader can easily implement advanced lenses,\nsuch as ",(0,t.jsx)(n.code,{children:"Optics.collectFirst"}),", ",(0,t.jsx)(n.code,{children:"Optics.collectLeftByKey"}),", etc, all found\n",(0,t.jsx)(n.a,{href:"https://github.com/stanch/reftree/blob/master/core/shared/src/main/scala/reftree/util/Optics.scala",children:"here"}),"."]})}),"\n",(0,t.jsx)(n.p,{children:"To conclude, here is an animation of a zipper and the tree it operates on\n(from my previous talk), produced (as we know now) not without zippers\u2019 help:"}),"\n",(0,t.jsx)(n.p,{children:(0,t.jsx)(n.img,{alt:"tree+zipper",src:i(1864).A+"",width:"1241",height:"690"})}),"\n",(0,t.jsxs)(n.p,{children:["That\u2019s all! Thank you for reading this far.\nI hope you are leaving this page with some great ",(0,t.jsx)(n.code,{children:"reftree"})," use-cases in mind :)"]})]})}function g(e={}){const{wrapper:n}={...(0,s.R)(),...e.components};return n?(0,t.jsx)(n,{...e,children:(0,t.jsx)(h,{...e})}):h(e)}},9696:(e,n,i)=>{i.d(n,{A:()=>t});const t=i.p+"assets/images/queue-668a708310e8e78475fbc9b757c69eb3.gif"},1864:(e,n,i)=>{i.d(n,{A:()=>t});const t=i.p+"assets/images/tree+zipper-b1cf73da287eda26f64b667ce8dcf311.gif"},9341:(e,n,i)=>{i.d(n,{A:()=>t});const t="data:image/png;base64,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"},108:(e,n,i)=>{i.d(n,{A:()=>t});const t=i.p+"assets/images/bob-6e8dad95c440d488af22193a37ae0d7f.png"},1842:(e,n,i)=>{i.d(n,{A:()=>t});const t=i.p+"assets/images/edge-0a8b76cae995e0a8c485063cb500de8a.png"},3959:(e,n,i)=>{i.d(n,{A:()=>t});const t=i.p+"assets/images/edgePath-e11472fa0a55e29fd9de9876ff597ceb.png"},3135:(e,n,i)=>{i.d(n,{A:()=>t});const t=i.p+"assets/images/edgePathElement-8070f24cd6faf257dbf2357bbc26ab75.png"},8908:(e,n,i)=>{i.d(n,{A:()=>t});const t=i.p+"assets/images/edgePathString-e95d3068168658ba325a9e873dbf2612.png"},7274:(e,n,i)=>{i.d(n,{A:()=>t});const t=i.p+"assets/images/edgePolyline-7c4044c84e38ce242eefda017e1e188c.png"},8914:(e,n,i)=>{i.d(n,{A:()=>t});const t=i.p+"assets/images/edges-100-9cbc68a0debf543244c33b7d26b22709.gif"},5047:(e,n,i)=>{i.d(n,{A:()=>t});const t=i.p+"assets/images/edges-4-788922eab8ebbf8f1d0ffaa9a071132b.gif"},2634:(e,n,i)=>{i.d(n,{A:()=>t});const t=i.p+"assets/images/generic-2c235d68ea9ba3134644dfbbfcfc8d0e.png"},4259:(e,n,i)=>{i.d(n,{A:()=>t});const t=i.p+"assets/images/notSoSimpleXml-d176756e1aae1946e6d6fe906c9d9bbc.png"},4447:(e,n,i)=>{i.d(n,{A:()=>t});const t="data:image/png;base64,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"},4967:(e,n,i)=>{i.d(n,{A:()=>t});const t="data:image/png;base64,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"},7692:(e,n,i)=>{i.d(n,{A:()=>t});const t=i.p+"assets/images/points-5c9c6c00db5175c0d948a09163a40637.png"},5846:(e,n,i)=>{i.d(n,{A:()=>t});const t=i.p+"assets/images/polylines-00e6358bcbdecbf9807c0bc2ade2d3f9.png"},3342:(e,n,i)=>{i.d(n,{A:()=>t});const t=i.p+"assets/images/reftree-db3d1866df4838172ab51dd6b7a592c1.png"},4792:(e,n,i)=>{i.d(n,{A:()=>t});const t=i.p+"assets/images/simpleXml-33e8a5f26cea7dd70c3808d86690488b.png"},9221:(e,n,i)=>{i.d(n,{A:()=>t});const t=i.p+"assets/images/x+y-c7fbf58ef886ad0cf3d32f18259c15be.png"},2385:(e,n,i)=>{i.d(n,{A:()=>t});const t=i.p+"assets/images/zipper1+2-bc931dc2a9a17d909a27f5ae5d1e095b.png"},3618:(e,n,i)=>{i.d(n,{A:()=>t});const t=i.p+"assets/images/zipper1-5f0e8ccac0548193671b75d4f96125e4.png"},5823:(e,n,i)=>{i.d(n,{A:()=>t});const t=i.p+"assets/images/zipper2b-b4d938aec16be4bc5d0601737ed9bd53.png"},1248:(e,n,i)=>{i.d(n,{A:()=>t});const t=i.p+"assets/images/zipper3-25b272311b7ec6ce2e1b0ce993d16055.png"},4095:(e,n,i)=>{i.d(n,{A:()=>t});const t=i.p+"assets/images/zipper4-cbb2cf5c5de815041892e7f3b09703f8.png"},5078:(e,n,i)=>{i.d(n,{A:()=>t});const t=i.p+"assets/images/zipper5-56ccb910b4b659418162c70c7d32c364.png"},6317:(e,n,i)=>{i.d(n,{A:()=>t});const t=i.p+"assets/images/zipper6-5e74a23116f43ca88bf512b027cbdd9e.png"}}]); \ No newline at end of file diff --git a/assets/js/71ffd77d.6ce84eb6.js b/assets/js/71ffd77d.6ce84eb6.js new file mode 100644 index 0000000..eee46e8 --- /dev/null +++ b/assets/js/71ffd77d.6ce84eb6.js @@ -0,0 +1 @@ +"use strict";(self.webpackChunkreftree=self.webpackChunkreftree||[]).push([[533],{3691:(e,t,r)=>{r.r(t),r.d(t,{assets:()=>u,contentTitle:()=>o,default:()=>p,frontMatter:()=>l,metadata:()=>c,toc:()=>d});var n=r(4848),a=r(8453),i=r(1470),s=r(9365);const l={sidebar_position:2},o="Getting started",c={id:"GettingStarted",title:"Getting started",description:"To use this library you will need to have GraphViz installed (and have dot on your PATH).",source:"@site/../site-gen/target/mdoc/GettingStarted.md",sourceDirName:".",slug:"/GettingStarted",permalink:"/reftree/docs/GettingStarted",draft:!1,unlisted:!1,editUrl:"https://github.com/stanch/reftree/tree/main/docs/../site-gen/target/mdoc/GettingStarted.md",tags:[],version:"current",sidebarPosition:2,frontMatter:{sidebar_position:2},sidebar:"mainSidebar",previous:{title:"Overview",permalink:"/reftree/docs/"},next:{title:"User guide",permalink:"/reftree/docs/Guide"}},u={},d=[{value:"Interactive usage",id:"interactive-usage",level:2},{value:"Including in your project",id:"including-in-your-project",level:2},{value:"Minimal example",id:"minimal-example",level:2}];function h(e){const t={a:"a",code:"code",em:"em",h1:"h1",h2:"h2",img:"img",p:"p",pre:"pre",...(0,a.R)(),...e.components};return(0,n.jsxs)(n.Fragment,{children:[(0,n.jsx)(t.h1,{id:"getting-started",children:"Getting started"}),"\n",(0,n.jsxs)(t.p,{children:["To use this library you will need to have ",(0,n.jsx)(t.a,{href:"http://www.graphviz.org/",children:"GraphViz"})," installed (and have ",(0,n.jsx)(t.code,{children:"dot"})," on your ",(0,n.jsx)(t.code,{children:"PATH"}),").\nI also recommend to install the ",(0,n.jsx)(t.a,{href:"https://github.com/adobe-fonts/source-code-pro",children:"Source Code Pro"})," fonts (regular and ",(0,n.jsx)(t.em,{children:"italic"}),"),\nas I find they look the best among the free options and therefore are used by default."]}),"\n",(0,n.jsxs)(t.p,{children:["For viewing PNG and animated GIF on Linux I recommend ",(0,n.jsx)(t.code,{children:"eog"})," and ",(0,n.jsx)(t.code,{children:"gifview"})," respectively."]}),"\n",(0,n.jsx)(t.h2,{id:"interactive-usage",children:"Interactive usage"}),"\n",(0,n.jsx)(t.p,{children:"To jump into an interactive session:"}),"\n",(0,n.jsx)(t.pre,{children:(0,n.jsx)(t.code,{children:"$ git clone https://github.com/stanch/reftree\n$ cd reftree\n$ sbt demo\n@ render(List(1, 2, 3))\n// display diagram.png with your favorite image viewer\n"})}),"\n",(0,n.jsx)(t.h2,{id:"including-in-your-project",children:"Including in your project"}),"\n",(0,n.jsxs)(t.p,{children:[(0,n.jsx)(t.code,{children:"reftree"})," is available for Scala 2.12 and 2.13. You can depend on the library by adding these lines to your ",(0,n.jsx)(t.code,{children:"build.sbt"}),":"]}),"\n","\n",(0,n.jsxs)(i.A,{groupId:"platform",children:[(0,n.jsx)(s.A,{value:"jvm",label:"JVM",default:!0,children:(0,n.jsx)(t.pre,{children:(0,n.jsx)(t.code,{className:"language-scala",children:'libraryDependencies += "io.github.stanch" %% "reftree" % "1.5.0"\n'})})}),(0,n.jsx)(s.A,{value:"js",label:"Scala.js 1.16+",children:(0,n.jsx)(t.pre,{children:(0,n.jsx)(t.code,{className:"language-scala",children:'libraryDependencies += "io.github.stanch" %%% "reftree" % "1.5.0"\n'})})})]}),"\n",(0,n.jsx)(t.h2,{id:"minimal-example",children:"Minimal example"}),"\n",(0,n.jsx)(t.pre,{children:(0,n.jsx)(t.code,{className:"language-scala",children:"import reftree.render.{Renderer, RenderingOptions}\nimport reftree.diagram.Diagram\nimport java.nio.file.Paths\n"})}),"\n",(0,n.jsx)(t.pre,{children:(0,n.jsx)(t.code,{className:"language-scala",children:'val ImagePath = "images"\n'})}),"\n",(0,n.jsx)(t.pre,{children:(0,n.jsx)(t.code,{className:"language-scala",children:'val renderer = Renderer(\n renderingOptions = RenderingOptions(density = 100),\n directory = Paths.get(ImagePath, "overview")\n)\nimport renderer._\n\ncase class Person(firstName: String, age: Int)\n\nDiagram.sourceCodeCaption(Person("Bob", 42)).render("example")\n'})}),"\n",(0,n.jsxs)(t.p,{children:["This generates ",(0,n.jsx)(t.code,{children:"images/overview/example.png"})," with the following image:"]}),"\n",(0,n.jsx)(t.p,{children:(0,n.jsx)(t.img,{alt:"bob",src:r(8710).A+"",width:"367",height:"363"})}),"\n",(0,n.jsxs)(t.p,{children:["For more details, please refer to the ",(0,n.jsx)(t.a,{href:"/reftree/docs/Guide",children:"guide"}),"."]})]})}function p(e={}){const{wrapper:t}={...(0,a.R)(),...e.components};return t?(0,n.jsx)(t,{...e,children:(0,n.jsx)(h,{...e})}):h(e)}},9365:(e,t,r)=>{r.d(t,{A:()=>s});r(6540);var n=r(4164);const a={tabItem:"tabItem_Ymn6"};var i=r(4848);function s(e){let{children:t,hidden:r,className:s}=e;return(0,i.jsx)("div",{role:"tabpanel",className:(0,n.A)(a.tabItem,s),hidden:r,children:t})}},1470:(e,t,r)=>{r.d(t,{A:()=>w});var n=r(6540),a=r(4164),i=r(3104),s=r(6347),l=r(205),o=r(7485),c=r(1682),u=r(679);function d(e){return n.Children.toArray(e).filter((e=>"\n"!==e)).map((e=>{if(!e||(0,n.isValidElement)(e)&&function(e){const{props:t}=e;return!!t&&"object"==typeof t&&"value"in t}(e))return e;throw new Error(`Docusaurus error: Bad child <${"string"==typeof e.type?e.type:e.type.name}>: all children of the component should be , and every should have a unique "value" prop.`)}))?.filter(Boolean)??[]}function h(e){const{values:t,children:r}=e;return(0,n.useMemo)((()=>{const e=t??function(e){return d(e).map((e=>{let{props:{value:t,label:r,attributes:n,default:a}}=e;return{value:t,label:r,attributes:n,default:a}}))}(r);return function(e){const t=(0,c.X)(e,((e,t)=>e.value===t.value));if(t.length>0)throw new Error(`Docusaurus error: Duplicate values "${t.map((e=>e.value)).join(", ")}" found in . Every value needs to be unique.`)}(e),e}),[t,r])}function p(e){let{value:t,tabValues:r}=e;return r.some((e=>e.value===t))}function m(e){let{queryString:t=!1,groupId:r}=e;const a=(0,s.W6)(),i=function(e){let{queryString:t=!1,groupId:r}=e;if("string"==typeof t)return t;if(!1===t)return null;if(!0===t&&!r)throw new Error('Docusaurus error: The component groupId prop is required if queryString=true, because this value is used as the search param name. You can also provide an explicit value such as queryString="my-search-param".');return r??null}({queryString:t,groupId:r});return[(0,o.aZ)(i),(0,n.useCallback)((e=>{if(!i)return;const t=new URLSearchParams(a.location.search);t.set(i,e),a.replace({...a.location,search:t.toString()})}),[i,a])]}function g(e){const{defaultValue:t,queryString:r=!1,groupId:a}=e,i=h(e),[s,o]=(0,n.useState)((()=>function(e){let{defaultValue:t,tabValues:r}=e;if(0===r.length)throw new Error("Docusaurus error: the component requires at least one children component");if(t){if(!p({value:t,tabValues:r}))throw new Error(`Docusaurus error: The has a defaultValue "${t}" but none of its children has the corresponding value. Available values are: ${r.map((e=>e.value)).join(", ")}. If you intend to show no default tab, use defaultValue={null} instead.`);return t}const n=r.find((e=>e.default))??r[0];if(!n)throw new Error("Unexpected error: 0 tabValues");return n.value}({defaultValue:t,tabValues:i}))),[c,d]=m({queryString:r,groupId:a}),[g,f]=function(e){let{groupId:t}=e;const r=function(e){return e?`docusaurus.tab.${e}`:null}(t),[a,i]=(0,u.Dv)(r);return[a,(0,n.useCallback)((e=>{r&&i.set(e)}),[r,i])]}({groupId:a}),b=(()=>{const e=c??g;return p({value:e,tabValues:i})?e:null})();(0,l.A)((()=>{b&&o(b)}),[b]);return{selectedValue:s,selectValue:(0,n.useCallback)((e=>{if(!p({value:e,tabValues:i}))throw new Error(`Can't select invalid tab value=${e}`);o(e),d(e),f(e)}),[d,f,i]),tabValues:i}}var f=r(2303);const b={tabList:"tabList__CuJ",tabItem:"tabItem_LNqP"};var v=r(4848);function x(e){let{className:t,block:r,selectedValue:n,selectValue:s,tabValues:l}=e;const o=[],{blockElementScrollPositionUntilNextRender:c}=(0,i.a_)(),u=e=>{const t=e.currentTarget,r=o.indexOf(t),a=l[r].value;a!==n&&(c(t),s(a))},d=e=>{let t=null;switch(e.key){case"Enter":u(e);break;case"ArrowRight":{const r=o.indexOf(e.currentTarget)+1;t=o[r]??o[0];break}case"ArrowLeft":{const r=o.indexOf(e.currentTarget)-1;t=o[r]??o[o.length-1];break}}t?.focus()};return(0,v.jsx)("ul",{role:"tablist","aria-orientation":"horizontal",className:(0,a.A)("tabs",{"tabs--block":r},t),children:l.map((e=>{let{value:t,label:r,attributes:i}=e;return(0,v.jsx)("li",{role:"tab",tabIndex:n===t?0:-1,"aria-selected":n===t,ref:e=>o.push(e),onKeyDown:d,onClick:u,...i,className:(0,a.A)("tabs__item",b.tabItem,i?.className,{"tabs__item--active":n===t}),children:r??t},t)}))})}function j(e){let{lazy:t,children:r,selectedValue:a}=e;const i=(Array.isArray(r)?r:[r]).filter(Boolean);if(t){const e=i.find((e=>e.props.value===a));return e?(0,n.cloneElement)(e,{className:"margin-top--md"}):null}return(0,v.jsx)("div",{className:"margin-top--md",children:i.map(((e,t)=>(0,n.cloneElement)(e,{key:t,hidden:e.props.value!==a})))})}function y(e){const t=g(e);return(0,v.jsxs)("div",{className:(0,a.A)("tabs-container",b.tabList),children:[(0,v.jsx)(x,{...t,...e}),(0,v.jsx)(j,{...t,...e})]})}function w(e){const t=(0,f.A)();return(0,v.jsx)(y,{...e,children:d(e.children)},String(t))}},8710:(e,t,r)=>{r.d(t,{A:()=>n});const n=r.p+"assets/images/example-279830c17e2bfcec051c7a3c6280e740.png"},8453:(e,t,r)=>{r.d(t,{R:()=>s,x:()=>l});var n=r(6540);const a={},i=n.createContext(a);function s(e){const t=n.useContext(i);return n.useMemo((function(){return"function"==typeof e?e(t):{...t,...e}}),[t,e])}function l(e){let t;return t=e.disableParentContext?"function"==typeof e.components?e.components(a):e.components||a:s(e.components),n.createElement(i.Provider,{value:t},e.children)}}}]); \ No newline at end of file diff --git a/assets/js/71ffd77d.8264f7e5.js b/assets/js/71ffd77d.8264f7e5.js deleted file mode 100644 index fa91a03..0000000 --- a/assets/js/71ffd77d.8264f7e5.js +++ /dev/null @@ -1 +0,0 @@ -"use strict";(self.webpackChunkreftree=self.webpackChunkreftree||[]).push([[533],{3691:(e,t,r)=>{r.r(t),r.d(t,{assets:()=>u,contentTitle:()=>o,default:()=>p,frontMatter:()=>l,metadata:()=>c,toc:()=>d});var n=r(4848),a=r(8453),i=r(1470),s=r(9365);const l={sidebar_position:2},o="Getting started",c={id:"GettingStarted",title:"Getting started",description:"To use this library you will need to have GraphViz installed (and have dot on your PATH).",source:"@site/../site-gen/target/mdoc/GettingStarted.md",sourceDirName:".",slug:"/GettingStarted",permalink:"/reftree/docs/GettingStarted",draft:!1,unlisted:!1,editUrl:"https://github.com/stanch/reftree/tree/main/docs/../site-gen/target/mdoc/GettingStarted.md",tags:[],version:"current",sidebarPosition:2,frontMatter:{sidebar_position:2},sidebar:"mainSidebar",previous:{title:"Overview",permalink:"/reftree/docs/"},next:{title:"User guide",permalink:"/reftree/docs/Guide"}},u={},d=[{value:"Interactive usage",id:"interactive-usage",level:2},{value:"Including in your project",id:"including-in-your-project",level:2},{value:"Minimal example",id:"minimal-example",level:2}];function h(e){const t={a:"a",code:"code",em:"em",h1:"h1",h2:"h2",img:"img",p:"p",pre:"pre",...(0,a.R)(),...e.components};return(0,n.jsxs)(n.Fragment,{children:[(0,n.jsx)(t.h1,{id:"getting-started",children:"Getting started"}),"\n",(0,n.jsxs)(t.p,{children:["To use this library you will need to have ",(0,n.jsx)(t.a,{href:"http://www.graphviz.org/",children:"GraphViz"})," installed (and have ",(0,n.jsx)(t.code,{children:"dot"})," on your ",(0,n.jsx)(t.code,{children:"PATH"}),").\nI also recommend to install the ",(0,n.jsx)(t.a,{href:"https://github.com/adobe-fonts/source-code-pro",children:"Source Code Pro"})," fonts (regular and ",(0,n.jsx)(t.em,{children:"italic"}),"),\nas I find they look the best among the free options and therefore are used by default."]}),"\n",(0,n.jsxs)(t.p,{children:["For viewing PNG and animated GIF on Linux I recommend ",(0,n.jsx)(t.code,{children:"eog"})," and ",(0,n.jsx)(t.code,{children:"gifview"})," respectively."]}),"\n",(0,n.jsx)(t.h2,{id:"interactive-usage",children:"Interactive usage"}),"\n",(0,n.jsx)(t.p,{children:"To jump into an interactive session:"}),"\n",(0,n.jsx)(t.pre,{children:(0,n.jsx)(t.code,{children:"$ git clone https://github.com/stanch/reftree\n$ cd reftree\n$ sbt demo\n@ render(List(1, 2, 3))\n// display diagram.png with your favorite image viewer\n"})}),"\n",(0,n.jsx)(t.h2,{id:"including-in-your-project",children:"Including in your project"}),"\n",(0,n.jsxs)(t.p,{children:["You can depend on the library by adding these lines to your ",(0,n.jsx)(t.code,{children:"build.sbt"}),":"]}),"\n","\n",(0,n.jsxs)(i.A,{groupId:"platform",children:[(0,n.jsx)(s.A,{value:"jvm",label:"JVM",default:!0,children:(0,n.jsx)(t.pre,{children:(0,n.jsx)(t.code,{className:"language-scala",children:'libraryDependencies += "io.github.stanch" %% "reftree" % "1.5.0"\n'})})}),(0,n.jsx)(s.A,{value:"js",label:"Scala.js",children:(0,n.jsx)(t.pre,{children:(0,n.jsx)(t.code,{className:"language-scala",children:'libraryDependencies += "io.github.stanch" %%% "reftree" % "1.5.0"\n'})})})]}),"\n",(0,n.jsx)(t.h2,{id:"minimal-example",children:"Minimal example"}),"\n",(0,n.jsx)(t.pre,{children:(0,n.jsx)(t.code,{className:"language-scala",children:"import reftree.render.{Renderer, RenderingOptions}\nimport reftree.diagram.Diagram\nimport java.nio.file.Paths\n"})}),"\n",(0,n.jsx)(t.pre,{children:(0,n.jsx)(t.code,{className:"language-scala",children:'val ImagePath = "images"\n'})}),"\n",(0,n.jsx)(t.pre,{children:(0,n.jsx)(t.code,{className:"language-scala",children:'val renderer = Renderer(\n renderingOptions = RenderingOptions(density = 100),\n directory = Paths.get(ImagePath, "overview")\n)\nimport renderer._\n\ncase class Person(firstName: String, age: Int)\n\nDiagram.sourceCodeCaption(Person("Bob", 42)).render("example")\n'})}),"\n",(0,n.jsxs)(t.p,{children:["This generates ",(0,n.jsx)(t.code,{children:"images/overview/example.png"})," with the following image:"]}),"\n",(0,n.jsx)(t.p,{children:(0,n.jsx)(t.img,{alt:"bob",src:r(8710).A+"",width:"367",height:"363"})}),"\n",(0,n.jsxs)(t.p,{children:["For more details, please refer to the ",(0,n.jsx)(t.a,{href:"/reftree/docs/Guide",children:"guide"}),"."]})]})}function p(e={}){const{wrapper:t}={...(0,a.R)(),...e.components};return t?(0,n.jsx)(t,{...e,children:(0,n.jsx)(h,{...e})}):h(e)}},9365:(e,t,r)=>{r.d(t,{A:()=>s});r(6540);var n=r(4164);const a={tabItem:"tabItem_Ymn6"};var i=r(4848);function s(e){let{children:t,hidden:r,className:s}=e;return(0,i.jsx)("div",{role:"tabpanel",className:(0,n.A)(a.tabItem,s),hidden:r,children:t})}},1470:(e,t,r)=>{r.d(t,{A:()=>w});var n=r(6540),a=r(4164),i=r(3104),s=r(6347),l=r(205),o=r(7485),c=r(1682),u=r(679);function d(e){return n.Children.toArray(e).filter((e=>"\n"!==e)).map((e=>{if(!e||(0,n.isValidElement)(e)&&function(e){const{props:t}=e;return!!t&&"object"==typeof t&&"value"in t}(e))return e;throw new Error(`Docusaurus error: Bad child <${"string"==typeof e.type?e.type:e.type.name}>: all children of the component should be , and every should have a unique "value" prop.`)}))?.filter(Boolean)??[]}function h(e){const{values:t,children:r}=e;return(0,n.useMemo)((()=>{const e=t??function(e){return d(e).map((e=>{let{props:{value:t,label:r,attributes:n,default:a}}=e;return{value:t,label:r,attributes:n,default:a}}))}(r);return function(e){const t=(0,c.X)(e,((e,t)=>e.value===t.value));if(t.length>0)throw new Error(`Docusaurus error: Duplicate values "${t.map((e=>e.value)).join(", ")}" found in . Every value needs to be unique.`)}(e),e}),[t,r])}function p(e){let{value:t,tabValues:r}=e;return r.some((e=>e.value===t))}function m(e){let{queryString:t=!1,groupId:r}=e;const a=(0,s.W6)(),i=function(e){let{queryString:t=!1,groupId:r}=e;if("string"==typeof t)return t;if(!1===t)return null;if(!0===t&&!r)throw new Error('Docusaurus error: The component groupId prop is required if queryString=true, because this value is used as the search param name. You can also provide an explicit value such as queryString="my-search-param".');return r??null}({queryString:t,groupId:r});return[(0,o.aZ)(i),(0,n.useCallback)((e=>{if(!i)return;const t=new URLSearchParams(a.location.search);t.set(i,e),a.replace({...a.location,search:t.toString()})}),[i,a])]}function g(e){const{defaultValue:t,queryString:r=!1,groupId:a}=e,i=h(e),[s,o]=(0,n.useState)((()=>function(e){let{defaultValue:t,tabValues:r}=e;if(0===r.length)throw new Error("Docusaurus error: the component requires at least one children component");if(t){if(!p({value:t,tabValues:r}))throw new Error(`Docusaurus error: The has a defaultValue "${t}" but none of its children has the corresponding value. Available values are: ${r.map((e=>e.value)).join(", ")}. If you intend to show no default tab, use defaultValue={null} instead.`);return t}const n=r.find((e=>e.default))??r[0];if(!n)throw new Error("Unexpected error: 0 tabValues");return n.value}({defaultValue:t,tabValues:i}))),[c,d]=m({queryString:r,groupId:a}),[g,f]=function(e){let{groupId:t}=e;const r=function(e){return e?`docusaurus.tab.${e}`:null}(t),[a,i]=(0,u.Dv)(r);return[a,(0,n.useCallback)((e=>{r&&i.set(e)}),[r,i])]}({groupId:a}),b=(()=>{const e=c??g;return p({value:e,tabValues:i})?e:null})();(0,l.A)((()=>{b&&o(b)}),[b]);return{selectedValue:s,selectValue:(0,n.useCallback)((e=>{if(!p({value:e,tabValues:i}))throw new Error(`Can't select invalid tab value=${e}`);o(e),d(e),f(e)}),[d,f,i]),tabValues:i}}var f=r(2303);const b={tabList:"tabList__CuJ",tabItem:"tabItem_LNqP"};var v=r(4848);function x(e){let{className:t,block:r,selectedValue:n,selectValue:s,tabValues:l}=e;const o=[],{blockElementScrollPositionUntilNextRender:c}=(0,i.a_)(),u=e=>{const t=e.currentTarget,r=o.indexOf(t),a=l[r].value;a!==n&&(c(t),s(a))},d=e=>{let t=null;switch(e.key){case"Enter":u(e);break;case"ArrowRight":{const r=o.indexOf(e.currentTarget)+1;t=o[r]??o[0];break}case"ArrowLeft":{const r=o.indexOf(e.currentTarget)-1;t=o[r]??o[o.length-1];break}}t?.focus()};return(0,v.jsx)("ul",{role:"tablist","aria-orientation":"horizontal",className:(0,a.A)("tabs",{"tabs--block":r},t),children:l.map((e=>{let{value:t,label:r,attributes:i}=e;return(0,v.jsx)("li",{role:"tab",tabIndex:n===t?0:-1,"aria-selected":n===t,ref:e=>o.push(e),onKeyDown:d,onClick:u,...i,className:(0,a.A)("tabs__item",b.tabItem,i?.className,{"tabs__item--active":n===t}),children:r??t},t)}))})}function j(e){let{lazy:t,children:r,selectedValue:a}=e;const i=(Array.isArray(r)?r:[r]).filter(Boolean);if(t){const e=i.find((e=>e.props.value===a));return e?(0,n.cloneElement)(e,{className:"margin-top--md"}):null}return(0,v.jsx)("div",{className:"margin-top--md",children:i.map(((e,t)=>(0,n.cloneElement)(e,{key:t,hidden:e.props.value!==a})))})}function y(e){const t=g(e);return(0,v.jsxs)("div",{className:(0,a.A)("tabs-container",b.tabList),children:[(0,v.jsx)(x,{...t,...e}),(0,v.jsx)(j,{...t,...e})]})}function w(e){const t=(0,f.A)();return(0,v.jsx)(y,{...e,children:d(e.children)},String(t))}},8710:(e,t,r)=>{r.d(t,{A:()=>n});const n=r.p+"assets/images/example-279830c17e2bfcec051c7a3c6280e740.png"},8453:(e,t,r)=>{r.d(t,{R:()=>s,x:()=>l});var n=r(6540);const a={},i=n.createContext(a);function s(e){const t=n.useContext(i);return n.useMemo((function(){return"function"==typeof e?e(t):{...t,...e}}),[t,e])}function l(e){let t;return t=e.disableParentContext?"function"==typeof e.components?e.components(a):e.components||a:s(e.components),n.createElement(i.Provider,{value:t},e.children)}}}]); \ No newline at end of file diff --git a/assets/js/b3d9ed0f.f977c0f4.js b/assets/js/b3d9ed0f.db6a0b09.js similarity index 51% rename from assets/js/b3d9ed0f.f977c0f4.js rename to assets/js/b3d9ed0f.db6a0b09.js index 5310c11..2c074fd 100644 --- a/assets/js/b3d9ed0f.f977c0f4.js +++ b/assets/js/b3d9ed0f.db6a0b09.js @@ -1 +1 @@ -"use strict";(self.webpackChunkreftree=self.webpackChunkreftree||[]).push([[814],{6444:(e,n,a)=>{a.r(n),a.d(n,{assets:()=>d,contentTitle:()=>c,default:()=>m,frontMatter:()=>l,metadata:()=>o,toc:()=>h});var s=a(4848),r=a(8453),t=a(3554),i=a.n(t);const l={sidebar_position:1,description:"This talk takes advantage of reftree to observe several immutable data structures in action and uncover their inner beauty. Having surfaced immutability\u2019s crucial tricks, we move our focus to lenses and zippers \u2014 handy tools that combine the convenience of the \u201cmutable world\u201d with the expressiveness of functional programming."},c="Unzipping Immutability",o={id:"talks/Immutability",title:"Unzipping Immutability",description:"This talk takes advantage of reftree to observe several immutable data structures in action and uncover their inner beauty. Having surfaced immutability\u2019s crucial tricks, we move our focus to lenses and zippers \u2014 handy tools that combine the convenience of the \u201cmutable world\u201d with the expressiveness of functional programming.",source:"@site/../site-gen/target/mdoc/talks/Immutability.md",sourceDirName:"talks",slug:"/talks/Immutability",permalink:"/reftree/docs/talks/Immutability",draft:!1,unlisted:!1,editUrl:"https://github.com/stanch/reftree/tree/main/docs/../site-gen/target/mdoc/talks/Immutability.md",tags:[],version:"current",sidebarPosition:1,frontMatter:{sidebar_position:1,description:"This talk takes advantage of reftree to observe several immutable data structures in action and uncover their inner beauty. Having surfaced immutability\u2019s crucial tricks, we move our focus to lenses and zippers \u2014 handy tools that combine the convenience of the \u201cmutable world\u201d with the expressiveness of functional programming."},sidebar:"mainSidebar",previous:{title:"Talks / Demos",permalink:"/reftree/docs/talks/"},next:{title:"Visualize your data structures!",permalink:"/reftree/docs/talks/Visualize"}},d={},h=[{value:"Immutable data structures",id:"immutable-data-structures",level:2},{value:"Lists",id:"lists",level:3},{value:"Queues",id:"queues",level:3},{value:"Vectors",id:"vectors",level:3},{value:"Finger Trees",id:"finger-trees",level:3},{value:"Lenses",id:"lenses",level:2},{value:"Zippers",id:"zippers",level:2},{value:"Useful resources",id:"useful-resources",level:2},{value:"Books, papers and talks",id:"books-papers-and-talks",level:3},{value:"Scala libraries",id:"scala-libraries",level:3}];function p(e){const n={a:"a",code:"code",em:"em",h1:"h1",h2:"h2",h3:"h3",img:"img",li:"li",p:"p",pre:"pre",ul:"ul",...(0,r.R)(),...e.components},{Details:t}=n;return t||function(e,n){throw new Error("Expected "+(n?"component":"object")+" `"+e+"` to be defined: you likely forgot to import, pass, or provide it.")}("Details",!0),(0,s.jsxs)(s.Fragment,{children:[(0,s.jsx)(n.h1,{id:"unzipping-immutability",children:"Unzipping Immutability"}),"\n",(0,s.jsx)(n.p,{children:"This page contains the materials for my talk \u201cUnzipping Immutability\u201d."}),"\n","\n",(0,s.jsx)(i(),{controls:!0,style:{marginBottom:"1em"},url:"https://www.youtube.com/watch?v=dOj-wk5MQ3k"}),"\n",(0,s.jsxs)(t,{children:[(0,s.jsx)("summary",{children:"Older videos"}),(0,s.jsxs)(n.ul,{children:["\n",(0,s.jsxs)(n.li,{children:["LX Scala, April 2016: ",(0,s.jsx)(n.a,{href:"https://vimeo.com/162214356",children:"https://vimeo.com/162214356"})]}),"\n",(0,s.jsxs)(n.li,{children:["Pixels Camp, October 2016: ",(0,s.jsx)(n.a,{href:"https://www.youtube.com/watch?v=yeMvhuD689A",children:"https://www.youtube.com/watch?v=yeMvhuD689A"})]}),"\n"]})]}),"\n",(0,s.jsx)(n.p,{children:"You can use this page in two ways:"}),"\n",(0,s.jsxs)(n.ul,{children:["\n",(0,s.jsx)(n.li,{children:"as a reference/refresher on the concepts covered in the talk;"}),"\n",(0,s.jsx)(n.li,{children:"as an interactive playground where you can try the same commands I presented."}),"\n"]}),"\n",(0,s.jsx)(n.p,{children:"Throughout this page we will assume the following\ndeclarations (each section might add its own):"}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-scala",children:"import reftree.core._\nimport reftree.diagram._\nimport reftree.render._\nimport reftree.demo.Data._\nimport scala.collection.immutable._\nimport java.nio.file.Paths\nimport Diagram.{sourceCodeCaption => diagram}\n"})}),"\n",(0,s.jsx)(n.p,{children:"To start an interactive session, just run"}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{children:"$ sbt demo\n@ render(List(1, 2, 3))\n"})}),"\n",(0,s.jsxs)(n.p,{children:["and open ",(0,s.jsx)(n.code,{children:"diagram.png"})," in your favorite image viewer (hopefully one that\nreloads images automatically on file change). You will also need to have\n",(0,s.jsx)(n.a,{href:"http://www.graphviz.org/",children:"GraphViz"})," installed. ",(0,s.jsx)(n.em,{children:"The interactive session\nalready has all the necessary imports in scope."})]}),"\n",(0,s.jsx)(n.h2,{id:"immutable-data-structures",children:"Immutable data structures"}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-scala",children:'// extra declarations for this section\nval renderer = Renderer(\n renderingOptions = RenderingOptions(density = 100),\n directory = Paths.get(ImagePath, "immutability")\n)\nimport renderer._\n'})}),"\n",(0,s.jsx)(n.h3,{id:"lists",children:"Lists"}),"\n",(0,s.jsx)(n.p,{children:"We\u2019ll start with one of the simplest structures: a list.\nIt consists of a number of cells pointing to each other:"}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-scala",children:"val list = List(1, 2, 3)\n// list: List[Int] = List(1, 2, 3)\n"})}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-scala",children:'diagram(list).render("list")\n'})}),"\n",(0,s.jsx)(n.p,{children:(0,s.jsx)(n.img,{alt:"list",src:a(8603).A+"",width:"322",height:"590"})}),"\n",(0,s.jsx)(n.p,{children:"Elements can be added to or removed from the front of the list with no effort,\nbecause we can share the same cells across several lists.\nThis would not be possible with a mutable list,\nsince modifying the shared part would modify every data structure making use of it."}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-scala",children:"val add = 0 :: list\n// add: List[Int] = List(0, 1, 2, 3)\nval remove = list.tail\n// remove: List[Int] = List(2, 3)\n"})}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-scala",children:'(diagram(list) + diagram(add) + diagram(remove)).render("lists")\n'})}),"\n",(0,s.jsx)(n.p,{children:(0,s.jsx)(n.img,{alt:"lists",src:a(2600).A+"",width:"455",height:"722"})}),"\n",(0,s.jsxs)(n.p,{children:["However we can\u2019t easily add elements at the end of the list, since the last cell\nis pointing to the empty list (",(0,s.jsx)(n.code,{children:"Nil"}),") and is immutable, i.e. cannot be changed.\nThus we are forced to create a new list every time:"]}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-scala",children:'(Animation\n .startWith(List(1))\n .iterate(_ :+ 2, _ :+ 3, _ :+ 4)\n .build()\n .render("list-append", tweakAnimation = _.withOnionSkinLayers(3)))\n'})}),"\n",(0,s.jsx)(n.p,{children:(0,s.jsx)(n.img,{alt:"list-append",src:a(2773).A+"",width:"675",height:"722"})}),"\n",(0,s.jsx)(n.p,{children:"This certainly does not look efficient compared to adding elements at the front:"}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-scala",children:'(Animation\n .startWith(List(1))\n .iterate(2 :: _, 3 :: _, 4 :: _)\n .build()\n .render("list-prepend"))\n'})}),"\n",(0,s.jsx)(n.p,{children:(0,s.jsx)(n.img,{alt:"list-prepend",src:a(2515).A+"",width:"457",height:"722"})}),"\n",(0,s.jsx)(n.h3,{id:"queues",children:"Queues"}),"\n",(0,s.jsx)(n.p,{children:"If we want to add elements on both sides efficiently, we need a different data structure: a queue.\nThe queue below, also known as a \u201cBanker\u2019s Queue\u201d, has two lists: one for prepending and one for appending."}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-scala",children:"val queue1 = Queue(1, 2, 3)\n// queue1: Queue[Int] = Queue(1, 2, 3)\nval queue2 = (queue1 :+ 4).tail\n// queue2: Queue[Int] = Queue(2, 3, 4)\n"})}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-scala",children:'(diagram(queue1) + diagram(queue2)).render("queues", _.withVerticalSpacing(1.2))\n'})}),"\n",(0,s.jsx)(n.p,{children:(0,s.jsx)(n.img,{alt:"queues",src:a(5623).A+"",width:"607",height:"992"})}),"\n",(0,s.jsx)(n.p,{children:"This way we can add and remove elements very easily at both ends.\nExcept when we try to remove an element and the respective list is empty!\nIn this case the queue will rotate the other list to make use of its elements.\nAlthough this operation is expensive, the usage pattern intended for a queue\nmakes it rare enough to yield great average (\u201cammortized\u201d) performance:"}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-scala",children:'(Animation\n .startWith(Queue(1, 2, 3))\n .repeat(3)(_.iterate(2)(q => q :+ (q.max + 1)).iterate(2)(_.tail))\n .build(Diagram.toStringCaption(_).withAnchor("queue"))\n .render("queue"))\n'})}),"\n",(0,s.jsx)(n.p,{children:(0,s.jsx)(n.img,{alt:"queue",src:a(9696).A+"",width:"540",height:"922"})}),"\n",(0,s.jsx)(n.h3,{id:"vectors",children:"Vectors"}),"\n",(0,s.jsxs)(n.p,{children:["One downside common to both lists and queues we saw before is that to get an element by index,\nwe need to potentially traverse the whole structure. A ",(0,s.jsx)(n.code,{children:"Vector"})," is a powerful data structure\naddressing this shortcoming and available in Scala (among other languages, like Clojure)."]}),"\n",(0,s.jsx)(n.p,{children:"Internally vectors utilize up to 6 layers of arrays, where 32 elements sit on the first layer,\n1024 \u2014 on the second, 32^3 \u2014 on the third, etc.\nTherefore getting any element by its index requires at most 6 pointer dereferences,\nwhich can be deemed constant time (yes, the trick is that the number of elements that can\nbe stored is limited by 2^31)."}),"\n",(0,s.jsx)(n.p,{children:"The internal 32-element arrays form the basic structural sharing blocks.\nFor small vectors they will be recreated on most operations:"}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-scala",children:"val vector1 = (1 to 20).toVector\n// vector1: Vector[Int] = Vector(1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20)\nval vector2 = vector1 :+ 21\n// vector2: Vector[Int] = Vector(1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21)\n"})}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-scala",children:'(diagram(vector1) + diagram(vector2)).render("vectors", _.withVerticalSpacing(2))\n'})}),"\n",(0,s.jsx)(n.p,{children:(0,s.jsx)(n.img,{alt:"vectors",src:a(1999).A+"",width:"2022",height:"601"})}),"\n",(0,s.jsx)(n.p,{children:"However as more layers leap into action, a huge chunk of the data can be shared:"}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-scala",children:"val vector3 = (1 to 100).toVector\n// vector3: Vector[Int] = Vector(1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 91, 92, 93, 94, 95, 96, 97, 98, 99, 100)\nval vector4 = vector3 :+ 21\n// vector4: Vector[Int] = Vector(1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 91, 92, 93, 94, 95, 96, 97, 98, 99, 100, 21)\n"})}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-scala",children:'(diagram(vector3) + diagram(vector4)).render("big-vectors", _.withVerticalSpacing(2))\n'})}),"\n",(0,s.jsx)(n.p,{children:(0,s.jsx)(n.img,{alt:"big-vectors",src:a(5390).A+"",width:"3785",height:"853"})}),"\n",(0,s.jsxs)(n.p,{children:["If you want to know more, this structure is covered in great detail by Jean Niklas L\u2019orange\n",(0,s.jsx)(n.a,{href:"http://hypirion.com/musings/understanding-persistent-vector-pt-1",children:"in his blog"}),".\nI also highly recommend watching ",(0,s.jsx)(n.a,{href:"https://www.youtube.com/watch?v=pNhBQJN44YQ",children:"this talk"})," by Daniel Spiewak."]}),"\n",(0,s.jsx)(n.h3,{id:"finger-trees",children:"Finger Trees"}),"\n",(0,s.jsxs)(n.p,{children:["To conclude this section, I would like to share a slightly less popular, but beautifully designed\ndata structure called \u201cfinger tree\u201d described in ",(0,s.jsx)(n.a,{href:"http://www.cs.ox.ac.uk/ralf.hinze/publications/FingerTrees.pdf",children:"this paper"}),"\nby Hinze and Paterson. Enjoy the read and this animation of a finger tree getting filled with some numbers:"]}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-scala",children:'import de.sciss.fingertree.{FingerTree, Measure}\nimport reftree.contrib.FingerTreeInstances._\n\nimplicit val measure = Measure.Indexed\n\nAnimation\n .startWith(FingerTree(1))\n .iterateWithIndex(21)((t, i) => t :+ (i + 1))\n .build(Diagram(_).withCaption("Finger Tree").withAnchor("tree"))\n .render("finger", _.withDensity(75).withVerticalSpacing(2))\n'})}),"\n",(0,s.jsx)(n.p,{children:(0,s.jsx)(n.img,{alt:"finger",src:a(7580).A+"",width:"1730",height:"1160"})}),"\n",(0,s.jsx)(n.h2,{id:"lenses",children:"Lenses"}),"\n",(0,s.jsxs)(n.p,{children:["So far we were looking into \u201cstandard\u201d data structures,\nbut in our code we often have to deal with custom data structures comprising our domain model.\nUpdating this sort of data can be tricky if it\u2019s immutable.\nFor case classes Scala gives us the ",(0,s.jsx)(n.code,{children:"copy"})," method:"]}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-scala",children:"case class Employee(\n name: String,\n salary: Long\n)\n"})}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-scala",children:'employee\n// res7: Employee = Employee(name = "Michael", salary = 4000L)\nval raisedEmployee = employee.copy(salary = employee.salary + 10)\n// raisedEmployee: Employee = Employee(name = "Michael", salary = 4010L)\n'})}),"\n",(0,s.jsxs)(n.p,{children:["However once composition comes into play, the resulting nested immutable data structures\nwould require a lot of ",(0,s.jsx)(n.code,{children:"copy"})," calls:"]}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-scala",children:"case class Employee(\n name: String,\n salary: Long\n)\n\ncase class Startup(\n name: String,\n founder: Employee,\n team: List[Employee]\n)\n"})}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-scala",children:'startup\n// res8: Startup = Startup(\n// name = "Acme",\n// founder = Employee(name = "Michael", salary = 4000L),\n// team = List(\n// Employee(name = "Adam", salary = 2100L),\n// Employee(name = "Bella", salary = 2100L),\n// Employee(name = "Chad", salary = 1980L),\n// Employee(name = "Delia", salary = 1850L)\n// )\n// )\nval raisedFounder = startup.copy(\n founder = startup.founder.copy(\n salary = startup.founder.salary + 10\n )\n)\n// raisedFounder: Startup = Startup(\n// name = "Acme",\n// founder = Employee(name = "Michael", salary = 4010L),\n// team = List(\n// Employee(name = "Adam", salary = 2100L),\n// Employee(name = "Bella", salary = 2100L),\n// Employee(name = "Chad", salary = 1980L),\n// Employee(name = "Delia", salary = 1850L)\n// )\n// )\n'})}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-scala",children:"// extra declarations for this section\nimport reftree.contrib.SimplifiedInstances.{list => listInstance}\nimport reftree.contrib.OpticInstances._\n"})}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-scala",children:'(diagram(startup) + diagram(raisedFounder)).render("startup")\n'})}),"\n",(0,s.jsx)(n.p,{children:(0,s.jsx)(n.img,{alt:"startup",src:a(7056).A+"",width:"1977",height:"701"})}),"\n",(0,s.jsx)(n.p,{children:"Ouch!"}),"\n",(0,s.jsxs)(n.p,{children:["A common solution to this problem is a \u201clens\u201d.\nIn the simplest case a lens is a pair of functions to get and set a value of type ",(0,s.jsx)(n.code,{children:"B"})," inside a value of type ",(0,s.jsx)(n.code,{children:"A"}),".\nIt\u2019s called a lens because it focuses on some part of the data and allows to update it.\nFor example, here is a lens that focuses on an employee\u2019s salary\n(using the excellent ",(0,s.jsx)(n.a,{href:"https://github.com/julien-truffaut/Monocle",children:"Monocle library"}),"):"]}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-scala",children:'import monocle.macros.GenLens\n\nval salaryLens = GenLens[Employee](_.salary)\n// salaryLens: monocle.package.Lens[Employee, Long] = repl.MdocSession$MdocApp$$anon$1@3c477363\n\nsalaryLens.get(startup.founder)\n// res10: Long = 4000L\nsalaryLens.modify(s => s + 10)(startup.founder)\n// res11: Employee = Employee(name = "Michael", salary = 4010L)\n'})}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-scala",children:'diagram(OpticFocus(salaryLens, startup.founder)).render("salaryLens")\n'})}),"\n",(0,s.jsx)(n.p,{children:(0,s.jsx)(n.img,{alt:"salaryLens",src:a(7817).A+"",width:"592",height:"363"})}),"\n",(0,s.jsx)(n.p,{children:"We can also define a lens that focuses on the startup\u2019s founder:"}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-scala",children:'val founderLens = GenLens[Startup](_.founder)\n// founderLens: monocle.package.Lens[Startup, Employee] = repl.MdocSession$MdocApp$$anon$2@389ba703\n\nfounderLens.get(startup)\n// res13: Employee = Employee(name = "Michael", salary = 4000L)\n'})}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-scala",children:'diagram(OpticFocus(founderLens, startup)).render("founderLens")\n'})}),"\n",(0,s.jsx)(n.p,{children:(0,s.jsx)(n.img,{alt:"founderLens",src:a(1738).A+"",width:"1832",height:"701"})}),"\n",(0,s.jsx)(n.p,{children:"It\u2019s not apparent yet how this would help, but the trick is that lenses can be composed:"}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-scala",children:'val founderSalaryLens = founderLens composeLens salaryLens\n// founderSalaryLens: monocle.PLens[Startup, Startup, Long, Long] = monocle.PLens$$anon$1@21c54dde\n\nfounderSalaryLens.get(startup)\n// res15: Long = 4000L\nfounderSalaryLens.modify(s => s + 10)(startup)\n// res16: Startup = Startup(\n// name = "Acme",\n// founder = Employee(name = "Michael", salary = 4010L),\n// team = List(\n// Employee(name = "Adam", salary = 2100L),\n// Employee(name = "Bella", salary = 2100L),\n// Employee(name = "Chad", salary = 1980L),\n// Employee(name = "Delia", salary = 1850L)\n// )\n// )\n'})}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-scala",children:'diagram(OpticFocus(founderSalaryLens, startup)).render("founderSalaryLens")\n'})}),"\n",(0,s.jsx)(n.p,{children:(0,s.jsx)(n.img,{alt:"founderSalaryLens",src:a(5288).A+"",width:"1866",height:"701"})}),"\n",(0,s.jsx)(n.p,{children:"One interesting thing is that lenses can focus on anything, not just direct attributes of the data.\nHere is a traversal \u2014 a more generic kind of lens \u2014 that focuses on all vowels in a string:"}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-scala",children:'diagram(OpticFocus(vowelTraversal, "example")).render("vowelTraversal")\n'})}),"\n",(0,s.jsx)(n.p,{children:(0,s.jsx)(n.img,{alt:"vowelTraversal",src:a(2964).A+"",width:"494",height:"193"})}),"\n",(0,s.jsx)(n.p,{children:"We can use it to give our founder a funny name:"}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-scala",children:'val employeeNameLens = GenLens[Employee](_.name)\n// employeeNameLens: monocle.package.Lens[Employee, String] = repl.MdocSession$MdocApp$$anon$3@8eeb1fa\nval founderVowelTraversal = founderLens composeLens employeeNameLens composeTraversal vowelTraversal\n// founderVowelTraversal: monocle.PTraversal[Startup, Startup, Char, Char] = monocle.PTraversal$$anon$2@37aed08b\n\nfounderVowelTraversal.modify(v => v.toUpper)(startup)\n// res19: Startup = Startup(\n// name = "Acme",\n// founder = Employee(name = "MIchAEl", salary = 4000L),\n// team = List(\n// Employee(name = "Adam", salary = 2100L),\n// Employee(name = "Bella", salary = 2100L),\n// Employee(name = "Chad", salary = 1980L),\n// Employee(name = "Delia", salary = 1850L)\n// )\n// )\n'})}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-scala",children:'diagram(OpticFocus(founderVowelTraversal, startup)).render("founderVowelTraversal")\n'})}),"\n",(0,s.jsx)(n.p,{children:(0,s.jsx)(n.img,{alt:"founderVowelTraversal",src:a(7289).A+"",width:"1889",height:"701"})}),"\n",(0,s.jsxs)(n.p,{children:["So far we have replaced the ",(0,s.jsx)(n.code,{children:"copy"})," boilerplate with a number of lens declarations.\nHowever most of the time our goal is just to update data."]}),"\n",(0,s.jsxs)(n.p,{children:["In Scala there is a great library called ",(0,s.jsx)(n.a,{href:"https://github.com/adamw/quicklens",children:"quicklens"}),"\nthat allows to do exactly that, creating all the necessary lenses under the hood:"]}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-scala",children:'import com.softwaremill.quicklens._\n\nval raisedCeo = startup.modify(_.founder.salary).using(s => s + 10)\n// raisedCeo: Startup = Startup(\n// name = "Acme",\n// founder = Employee(name = "Michael", salary = 4010L),\n// team = List(\n// Employee(name = "Adam", salary = 2100L),\n// Employee(name = "Bella", salary = 2100L),\n// Employee(name = "Chad", salary = 1980L),\n// Employee(name = "Delia", salary = 1850L)\n// )\n// )\n'})}),"\n",(0,s.jsx)(n.p,{children:"You might think this is approaching the syntax for updating mutable data,\nbut actually we have already surpassed it, since lenses are much more flexible:"}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-scala",children:'val raisedEveryone = startup.modifyAll(_.founder.salary, _.team.each.salary).using(s => s + 10)\n// raisedEveryone: Startup = Startup(\n// name = "Acme",\n// founder = Employee(name = "Michael", salary = 4010L),\n// team = List(\n// Employee(name = "Adam", salary = 2110L),\n// Employee(name = "Bella", salary = 2110L),\n// Employee(name = "Chad", salary = 1990L),\n// Employee(name = "Delia", salary = 1860L)\n// )\n// )\n'})}),"\n",(0,s.jsx)(n.h2,{id:"zippers",children:"Zippers"}),"\n",(0,s.jsx)(n.p,{children:"In our domain models we are often faced with recursive data structures.\nConsider this example:"}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-scala",children:"case class Employee(\n name: String,\n salary: Long\n)\n\ncase class Hierarchy(\n employee: Employee,\n team: List[Hierarchy]\n)\n\ncase class Company(\n name: String,\n hierarchy: Hierarchy\n)\n"})}),"\n",(0,s.jsxs)(n.p,{children:["The ",(0,s.jsx)(n.code,{children:"Hierarchy"})," class refers to itself.\nLet\u2019s grab a company object and display its hierarchy as a tree:"]}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-scala",children:"// extra declarations for this section\nimport zipper._\nimport reftree.contrib.SimplifiedInstances.option\nimport reftree.contrib.ZipperInstances._\n"})}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-scala",children:'diagram(company.hierarchy).render("company")\n'})}),"\n",(0,s.jsx)(n.p,{children:(0,s.jsx)(n.img,{alt:"company",src:a(3422).A+"",width:"2283",height:"1210"})}),"\n",(0,s.jsxs)(n.p,{children:["What if we want to navigate through this tree and modify it along the way?\nWe can use ",(0,s.jsx)(n.a,{href:"#lenses",children:"lenses"}),", but the recursive nature of the tree allows for a better solution."]}),"\n",(0,s.jsxs)(n.p,{children:["This solution is called a \u201cZipper\u201d, and was introduced by G\xe9rard Huet in 1997.\nIt consists of a \u201ccursor\u201d pointing to a location anywhere in the tree \u2014 \u201ccurrent focus\u201d.\nThe cursor can be moved freely with operations like ",(0,s.jsx)(n.code,{children:"moveDownLeft"}),", ",(0,s.jsx)(n.code,{children:"moveRight"}),", ",(0,s.jsx)(n.code,{children:"moveUp"}),", etc.\nCurrent focus can be updated, deleted, or new nodes can be inserted to its left or right.\nZippers are immutable, and every operation returns a new Zipper.\nAll the changes made to the tree can be committed, yielding a new modified version of the original tree."]}),"\n",(0,s.jsx)(n.p,{children:"Here is how we would insert a new employee into the hierarchy:"}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-scala",children:"val updatedHierarchy = Zipper(company.hierarchy).moveDownRight.moveDownRight.insertRight(newHire).commit\n"})}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-scala",children:'(diagram(company.hierarchy) + diagram(updatedHierarchy)).render("updatedHierarchy")\n'})}),"\n",(0,s.jsx)(n.p,{children:(0,s.jsx)(n.img,{alt:"updatedHierarchy",src:a(365).A+"",width:"2505",height:"1210"})}),"\n",(0,s.jsxs)(n.p,{children:["My ",(0,s.jsx)(n.a,{href:"https://github.com/stanch/zipper#zipper--an-implementation-of-huets-zipper",children:"zipper library"}),"\nprovides a few useful movements and operations."]}),"\n",(0,s.jsx)(n.p,{children:"Let\u2019s consider a simpler recursive data structure:"}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-scala",children:"case class Tree(x: Int, c: List[Tree] = List.empty)\n"})}),"\n",(0,s.jsx)(n.p,{children:"and a simple tree:"}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-scala",children:"simpleTree\n// res23: Tree = Tree(\n// x = 1,\n// c = List(\n// Tree(x = 2, c = List()),\n// Tree(x = 3, c = List()),\n// Tree(x = 4, c = List()),\n// Tree(x = 5, c = List(Tree(x = 6, c = List()), Tree(x = 7, c = List())))\n// )\n// )\n"})}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-scala",children:'diagram(simpleTree).render("simpleTree")\n'})}),"\n",(0,s.jsx)(n.p,{children:(0,s.jsx)(n.img,{alt:"simpleTree",src:a(977).A+"",width:"925",height:"721"})}),"\n",(0,s.jsx)(n.p,{children:"When we wrap a Zipper around this tree, it does not look very interesting yet:"}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-scala",children:"val zipper1 = Zipper(simpleTree)\n"})}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-scala",children:'(diagram(simpleTree) + diagram(zipper1)).render("zipper1")\n'})}),"\n",(0,s.jsx)(n.p,{children:(0,s.jsx)(n.img,{alt:"zipper1",src:a(3596).A+"",width:"998",height:"890"})}),"\n",(0,s.jsx)(n.p,{children:"We can see that it just points to the original tree and has some other empty fields.\nMore specifically, a Zipper consists of four pointers:"}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-scala",children:"case class Zipper[A](\n left: List[A], // left siblings of the focus\n focus: A, // the current focus\n right: List[A], // right siblings of the focus\n top: Option[Zipper[A]] // the parent zipper\n)\n"})}),"\n",(0,s.jsx)(n.p,{children:"In this case the focus is the root of the tree, which has no siblings,\nand the parent zipper does not exist, since we are at the top level."}),"\n",(0,s.jsx)(n.p,{children:"One thing we can do right away is modify the focus:"}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-scala",children:"val zipper2 = zipper1.update(focus => focus.copy(x = focus.x + 99))\n"})}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-scala",children:'(diagram(simpleTree) + diagram(zipper1) + diagram(zipper2)).render("zipper2")\n'})}),"\n",(0,s.jsx)(n.p,{children:(0,s.jsx)(n.img,{alt:"zipper2",src:a(7879).A+"",width:"1299",height:"890"})}),"\n",(0,s.jsx)(n.p,{children:"We just created a new tree! To obtain it, we have to commit the changes:"}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-scala",children:"val tree2 = zipper2.commit\n"})}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-scala",children:'(diagram(simpleTree) + diagram(tree2)).render("tree2")\n'})}),"\n",(0,s.jsx)(n.p,{children:(0,s.jsx)(n.img,{alt:"tree2",src:a(4025).A+"",width:"947",height:"721"})}),"\n",(0,s.jsx)(n.p,{children:"If you were following closely,\nyou would notice that nothing spectacular happened yet:\nwe could\u2019ve easily obtained the same result by modifying the tree directly:"}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-scala",children:"val tree2b = simpleTree.copy(x = simpleTree.x + 99)\n\nassert(tree2b == tree2)\n"})}),"\n",(0,s.jsx)(n.p,{children:"The power of Zipper becomes apparent when we go one or more levels deep.\nTo move down the tree, we \u201cunzip\u201d it, separating the child nodes into\nthe focused node and its left and right siblings:"}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-scala",children:"val zipper3 = zipper1.moveDownLeft\n"})}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-scala",children:'(diagram(zipper1) + diagram(zipper3)).render("zipper1+3")\n'})}),"\n",(0,s.jsx)(n.p,{children:(0,s.jsx)(n.img,{alt:"zipper1+3",src:a(3146).A+"",width:"973",height:"1060"})}),"\n",(0,s.jsx)(n.p,{children:"The new Zipper links to the old one,\nwhich will allow us to return to the root of the tree when we are done applying changes.\nThis link however prevents us from seeing the picture clearly.\nLet\u2019s look at the second zipper alone:"}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-scala",children:'diagram(zipper3).render("zipper3")\n'})}),"\n",(0,s.jsx)(n.p,{children:(0,s.jsx)(n.img,{alt:"zipper3",src:a(4862).A+"",width:"927",height:"758"})}),"\n",(0,s.jsxs)(n.p,{children:["Great! We have ",(0,s.jsx)(n.code,{children:"2"})," in focus and ",(0,s.jsx)(n.code,{children:"3, 4, 5"})," as right siblings. What happens if we move right a bit?"]}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-scala",children:"val zipper4 = zipper3.moveRightBy(2)\n"})}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-scala",children:'diagram(zipper4).render("zipper4")\n'})}),"\n",(0,s.jsx)(n.p,{children:(0,s.jsx)(n.img,{alt:"zipper4",src:a(3358).A+"",width:"818",height:"758"})}),"\n",(0,s.jsx)(n.p,{children:"This is interesting! Notice that the left siblings are \u201cinverted\u201d.\nThis allows to move left and right in constant time, because the sibling\nadjacent to the focus is always at the head of the list."}),"\n",(0,s.jsx)(n.p,{children:"This also allows us to insert new siblings easily:"}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-scala",children:"val zipper5 = zipper4.insertLeft(Tree(34))\n"})}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-scala",children:'diagram(zipper5).render("zipper5")\n'})}),"\n",(0,s.jsx)(n.p,{children:(0,s.jsx)(n.img,{alt:"zipper5",src:a(2712).A+"",width:"935",height:"758"})}),"\n",(0,s.jsx)(n.p,{children:"And, as you might know, we can delete nodes and update the focus:"}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-scala",children:"val zipper6 = zipper5.deleteAndMoveRight.set(Tree(45))\n"})}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-scala",children:'diagram(zipper6).render("zipper6")\n'})}),"\n",(0,s.jsx)(n.p,{children:(0,s.jsx)(n.img,{alt:"zipper6",src:a(7203).A+"",width:"531",height:"494"})}),"\n",(0,s.jsx)(n.p,{children:"Finally, when we move up, the siblings at the current level are \u201czipped\u201d\ntogether and their parent node is updated:"}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-scala",children:"val zipper7 = zipper6.moveUp\n"})}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-scala",children:'diagram(zipper7).render("zipper7")\n'})}),"\n",(0,s.jsx)(n.p,{children:(0,s.jsx)(n.img,{alt:"zipper7",src:a(1994).A+"",width:"777",height:"626"})}),"\n",(0,s.jsxs)(n.p,{children:["You can probably guess by now that ",(0,s.jsx)(n.code,{children:".commit"})," is a shorthand for going\nall the way up (applying all the changes) and returning the focus:"]}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-scala",children:"val tree3a = zipper6.moveUp.focus\nval tree3b = zipper6.commit\n\nassert(tree3a == tree3b)\n"})}),"\n",(0,s.jsx)(n.p,{children:"Here is an animation of the navigation process:"}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-scala",children:'val movement = Animation\n .startWith(Zipper(Data.simpleTree))\n .iterate(\n _.moveDownLeft,\n _.moveRight, _.moveRight, _.moveRight,\n _.moveDownLeft,\n _.moveRight, _.moveLeft,\n _.top.get,\n _.moveLeft, _.moveLeft, _.moveLeft,\n _.top.get\n )\n\nval trees = movement\n .build(z => Diagram(ZipperFocus(z, Data.simpleTree)).withCaption("Tree").withAnchor("tree"))\n .toNamespace("tree")\n\nval zippers = movement\n .build(Diagram(_).withCaption("Zipper").withAnchor("zipper").withColor(2))\n .toNamespace("zipper")\n\n(trees + zippers).render("tree+zipper")\n'})}),"\n",(0,s.jsx)(n.p,{children:(0,s.jsx)(n.img,{alt:"tree+zipper",src:a(1864).A+"",width:"1241",height:"690"})}),"\n",(0,s.jsx)(n.h2,{id:"useful-resources",children:"Useful resources"}),"\n",(0,s.jsx)(n.h3,{id:"books-papers-and-talks",children:"Books, papers and talks"}),"\n",(0,s.jsxs)(n.ul,{children:["\n",(0,s.jsxs)(n.li,{children:[(0,s.jsx)(n.a,{href:"http://www.amazon.com/Purely-Functional-Structures-Chris-Okasaki/dp/0521663504",children:"Purely functional data structures"})," by Chris Okasaki,\nand/or ",(0,s.jsx)(n.a,{href:"https://www.cs.cmu.edu/~rwh/theses/okasaki.pdf",children:"his PhD thesis"})," \u2014 ",(0,s.jsx)(n.em,{children:"the"})," introduction to immutable data structures"]}),"\n",(0,s.jsxs)(n.li,{children:[(0,s.jsx)(n.a,{href:"http://cstheory.stackexchange.com/a/1550",children:"What\u2019s new in purely functional data structures since Okasaki"})," \u2014 an excellent StackExchange answer\nwith pointers for further reading"]}),"\n",(0,s.jsxs)(n.li,{children:[(0,s.jsx)(n.a,{href:"https://www.youtube.com/watch?v=pNhBQJN44YQ",children:"Extreme cleverness"})," by Daniel Spiewak \u2014 a superb talk\ncovering several immutable data structures (implemented ",(0,s.jsx)(n.a,{href:"https://github.com/djspiewak/extreme-cleverness",children:"here"}),")"]}),"\n",(0,s.jsxs)(n.li,{children:[(0,s.jsx)(n.a,{href:"http://hypirion.com/musings/understanding-persistent-vector-pt-1",children:"Understanding Clojure\u2019s Persistent Vectors, part 1"}),"\nand ",(0,s.jsx)(n.a,{href:"http://hypirion.com/musings/understanding-persistent-vector-pt-2",children:"part 2"})," \u2014 a series of blog posts by Jean Niklas L\u2019orange"]}),"\n",(0,s.jsxs)(n.li,{children:[(0,s.jsx)(n.a,{href:"http://www.cs.ox.ac.uk/ralf.hinze/publications/FingerTrees.pdf",children:"Finger Trees"})," and\n",(0,s.jsx)(n.a,{href:"http://www.cs.ox.ac.uk/ralf.hinze/publications/Brother12.pdf",children:"1-2 Brother Trees"})," described by Hinze and Paterson"]}),"\n",(0,s.jsxs)(n.li,{children:[(0,s.jsx)(n.a,{href:"https://www.st.cs.uni-saarland.de/edu/seminare/2005/advanced-fp/docs/huet-zipper.pdf",children:"Huet\u2019s original Zipper paper"})," \u2014 a great short read\nintroducing the Zipper"]}),"\n",(0,s.jsxs)(n.li,{children:[(0,s.jsx)(n.a,{href:"http://dspace.library.uu.nl/bitstream/handle/1874/2532/2001-33.pdf",children:"Weaving a web"})," by Hinze and Jeuring \u2014\nanother interesting Zipper-like approach"]}),"\n"]}),"\n",(0,s.jsx)(n.h3,{id:"scala-libraries",children:"Scala libraries"}),"\n",(0,s.jsxs)(n.ul,{children:["\n",(0,s.jsxs)(n.li,{children:[(0,s.jsx)(n.a,{href:"https://github.com/stanch/zipper",children:"zipper"})," \u2014 my Zipper implementation"]}),"\n",(0,s.jsxs)(n.li,{children:[(0,s.jsx)(n.a,{href:"https://github.com/julien-truffaut/Monocle",children:"Monocle"})," \u2014 an \u201coptics\u201d library"]}),"\n",(0,s.jsxs)(n.li,{children:[(0,s.jsx)(n.a,{href:"https://github.com/adamw/quicklens",children:"Quicklens"})," \u2014 a simpler way to update nested case classes"]}),"\n",(0,s.jsxs)(n.li,{children:[(0,s.jsx)(n.a,{href:"https://github.com/Sciss/FingerTree",children:"FingerTree"})," \u2014 an implementation of the Finger Tree data structure"]}),"\n"]})]})}function m(e={}){const{wrapper:n}={...(0,r.R)(),...e.components};return n?(0,s.jsx)(n,{...e,children:(0,s.jsx)(p,{...e})}):p(e)}},7580:(e,n,a)=>{a.d(n,{A:()=>s});const s=a.p+"assets/images/finger-b0999bed13c76359869b636227548cc6.gif"},5390:(e,n,a)=>{a.d(n,{A:()=>s});const s=a.p+"assets/images/big-vectors-4b0f5120bf4a32b8802dc41e520eb37a.png"},3422:(e,n,a)=>{a.d(n,{A:()=>s});const s=a.p+"assets/images/company-3188e3cf359d9f1c7224ffceed126083.png"},1738:(e,n,a)=>{a.d(n,{A:()=>s});const s=a.p+"assets/images/founderLens-005975324dcee19c77494d453f8647c2.png"},5288:(e,n,a)=>{a.d(n,{A:()=>s});const s=a.p+"assets/images/founderSalaryLens-89e3e6d28efff27c4c8f563cfa49d3ee.png"},7289:(e,n,a)=>{a.d(n,{A:()=>s});const s=a.p+"assets/images/founderVowelTraversal-d35014cf80294acb4990d554bcedb0d3.png"},2773:(e,n,a)=>{a.d(n,{A:()=>s});const s=a.p+"assets/images/list-append-063e66f9ba76613ce2a8ba351ea27ee8.gif"},2515:(e,n,a)=>{a.d(n,{A:()=>s});const s=a.p+"assets/images/list-prepend-d90a987fd8bfc72246aa9c61dbfcf1c8.gif"},8603:(e,n,a)=>{a.d(n,{A:()=>s});const s=a.p+"assets/images/list-a190da16976869e8e07bee62d501a9f7.png"},2600:(e,n,a)=>{a.d(n,{A:()=>s});const s=a.p+"assets/images/lists-58f15f89774ee685a16243b40e9ba7c2.png"},5623:(e,n,a)=>{a.d(n,{A:()=>s});const s=a.p+"assets/images/queues-5efd3f3abf9c377155a0b4b6de6a77f9.png"},7817:(e,n,a)=>{a.d(n,{A:()=>s});const s=a.p+"assets/images/salaryLens-0d89d07dc97f512aa7fe6b50f061cfcd.png"},977:(e,n,a)=>{a.d(n,{A:()=>s});const s=a.p+"assets/images/simpleTree-bbaebacbd585b97eea7d64d94848caf8.png"},7056:(e,n,a)=>{a.d(n,{A:()=>s});const s=a.p+"assets/images/startup-2a750a1231daf1c2d9c548867f54ffc1.png"},4025:(e,n,a)=>{a.d(n,{A:()=>s});const s=a.p+"assets/images/tree2-9e8f1e7e745ac62ee27f909cf0770ae0.png"},365:(e,n,a)=>{a.d(n,{A:()=>s});const s=a.p+"assets/images/updatedHierarchy-ca0b5d651e31d5815f0d14c96fd928ad.png"},1999:(e,n,a)=>{a.d(n,{A:()=>s});const s=a.p+"assets/images/vectors-d5f0252feb82955ab0584957427fb307.png"},2964:(e,n,a)=>{a.d(n,{A:()=>s});const s=a.p+"assets/images/vowelTraversal-6534f12953b90dc5b6d40f6ee44e6c27.png"},3146:(e,n,a)=>{a.d(n,{A:()=>s});const s=a.p+"assets/images/zipper1+3-dafaaae54d3abc9716c59963ceb6f171.png"},3596:(e,n,a)=>{a.d(n,{A:()=>s});const s=a.p+"assets/images/zipper1-4a80c9c87d37df30e02c064945cde2f6.png"},7879:(e,n,a)=>{a.d(n,{A:()=>s});const s=a.p+"assets/images/zipper2-1176392f67e450e82bfa63dabbeacf90.png"},4862:(e,n,a)=>{a.d(n,{A:()=>s});const s=a.p+"assets/images/zipper3-9ab483dc84adf5005cafecc77e51e394.png"},3358:(e,n,a)=>{a.d(n,{A:()=>s});const s=a.p+"assets/images/zipper4-283827a5d36341e55ced1d024b328633.png"},2712:(e,n,a)=>{a.d(n,{A:()=>s});const s=a.p+"assets/images/zipper5-ce83ff83213a95c042b81012c81a28fc.png"},7203:(e,n,a)=>{a.d(n,{A:()=>s});const s=a.p+"assets/images/zipper6-4431d6974f1d299e656fa1e5c8d54fb0.png"},1994:(e,n,a)=>{a.d(n,{A:()=>s});const s=a.p+"assets/images/zipper7-cd17a63fb6f4732884da00b544ffa549.png"},9696:(e,n,a)=>{a.d(n,{A:()=>s});const s=a.p+"assets/images/queue-668a708310e8e78475fbc9b757c69eb3.gif"},1864:(e,n,a)=>{a.d(n,{A:()=>s});const s=a.p+"assets/images/tree+zipper-b1cf73da287eda26f64b667ce8dcf311.gif"}}]); \ No newline at end of file +"use strict";(self.webpackChunkreftree=self.webpackChunkreftree||[]).push([[814],{6444:(e,n,a)=>{a.r(n),a.d(n,{assets:()=>d,contentTitle:()=>c,default:()=>m,frontMatter:()=>l,metadata:()=>o,toc:()=>h});var s=a(4848),r=a(8453),t=a(3554),i=a.n(t);const l={sidebar_position:1,description:"This talk takes advantage of reftree to observe several immutable data structures in action and uncover their inner beauty. Having surfaced immutability\u2019s crucial tricks, we move our focus to lenses and zippers \u2014 handy tools that combine the convenience of the \u201cmutable world\u201d with the expressiveness of functional programming."},c="Unzipping Immutability",o={id:"talks/Immutability",title:"Unzipping Immutability",description:"This talk takes advantage of reftree to observe several immutable data structures in action and uncover their inner beauty. Having surfaced immutability\u2019s crucial tricks, we move our focus to lenses and zippers \u2014 handy tools that combine the convenience of the \u201cmutable world\u201d with the expressiveness of functional programming.",source:"@site/../site-gen/target/mdoc/talks/Immutability.md",sourceDirName:"talks",slug:"/talks/Immutability",permalink:"/reftree/docs/talks/Immutability",draft:!1,unlisted:!1,editUrl:"https://github.com/stanch/reftree/tree/main/docs/../site-gen/target/mdoc/talks/Immutability.md",tags:[],version:"current",sidebarPosition:1,frontMatter:{sidebar_position:1,description:"This talk takes advantage of reftree to observe several immutable data structures in action and uncover their inner beauty. Having surfaced immutability\u2019s crucial tricks, we move our focus to lenses and zippers \u2014 handy tools that combine the convenience of the \u201cmutable world\u201d with the expressiveness of functional programming."},sidebar:"mainSidebar",previous:{title:"Talks / Demos",permalink:"/reftree/docs/talks/"},next:{title:"Visualize your data structures!",permalink:"/reftree/docs/talks/Visualize"}},d={},h=[{value:"Immutable data structures",id:"immutable-data-structures",level:2},{value:"Lists",id:"lists",level:3},{value:"Queues",id:"queues",level:3},{value:"Vectors",id:"vectors",level:3},{value:"Finger Trees",id:"finger-trees",level:3},{value:"Lenses",id:"lenses",level:2},{value:"Zippers",id:"zippers",level:2},{value:"Useful resources",id:"useful-resources",level:2},{value:"Books, papers and talks",id:"books-papers-and-talks",level:3},{value:"Scala libraries",id:"scala-libraries",level:3}];function p(e){const n={a:"a",code:"code",em:"em",h1:"h1",h2:"h2",h3:"h3",img:"img",li:"li",p:"p",pre:"pre",ul:"ul",...(0,r.R)(),...e.components},{Details:t}=n;return t||function(e,n){throw new Error("Expected "+(n?"component":"object")+" `"+e+"` to be defined: you likely forgot to import, pass, or provide it.")}("Details",!0),(0,s.jsxs)(s.Fragment,{children:[(0,s.jsx)(n.h1,{id:"unzipping-immutability",children:"Unzipping Immutability"}),"\n",(0,s.jsx)(n.p,{children:"This page contains the materials for my talk \u201cUnzipping Immutability\u201d."}),"\n","\n",(0,s.jsx)(i(),{controls:!0,style:{marginBottom:"1em"},url:"https://www.youtube.com/watch?v=dOj-wk5MQ3k"}),"\n",(0,s.jsxs)(t,{children:[(0,s.jsx)("summary",{children:"Older videos"}),(0,s.jsxs)(n.ul,{children:["\n",(0,s.jsxs)(n.li,{children:["LX Scala, April 2016: ",(0,s.jsx)(n.a,{href:"https://vimeo.com/162214356",children:"https://vimeo.com/162214356"})]}),"\n",(0,s.jsxs)(n.li,{children:["Pixels Camp, October 2016: ",(0,s.jsx)(n.a,{href:"https://www.youtube.com/watch?v=yeMvhuD689A",children:"https://www.youtube.com/watch?v=yeMvhuD689A"})]}),"\n"]})]}),"\n",(0,s.jsx)(n.p,{children:"You can use this page in two ways:"}),"\n",(0,s.jsxs)(n.ul,{children:["\n",(0,s.jsx)(n.li,{children:"as a reference/refresher on the concepts covered in the talk;"}),"\n",(0,s.jsx)(n.li,{children:"as an interactive playground where you can try the same commands I presented."}),"\n"]}),"\n",(0,s.jsx)(n.p,{children:"Throughout this page we will assume the following\ndeclarations (each section might add its own):"}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-scala",children:"import reftree.core._\nimport reftree.diagram._\nimport reftree.render._\nimport reftree.demo.Data._\nimport scala.collection.immutable._\nimport java.nio.file.Paths\nimport Diagram.{sourceCodeCaption => diagram}\n"})}),"\n",(0,s.jsx)(n.p,{children:"To start an interactive session, just run"}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{children:"$ sbt demo\n@ render(List(1, 2, 3))\n"})}),"\n",(0,s.jsxs)(n.p,{children:["and open ",(0,s.jsx)(n.code,{children:"diagram.png"})," in your favorite image viewer (hopefully one that\nreloads images automatically on file change). You will also need to have\n",(0,s.jsx)(n.a,{href:"http://www.graphviz.org/",children:"GraphViz"})," installed. ",(0,s.jsx)(n.em,{children:"The interactive session\nalready has all the necessary imports in scope."})]}),"\n",(0,s.jsx)(n.h2,{id:"immutable-data-structures",children:"Immutable data structures"}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-scala",children:'// extra declarations for this section\nval renderer = Renderer(\n renderingOptions = RenderingOptions(density = 100),\n directory = Paths.get(ImagePath, "immutability")\n)\nimport renderer._\n'})}),"\n",(0,s.jsx)(n.h3,{id:"lists",children:"Lists"}),"\n",(0,s.jsx)(n.p,{children:"We\u2019ll start with one of the simplest structures: a list.\nIt consists of a number of cells pointing to each other:"}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-scala",children:"val list = List(1, 2, 3)\n// list: List[Int] = List(1, 2, 3)\n"})}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-scala",children:'diagram(list).render("list")\n'})}),"\n",(0,s.jsx)(n.p,{children:(0,s.jsx)(n.img,{alt:"list",src:a(8603).A+"",width:"322",height:"590"})}),"\n",(0,s.jsx)(n.p,{children:"Elements can be added to or removed from the front of the list with no effort,\nbecause we can share the same cells across several lists.\nThis would not be possible with a mutable list,\nsince modifying the shared part would modify every data structure making use of it."}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-scala",children:"val add = 0 :: list\n// add: List[Int] = List(0, 1, 2, 3)\nval remove = list.tail\n// remove: List[Int] = List(2, 3)\n"})}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-scala",children:'(diagram(list) + diagram(add) + diagram(remove)).render("lists")\n'})}),"\n",(0,s.jsx)(n.p,{children:(0,s.jsx)(n.img,{alt:"lists",src:a(2600).A+"",width:"455",height:"722"})}),"\n",(0,s.jsxs)(n.p,{children:["However we can\u2019t easily add elements at the end of the list, since the last cell\nis pointing to the empty list (",(0,s.jsx)(n.code,{children:"Nil"}),") and is immutable, i.e. cannot be changed.\nThus we are forced to create a new list every time:"]}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-scala",children:'(Animation\n .startWith(List(1))\n .iterate(_ :+ 2, _ :+ 3, _ :+ 4)\n .build()\n .render("list-append", tweakAnimation = _.withOnionSkinLayers(3)))\n'})}),"\n",(0,s.jsx)(n.p,{children:(0,s.jsx)(n.img,{alt:"list-append",src:a(2773).A+"",width:"675",height:"722"})}),"\n",(0,s.jsx)(n.p,{children:"This certainly does not look efficient compared to adding elements at the front:"}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-scala",children:'(Animation\n .startWith(List(1))\n .iterate(2 :: _, 3 :: _, 4 :: _)\n .build()\n .render("list-prepend"))\n'})}),"\n",(0,s.jsx)(n.p,{children:(0,s.jsx)(n.img,{alt:"list-prepend",src:a(2515).A+"",width:"457",height:"722"})}),"\n",(0,s.jsx)(n.h3,{id:"queues",children:"Queues"}),"\n",(0,s.jsx)(n.p,{children:"If we want to add elements on both sides efficiently, we need a different data structure: a queue.\nThe queue below, also known as a \u201cBanker\u2019s Queue\u201d, has two lists: one for prepending and one for appending."}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-scala",children:"val queue1 = Queue(1, 2, 3)\n// queue1: Queue[Int] = Queue(1, 2, 3)\nval queue2 = (queue1 :+ 4).tail\n// queue2: Queue[Int] = Queue(2, 3, 4)\n"})}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-scala",children:'(diagram(queue1) + diagram(queue2)).render("queues", _.withVerticalSpacing(1.2))\n'})}),"\n",(0,s.jsx)(n.p,{children:(0,s.jsx)(n.img,{alt:"queues",src:a(5623).A+"",width:"607",height:"992"})}),"\n",(0,s.jsx)(n.p,{children:"This way we can add and remove elements very easily at both ends.\nExcept when we try to remove an element and the respective list is empty!\nIn this case the queue will rotate the other list to make use of its elements.\nAlthough this operation is expensive, the usage pattern intended for a queue\nmakes it rare enough to yield great average (\u201cammortized\u201d) performance:"}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-scala",children:'(Animation\n .startWith(Queue(1, 2, 3))\n .repeat(3)(_.iterate(2)(q => q :+ (q.max + 1)).iterate(2)(_.tail))\n .build(Diagram.toStringCaption(_).withAnchor("queue"))\n .render("queue"))\n'})}),"\n",(0,s.jsx)(n.p,{children:(0,s.jsx)(n.img,{alt:"queue",src:a(9696).A+"",width:"540",height:"922"})}),"\n",(0,s.jsx)(n.h3,{id:"vectors",children:"Vectors"}),"\n",(0,s.jsxs)(n.p,{children:["One downside common to both lists and queues we saw before is that to get an element by index,\nwe need to potentially traverse the whole structure. A ",(0,s.jsx)(n.code,{children:"Vector"})," is a powerful data structure\naddressing this shortcoming and available in Scala (among other languages, like Clojure)."]}),"\n",(0,s.jsx)(n.p,{children:"Internally vectors utilize up to 6 layers of arrays, where 32 elements sit on the first layer,\n1024 \u2014 on the second, 32^3 \u2014 on the third, etc.\nTherefore getting any element by its index requires at most 6 pointer dereferences,\nwhich can be deemed constant time (yes, the trick is that the number of elements that can\nbe stored is limited by 2^31)."}),"\n",(0,s.jsx)(n.p,{children:"The internal 32-element arrays form the basic structural sharing blocks.\nFor small vectors they will be recreated on most operations:"}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-scala",children:"val vector1 = (1 to 20).toVector\n// vector1: Vector[Int] = Vector(1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20)\nval vector2 = vector1 :+ 21\n// vector2: Vector[Int] = Vector(1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21)\n"})}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-scala",children:'(diagram(vector1) + diagram(vector2)).render("vectors", _.withVerticalSpacing(2))\n'})}),"\n",(0,s.jsx)(n.p,{children:(0,s.jsx)(n.img,{alt:"vectors",src:a(1999).A+"",width:"2022",height:"601"})}),"\n",(0,s.jsx)(n.p,{children:"However as more layers leap into action, a huge chunk of the data can be shared:"}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-scala",children:"val vector3 = (1 to 100).toVector\n// vector3: Vector[Int] = Vector(1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 91, 92, 93, 94, 95, 96, 97, 98, 99, 100)\nval vector4 = vector3 :+ 21\n// vector4: Vector[Int] = Vector(1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 91, 92, 93, 94, 95, 96, 97, 98, 99, 100, 21)\n"})}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-scala",children:'(diagram(vector3) + diagram(vector4)).render("big-vectors", _.withVerticalSpacing(2))\n'})}),"\n",(0,s.jsx)(n.p,{children:(0,s.jsx)(n.img,{alt:"big-vectors",src:a(5390).A+"",width:"3785",height:"853"})}),"\n",(0,s.jsxs)(n.p,{children:["If you want to know more, this structure is covered in great detail by Jean Niklas L\u2019orange\n",(0,s.jsx)(n.a,{href:"http://hypirion.com/musings/understanding-persistent-vector-pt-1",children:"in his blog"}),".\nI also highly recommend watching ",(0,s.jsx)(n.a,{href:"https://www.youtube.com/watch?v=pNhBQJN44YQ",children:"this talk"})," by Daniel Spiewak."]}),"\n",(0,s.jsx)(n.h3,{id:"finger-trees",children:"Finger Trees"}),"\n",(0,s.jsxs)(n.p,{children:["To conclude this section, I would like to share a slightly less popular, but beautifully designed\ndata structure called \u201cfinger tree\u201d described in ",(0,s.jsx)(n.a,{href:"http://www.cs.ox.ac.uk/ralf.hinze/publications/FingerTrees.pdf",children:"this paper"}),"\nby Hinze and Paterson. Enjoy the read and this animation of a finger tree getting filled with some numbers:"]}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-scala",children:'import de.sciss.fingertree.{FingerTree, Measure}\nimport reftree.contrib.FingerTreeInstances._\n\nimplicit val measure = Measure.Indexed\n\nAnimation\n .startWith(FingerTree(1))\n .iterateWithIndex(21)((t, i) => t :+ (i + 1))\n .build(Diagram(_).withCaption("Finger Tree").withAnchor("tree"))\n .render("finger", _.withDensity(75).withVerticalSpacing(2))\n'})}),"\n",(0,s.jsx)(n.p,{children:(0,s.jsx)(n.img,{alt:"finger",src:a(7580).A+"",width:"1730",height:"1160"})}),"\n",(0,s.jsx)(n.h2,{id:"lenses",children:"Lenses"}),"\n",(0,s.jsxs)(n.p,{children:["So far we were looking into \u201cstandard\u201d data structures,\nbut in our code we often have to deal with custom data structures comprising our domain model.\nUpdating this sort of data can be tricky if it\u2019s immutable.\nFor case classes Scala gives us the ",(0,s.jsx)(n.code,{children:"copy"})," method:"]}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-scala",children:"case class Employee(\n name: String,\n salary: Long\n)\n"})}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-scala",children:'employee\n// res7: Employee = Employee(name = "Michael", salary = 4000L)\nval raisedEmployee = employee.copy(salary = employee.salary + 10)\n// raisedEmployee: Employee = Employee(name = "Michael", salary = 4010L)\n'})}),"\n",(0,s.jsxs)(n.p,{children:["However once composition comes into play, the resulting nested immutable data structures\nwould require a lot of ",(0,s.jsx)(n.code,{children:"copy"})," calls:"]}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-scala",children:"case class Employee(\n name: String,\n salary: Long\n)\n\ncase class Startup(\n name: String,\n founder: Employee,\n team: List[Employee]\n)\n"})}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-scala",children:'startup\n// res8: Startup = Startup(\n// name = "Acme",\n// founder = Employee(name = "Michael", salary = 4000L),\n// team = List(\n// Employee(name = "Adam", salary = 2100L),\n// Employee(name = "Bella", salary = 2100L),\n// Employee(name = "Chad", salary = 1980L),\n// Employee(name = "Delia", salary = 1850L)\n// )\n// )\nval raisedFounder = startup.copy(\n founder = startup.founder.copy(\n salary = startup.founder.salary + 10\n )\n)\n// raisedFounder: Startup = Startup(\n// name = "Acme",\n// founder = Employee(name = "Michael", salary = 4010L),\n// team = List(\n// Employee(name = "Adam", salary = 2100L),\n// Employee(name = "Bella", salary = 2100L),\n// Employee(name = "Chad", salary = 1980L),\n// Employee(name = "Delia", salary = 1850L)\n// )\n// )\n'})}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-scala",children:"// extra declarations for this section\nimport reftree.contrib.SimplifiedInstances.{list => listInstance}\nimport reftree.contrib.OpticInstances._\n"})}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-scala",children:'(diagram(startup) + diagram(raisedFounder)).render("startup")\n'})}),"\n",(0,s.jsx)(n.p,{children:(0,s.jsx)(n.img,{alt:"startup",src:a(7056).A+"",width:"1977",height:"701"})}),"\n",(0,s.jsx)(n.p,{children:"Ouch!"}),"\n",(0,s.jsxs)(n.p,{children:["A common solution to this problem is a \u201clens\u201d.\nIn the simplest case a lens is a pair of functions to get and set a value of type ",(0,s.jsx)(n.code,{children:"B"})," inside a value of type ",(0,s.jsx)(n.code,{children:"A"}),".\nIt\u2019s called a lens because it focuses on some part of the data and allows to update it.\nFor example, here is a lens that focuses on an employee\u2019s salary\n(using the excellent ",(0,s.jsx)(n.a,{href:"https://github.com/julien-truffaut/Monocle",children:"Monocle library"}),"):"]}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-scala",children:'import monocle.macros.GenLens\n\nval salaryLens = GenLens[Employee](_.salary)\n// salaryLens: monocle.package.Lens[Employee, Long] = repl.MdocSession$MdocApp$$anon$1@5eedf892\n\nsalaryLens.get(startup.founder)\n// res10: Long = 4000L\nsalaryLens.modify(s => s + 10)(startup.founder)\n// res11: Employee = Employee(name = "Michael", salary = 4010L)\n'})}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-scala",children:'diagram(OpticFocus(salaryLens, startup.founder)).render("salaryLens")\n'})}),"\n",(0,s.jsx)(n.p,{children:(0,s.jsx)(n.img,{alt:"salaryLens",src:a(7817).A+"",width:"592",height:"363"})}),"\n",(0,s.jsx)(n.p,{children:"We can also define a lens that focuses on the startup\u2019s founder:"}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-scala",children:'val founderLens = GenLens[Startup](_.founder)\n// founderLens: monocle.package.Lens[Startup, Employee] = repl.MdocSession$MdocApp$$anon$2@56f23749\n\nfounderLens.get(startup)\n// res13: Employee = Employee(name = "Michael", salary = 4000L)\n'})}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-scala",children:'diagram(OpticFocus(founderLens, startup)).render("founderLens")\n'})}),"\n",(0,s.jsx)(n.p,{children:(0,s.jsx)(n.img,{alt:"founderLens",src:a(1738).A+"",width:"1832",height:"701"})}),"\n",(0,s.jsx)(n.p,{children:"It\u2019s not apparent yet how this would help, but the trick is that lenses can be composed:"}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-scala",children:'val founderSalaryLens = founderLens composeLens salaryLens\n// founderSalaryLens: monocle.PLens[Startup, Startup, Long, Long] = monocle.PLens$$anon$1@3b84f3b2\n\nfounderSalaryLens.get(startup)\n// res15: Long = 4000L\nfounderSalaryLens.modify(s => s + 10)(startup)\n// res16: Startup = Startup(\n// name = "Acme",\n// founder = Employee(name = "Michael", salary = 4010L),\n// team = List(\n// Employee(name = "Adam", salary = 2100L),\n// Employee(name = "Bella", salary = 2100L),\n// Employee(name = "Chad", salary = 1980L),\n// Employee(name = "Delia", salary = 1850L)\n// )\n// )\n'})}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-scala",children:'diagram(OpticFocus(founderSalaryLens, startup)).render("founderSalaryLens")\n'})}),"\n",(0,s.jsx)(n.p,{children:(0,s.jsx)(n.img,{alt:"founderSalaryLens",src:a(5288).A+"",width:"1866",height:"701"})}),"\n",(0,s.jsx)(n.p,{children:"One interesting thing is that lenses can focus on anything, not just direct attributes of the data.\nHere is a traversal \u2014 a more generic kind of lens \u2014 that focuses on all vowels in a string:"}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-scala",children:'diagram(OpticFocus(vowelTraversal, "example")).render("vowelTraversal")\n'})}),"\n",(0,s.jsx)(n.p,{children:(0,s.jsx)(n.img,{alt:"vowelTraversal",src:a(2964).A+"",width:"494",height:"193"})}),"\n",(0,s.jsx)(n.p,{children:"We can use it to give our founder a funny name:"}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-scala",children:'val employeeNameLens = GenLens[Employee](_.name)\n// employeeNameLens: monocle.package.Lens[Employee, String] = repl.MdocSession$MdocApp$$anon$3@47f92c7a\nval founderVowelTraversal = founderLens composeLens employeeNameLens composeTraversal vowelTraversal\n// founderVowelTraversal: monocle.PTraversal[Startup, Startup, Char, Char] = monocle.PTraversal$$anon$2@73f506c1\n\nfounderVowelTraversal.modify(v => v.toUpper)(startup)\n// res19: Startup = Startup(\n// name = "Acme",\n// founder = Employee(name = "MIchAEl", salary = 4000L),\n// team = List(\n// Employee(name = "Adam", salary = 2100L),\n// Employee(name = "Bella", salary = 2100L),\n// Employee(name = "Chad", salary = 1980L),\n// Employee(name = "Delia", salary = 1850L)\n// )\n// )\n'})}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-scala",children:'diagram(OpticFocus(founderVowelTraversal, startup)).render("founderVowelTraversal")\n'})}),"\n",(0,s.jsx)(n.p,{children:(0,s.jsx)(n.img,{alt:"founderVowelTraversal",src:a(7289).A+"",width:"1889",height:"701"})}),"\n",(0,s.jsxs)(n.p,{children:["So far we have replaced the ",(0,s.jsx)(n.code,{children:"copy"})," boilerplate with a number of lens declarations.\nHowever most of the time our goal is just to update data."]}),"\n",(0,s.jsxs)(n.p,{children:["In Scala there is a great library called ",(0,s.jsx)(n.a,{href:"https://github.com/adamw/quicklens",children:"quicklens"}),"\nthat allows to do exactly that, creating all the necessary lenses under the hood:"]}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-scala",children:'import com.softwaremill.quicklens._\n\nval raisedCeo = startup.modify(_.founder.salary).using(s => s + 10)\n// raisedCeo: Startup = Startup(\n// name = "Acme",\n// founder = Employee(name = "Michael", salary = 4010L),\n// team = List(\n// Employee(name = "Adam", salary = 2100L),\n// Employee(name = "Bella", salary = 2100L),\n// Employee(name = "Chad", salary = 1980L),\n// Employee(name = "Delia", salary = 1850L)\n// )\n// )\n'})}),"\n",(0,s.jsx)(n.p,{children:"You might think this is approaching the syntax for updating mutable data,\nbut actually we have already surpassed it, since lenses are much more flexible:"}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-scala",children:'val raisedEveryone = startup.modifyAll(_.founder.salary, _.team.each.salary).using(s => s + 10)\n// raisedEveryone: Startup = Startup(\n// name = "Acme",\n// founder = Employee(name = "Michael", salary = 4010L),\n// team = List(\n// Employee(name = "Adam", salary = 2110L),\n// Employee(name = "Bella", salary = 2110L),\n// Employee(name = "Chad", salary = 1990L),\n// Employee(name = "Delia", salary = 1860L)\n// )\n// )\n'})}),"\n",(0,s.jsx)(n.h2,{id:"zippers",children:"Zippers"}),"\n",(0,s.jsx)(n.p,{children:"In our domain models we are often faced with recursive data structures.\nConsider this example:"}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-scala",children:"case class Employee(\n name: String,\n salary: Long\n)\n\ncase class Hierarchy(\n employee: Employee,\n team: List[Hierarchy]\n)\n\ncase class Company(\n name: String,\n hierarchy: Hierarchy\n)\n"})}),"\n",(0,s.jsxs)(n.p,{children:["The ",(0,s.jsx)(n.code,{children:"Hierarchy"})," class refers to itself.\nLet\u2019s grab a company object and display its hierarchy as a tree:"]}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-scala",children:"// extra declarations for this section\nimport zipper._\nimport reftree.contrib.SimplifiedInstances.option\nimport reftree.contrib.ZipperInstances._\n"})}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-scala",children:'diagram(company.hierarchy).render("company")\n'})}),"\n",(0,s.jsx)(n.p,{children:(0,s.jsx)(n.img,{alt:"company",src:a(3422).A+"",width:"2283",height:"1210"})}),"\n",(0,s.jsxs)(n.p,{children:["What if we want to navigate through this tree and modify it along the way?\nWe can use ",(0,s.jsx)(n.a,{href:"#lenses",children:"lenses"}),", but the recursive nature of the tree allows for a better solution."]}),"\n",(0,s.jsxs)(n.p,{children:["This solution is called a \u201cZipper\u201d, and was introduced by G\xe9rard Huet in 1997.\nIt consists of a \u201ccursor\u201d pointing to a location anywhere in the tree \u2014 \u201ccurrent focus\u201d.\nThe cursor can be moved freely with operations like ",(0,s.jsx)(n.code,{children:"moveDownLeft"}),", ",(0,s.jsx)(n.code,{children:"moveRight"}),", ",(0,s.jsx)(n.code,{children:"moveUp"}),", etc.\nCurrent focus can be updated, deleted, or new nodes can be inserted to its left or right.\nZippers are immutable, and every operation returns a new Zipper.\nAll the changes made to the tree can be committed, yielding a new modified version of the original tree."]}),"\n",(0,s.jsx)(n.p,{children:"Here is how we would insert a new employee into the hierarchy:"}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-scala",children:"val updatedHierarchy = Zipper(company.hierarchy).moveDownRight.moveDownRight.insertRight(newHire).commit\n"})}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-scala",children:'(diagram(company.hierarchy) + diagram(updatedHierarchy)).render("updatedHierarchy")\n'})}),"\n",(0,s.jsx)(n.p,{children:(0,s.jsx)(n.img,{alt:"updatedHierarchy",src:a(365).A+"",width:"2505",height:"1210"})}),"\n",(0,s.jsxs)(n.p,{children:["My ",(0,s.jsx)(n.a,{href:"https://github.com/stanch/zipper#zipper--an-implementation-of-huets-zipper",children:"zipper library"}),"\nprovides a few useful movements and operations."]}),"\n",(0,s.jsx)(n.p,{children:"Let\u2019s consider a simpler recursive data structure:"}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-scala",children:"case class Tree(x: Int, c: List[Tree] = List.empty)\n"})}),"\n",(0,s.jsx)(n.p,{children:"and a simple tree:"}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-scala",children:"simpleTree\n// res23: Tree = Tree(\n// x = 1,\n// c = List(\n// Tree(x = 2, c = List()),\n// Tree(x = 3, c = List()),\n// Tree(x = 4, c = List()),\n// Tree(x = 5, c = List(Tree(x = 6, c = List()), Tree(x = 7, c = List())))\n// )\n// )\n"})}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-scala",children:'diagram(simpleTree).render("simpleTree")\n'})}),"\n",(0,s.jsx)(n.p,{children:(0,s.jsx)(n.img,{alt:"simpleTree",src:a(977).A+"",width:"925",height:"721"})}),"\n",(0,s.jsx)(n.p,{children:"When we wrap a Zipper around this tree, it does not look very interesting yet:"}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-scala",children:"val zipper1 = Zipper(simpleTree)\n"})}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-scala",children:'(diagram(simpleTree) + diagram(zipper1)).render("zipper1")\n'})}),"\n",(0,s.jsx)(n.p,{children:(0,s.jsx)(n.img,{alt:"zipper1",src:a(3596).A+"",width:"998",height:"890"})}),"\n",(0,s.jsx)(n.p,{children:"We can see that it just points to the original tree and has some other empty fields.\nMore specifically, a Zipper consists of four pointers:"}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-scala",children:"case class Zipper[A](\n left: List[A], // left siblings of the focus\n focus: A, // the current focus\n right: List[A], // right siblings of the focus\n top: Option[Zipper[A]] // the parent zipper\n)\n"})}),"\n",(0,s.jsx)(n.p,{children:"In this case the focus is the root of the tree, which has no siblings,\nand the parent zipper does not exist, since we are at the top level."}),"\n",(0,s.jsx)(n.p,{children:"One thing we can do right away is modify the focus:"}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-scala",children:"val zipper2 = zipper1.update(focus => focus.copy(x = focus.x + 99))\n"})}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-scala",children:'(diagram(simpleTree) + diagram(zipper1) + diagram(zipper2)).render("zipper2")\n'})}),"\n",(0,s.jsx)(n.p,{children:(0,s.jsx)(n.img,{alt:"zipper2",src:a(7879).A+"",width:"1299",height:"890"})}),"\n",(0,s.jsx)(n.p,{children:"We just created a new tree! To obtain it, we have to commit the changes:"}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-scala",children:"val tree2 = zipper2.commit\n"})}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-scala",children:'(diagram(simpleTree) + diagram(tree2)).render("tree2")\n'})}),"\n",(0,s.jsx)(n.p,{children:(0,s.jsx)(n.img,{alt:"tree2",src:a(4025).A+"",width:"947",height:"721"})}),"\n",(0,s.jsx)(n.p,{children:"If you were following closely,\nyou would notice that nothing spectacular happened yet:\nwe could\u2019ve easily obtained the same result by modifying the tree directly:"}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-scala",children:"val tree2b = simpleTree.copy(x = simpleTree.x + 99)\n\nassert(tree2b == tree2)\n"})}),"\n",(0,s.jsx)(n.p,{children:"The power of Zipper becomes apparent when we go one or more levels deep.\nTo move down the tree, we \u201cunzip\u201d it, separating the child nodes into\nthe focused node and its left and right siblings:"}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-scala",children:"val zipper3 = zipper1.moveDownLeft\n"})}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-scala",children:'(diagram(zipper1) + diagram(zipper3)).render("zipper1+3")\n'})}),"\n",(0,s.jsx)(n.p,{children:(0,s.jsx)(n.img,{alt:"zipper1+3",src:a(3146).A+"",width:"973",height:"1060"})}),"\n",(0,s.jsx)(n.p,{children:"The new Zipper links to the old one,\nwhich will allow us to return to the root of the tree when we are done applying changes.\nThis link however prevents us from seeing the picture clearly.\nLet\u2019s look at the second zipper alone:"}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-scala",children:'diagram(zipper3).render("zipper3")\n'})}),"\n",(0,s.jsx)(n.p,{children:(0,s.jsx)(n.img,{alt:"zipper3",src:a(4862).A+"",width:"927",height:"758"})}),"\n",(0,s.jsxs)(n.p,{children:["Great! We have ",(0,s.jsx)(n.code,{children:"2"})," in focus and ",(0,s.jsx)(n.code,{children:"3, 4, 5"})," as right siblings. What happens if we move right a bit?"]}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-scala",children:"val zipper4 = zipper3.moveRightBy(2)\n"})}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-scala",children:'diagram(zipper4).render("zipper4")\n'})}),"\n",(0,s.jsx)(n.p,{children:(0,s.jsx)(n.img,{alt:"zipper4",src:a(3358).A+"",width:"818",height:"758"})}),"\n",(0,s.jsx)(n.p,{children:"This is interesting! Notice that the left siblings are \u201cinverted\u201d.\nThis allows to move left and right in constant time, because the sibling\nadjacent to the focus is always at the head of the list."}),"\n",(0,s.jsx)(n.p,{children:"This also allows us to insert new siblings easily:"}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-scala",children:"val zipper5 = zipper4.insertLeft(Tree(34))\n"})}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-scala",children:'diagram(zipper5).render("zipper5")\n'})}),"\n",(0,s.jsx)(n.p,{children:(0,s.jsx)(n.img,{alt:"zipper5",src:a(2712).A+"",width:"935",height:"758"})}),"\n",(0,s.jsx)(n.p,{children:"And, as you might know, we can delete nodes and update the focus:"}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-scala",children:"val zipper6 = zipper5.deleteAndMoveRight.set(Tree(45))\n"})}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-scala",children:'diagram(zipper6).render("zipper6")\n'})}),"\n",(0,s.jsx)(n.p,{children:(0,s.jsx)(n.img,{alt:"zipper6",src:a(7203).A+"",width:"531",height:"494"})}),"\n",(0,s.jsx)(n.p,{children:"Finally, when we move up, the siblings at the current level are \u201czipped\u201d\ntogether and their parent node is updated:"}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-scala",children:"val zipper7 = zipper6.moveUp\n"})}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-scala",children:'diagram(zipper7).render("zipper7")\n'})}),"\n",(0,s.jsx)(n.p,{children:(0,s.jsx)(n.img,{alt:"zipper7",src:a(1994).A+"",width:"777",height:"626"})}),"\n",(0,s.jsxs)(n.p,{children:["You can probably guess by now that ",(0,s.jsx)(n.code,{children:".commit"})," is a shorthand for going\nall the way up (applying all the changes) and returning the focus:"]}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-scala",children:"val tree3a = zipper6.moveUp.focus\nval tree3b = zipper6.commit\n\nassert(tree3a == tree3b)\n"})}),"\n",(0,s.jsx)(n.p,{children:"Here is an animation of the navigation process:"}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-scala",children:'val movement = Animation\n .startWith(Zipper(Data.simpleTree))\n .iterate(\n _.moveDownLeft,\n _.moveRight, _.moveRight, _.moveRight,\n _.moveDownLeft,\n _.moveRight, _.moveLeft,\n _.top.get,\n _.moveLeft, _.moveLeft, _.moveLeft,\n _.top.get\n )\n\nval trees = movement\n .build(z => Diagram(ZipperFocus(z, Data.simpleTree)).withCaption("Tree").withAnchor("tree"))\n .toNamespace("tree")\n\nval zippers = movement\n .build(Diagram(_).withCaption("Zipper").withAnchor("zipper").withColor(2))\n .toNamespace("zipper")\n\n(trees + zippers).render("tree+zipper")\n'})}),"\n",(0,s.jsx)(n.p,{children:(0,s.jsx)(n.img,{alt:"tree+zipper",src:a(1864).A+"",width:"1241",height:"690"})}),"\n",(0,s.jsx)(n.h2,{id:"useful-resources",children:"Useful resources"}),"\n",(0,s.jsx)(n.h3,{id:"books-papers-and-talks",children:"Books, papers and talks"}),"\n",(0,s.jsxs)(n.ul,{children:["\n",(0,s.jsxs)(n.li,{children:[(0,s.jsx)(n.a,{href:"http://www.amazon.com/Purely-Functional-Structures-Chris-Okasaki/dp/0521663504",children:"Purely functional data structures"})," by Chris Okasaki,\nand/or ",(0,s.jsx)(n.a,{href:"https://www.cs.cmu.edu/~rwh/theses/okasaki.pdf",children:"his PhD thesis"})," \u2014 ",(0,s.jsx)(n.em,{children:"the"})," introduction to immutable data structures"]}),"\n",(0,s.jsxs)(n.li,{children:[(0,s.jsx)(n.a,{href:"http://cstheory.stackexchange.com/a/1550",children:"What\u2019s new in purely functional data structures since Okasaki"})," \u2014 an excellent StackExchange answer\nwith pointers for further reading"]}),"\n",(0,s.jsxs)(n.li,{children:[(0,s.jsx)(n.a,{href:"https://www.youtube.com/watch?v=pNhBQJN44YQ",children:"Extreme cleverness"})," by Daniel Spiewak \u2014 a superb talk\ncovering several immutable data structures (implemented ",(0,s.jsx)(n.a,{href:"https://github.com/djspiewak/extreme-cleverness",children:"here"}),")"]}),"\n",(0,s.jsxs)(n.li,{children:[(0,s.jsx)(n.a,{href:"http://hypirion.com/musings/understanding-persistent-vector-pt-1",children:"Understanding Clojure\u2019s Persistent Vectors, part 1"}),"\nand ",(0,s.jsx)(n.a,{href:"http://hypirion.com/musings/understanding-persistent-vector-pt-2",children:"part 2"})," \u2014 a series of blog posts by Jean Niklas L\u2019orange"]}),"\n",(0,s.jsxs)(n.li,{children:[(0,s.jsx)(n.a,{href:"http://www.cs.ox.ac.uk/ralf.hinze/publications/FingerTrees.pdf",children:"Finger Trees"})," and\n",(0,s.jsx)(n.a,{href:"http://www.cs.ox.ac.uk/ralf.hinze/publications/Brother12.pdf",children:"1-2 Brother Trees"})," described by Hinze and Paterson"]}),"\n",(0,s.jsxs)(n.li,{children:[(0,s.jsx)(n.a,{href:"https://www.st.cs.uni-saarland.de/edu/seminare/2005/advanced-fp/docs/huet-zipper.pdf",children:"Huet\u2019s original Zipper paper"})," \u2014 a great short read\nintroducing the Zipper"]}),"\n",(0,s.jsxs)(n.li,{children:[(0,s.jsx)(n.a,{href:"http://dspace.library.uu.nl/bitstream/handle/1874/2532/2001-33.pdf",children:"Weaving a web"})," by Hinze and Jeuring \u2014\nanother interesting Zipper-like approach"]}),"\n"]}),"\n",(0,s.jsx)(n.h3,{id:"scala-libraries",children:"Scala libraries"}),"\n",(0,s.jsxs)(n.ul,{children:["\n",(0,s.jsxs)(n.li,{children:[(0,s.jsx)(n.a,{href:"https://github.com/stanch/zipper",children:"zipper"})," \u2014 my Zipper implementation"]}),"\n",(0,s.jsxs)(n.li,{children:[(0,s.jsx)(n.a,{href:"https://github.com/julien-truffaut/Monocle",children:"Monocle"})," \u2014 an \u201coptics\u201d library"]}),"\n",(0,s.jsxs)(n.li,{children:[(0,s.jsx)(n.a,{href:"https://github.com/adamw/quicklens",children:"Quicklens"})," \u2014 a simpler way to update nested case classes"]}),"\n",(0,s.jsxs)(n.li,{children:[(0,s.jsx)(n.a,{href:"https://github.com/Sciss/FingerTree",children:"FingerTree"})," \u2014 an implementation of the Finger Tree data structure"]}),"\n"]})]})}function m(e={}){const{wrapper:n}={...(0,r.R)(),...e.components};return n?(0,s.jsx)(n,{...e,children:(0,s.jsx)(p,{...e})}):p(e)}},7580:(e,n,a)=>{a.d(n,{A:()=>s});const s=a.p+"assets/images/finger-b0999bed13c76359869b636227548cc6.gif"},5390:(e,n,a)=>{a.d(n,{A:()=>s});const s=a.p+"assets/images/big-vectors-4b0f5120bf4a32b8802dc41e520eb37a.png"},3422:(e,n,a)=>{a.d(n,{A:()=>s});const s=a.p+"assets/images/company-3188e3cf359d9f1c7224ffceed126083.png"},1738:(e,n,a)=>{a.d(n,{A:()=>s});const s=a.p+"assets/images/founderLens-005975324dcee19c77494d453f8647c2.png"},5288:(e,n,a)=>{a.d(n,{A:()=>s});const s=a.p+"assets/images/founderSalaryLens-89e3e6d28efff27c4c8f563cfa49d3ee.png"},7289:(e,n,a)=>{a.d(n,{A:()=>s});const s=a.p+"assets/images/founderVowelTraversal-d35014cf80294acb4990d554bcedb0d3.png"},2773:(e,n,a)=>{a.d(n,{A:()=>s});const s=a.p+"assets/images/list-append-063e66f9ba76613ce2a8ba351ea27ee8.gif"},2515:(e,n,a)=>{a.d(n,{A:()=>s});const s=a.p+"assets/images/list-prepend-d90a987fd8bfc72246aa9c61dbfcf1c8.gif"},8603:(e,n,a)=>{a.d(n,{A:()=>s});const s=a.p+"assets/images/list-a190da16976869e8e07bee62d501a9f7.png"},2600:(e,n,a)=>{a.d(n,{A:()=>s});const s=a.p+"assets/images/lists-58f15f89774ee685a16243b40e9ba7c2.png"},5623:(e,n,a)=>{a.d(n,{A:()=>s});const s=a.p+"assets/images/queues-5efd3f3abf9c377155a0b4b6de6a77f9.png"},7817:(e,n,a)=>{a.d(n,{A:()=>s});const s=a.p+"assets/images/salaryLens-0d89d07dc97f512aa7fe6b50f061cfcd.png"},977:(e,n,a)=>{a.d(n,{A:()=>s});const s=a.p+"assets/images/simpleTree-bbaebacbd585b97eea7d64d94848caf8.png"},7056:(e,n,a)=>{a.d(n,{A:()=>s});const s=a.p+"assets/images/startup-2a750a1231daf1c2d9c548867f54ffc1.png"},4025:(e,n,a)=>{a.d(n,{A:()=>s});const s=a.p+"assets/images/tree2-9e8f1e7e745ac62ee27f909cf0770ae0.png"},365:(e,n,a)=>{a.d(n,{A:()=>s});const s=a.p+"assets/images/updatedHierarchy-ca0b5d651e31d5815f0d14c96fd928ad.png"},1999:(e,n,a)=>{a.d(n,{A:()=>s});const s=a.p+"assets/images/vectors-d5f0252feb82955ab0584957427fb307.png"},2964:(e,n,a)=>{a.d(n,{A:()=>s});const s=a.p+"assets/images/vowelTraversal-6534f12953b90dc5b6d40f6ee44e6c27.png"},3146:(e,n,a)=>{a.d(n,{A:()=>s});const s=a.p+"assets/images/zipper1+3-dafaaae54d3abc9716c59963ceb6f171.png"},3596:(e,n,a)=>{a.d(n,{A:()=>s});const s=a.p+"assets/images/zipper1-4a80c9c87d37df30e02c064945cde2f6.png"},7879:(e,n,a)=>{a.d(n,{A:()=>s});const s=a.p+"assets/images/zipper2-1176392f67e450e82bfa63dabbeacf90.png"},4862:(e,n,a)=>{a.d(n,{A:()=>s});const s=a.p+"assets/images/zipper3-9ab483dc84adf5005cafecc77e51e394.png"},3358:(e,n,a)=>{a.d(n,{A:()=>s});const s=a.p+"assets/images/zipper4-283827a5d36341e55ced1d024b328633.png"},2712:(e,n,a)=>{a.d(n,{A:()=>s});const s=a.p+"assets/images/zipper5-ce83ff83213a95c042b81012c81a28fc.png"},7203:(e,n,a)=>{a.d(n,{A:()=>s});const s=a.p+"assets/images/zipper6-4431d6974f1d299e656fa1e5c8d54fb0.png"},1994:(e,n,a)=>{a.d(n,{A:()=>s});const s=a.p+"assets/images/zipper7-cd17a63fb6f4732884da00b544ffa549.png"},9696:(e,n,a)=>{a.d(n,{A:()=>s});const s=a.p+"assets/images/queue-668a708310e8e78475fbc9b757c69eb3.gif"},1864:(e,n,a)=>{a.d(n,{A:()=>s});const s=a.p+"assets/images/tree+zipper-b1cf73da287eda26f64b667ce8dcf311.gif"}}]); \ No newline at end of file diff --git a/assets/js/runtime~main.225eaf82.js b/assets/js/runtime~main.df705e48.js similarity index 96% rename from assets/js/runtime~main.225eaf82.js rename to assets/js/runtime~main.df705e48.js index f606b39..7cfd283 100644 --- a/assets/js/runtime~main.225eaf82.js +++ b/assets/js/runtime~main.df705e48.js @@ -1 +1 @@ -(()=>{"use strict";var e,r,a,t,o,c={},l={};function d(e){var r=l[e];if(void 0!==r)return r.exports;var a=l[e]={exports:{}};return c[e].call(a.exports,a,a.exports,d),a.exports}d.m=c,e=[],d.O=(r,a,t,o)=>{if(!a){var c=1/0;for(f=0;f=o)&&Object.keys(d.O).every((e=>d.O[e](a[n])))?a.splice(n--,1):(l=!1,o0&&e[f-1][2]>o;f--)e[f]=e[f-1];e[f]=[a,t,o]},d.n=e=>{var r=e&&e.__esModule?()=>e.default:()=>e;return d.d(r,{a:r}),r},a=Object.getPrototypeOf?e=>Object.getPrototypeOf(e):e=>e.__proto__,d.t=function(e,t){if(1&t&&(e=this(e)),8&t)return e;if("object"==typeof e&&e){if(4&t&&e.__esModule)return e;if(16&t&&"function"==typeof e.then)return e}var o=Object.create(null);d.r(o);var c={};r=r||[null,a({}),a([]),a(a)];for(var l=2&t&&e;"object"==typeof l&&!~r.indexOf(l);l=a(l))Object.getOwnPropertyNames(l).forEach((r=>c[r]=()=>e[r]));return c.default=()=>e,d.d(o,c),o},d.d=(e,r)=>{for(var a in r)d.o(r,a)&&!d.o(e,a)&&Object.defineProperty(e,a,{enumerable:!0,get:r[a]})},d.f={},d.e=e=>Promise.all(Object.keys(d.f).reduce(((r,a)=>(d.f[a](e,r),r)),[])),d.u=e=>"assets/js/"+({42:"reactPlayerTwitch",48:"a94703ab",66:"e7ed7793",98:"a7bd4aaa",173:"reactPlayerVimeo",235:"a7456010",328:"reactPlayerDailyMotion",340:"reactPlayerWistia",350:"66c1eed3",353:"reactPlayerPreview",392:"reactPlayerVidyard",396:"9d744931",401:"17896441",446:"reactPlayerYouTube",458:"reactPlayerFilePlayer",463:"reactPlayerKaltura",533:"71ffd77d",570:"reactPlayerMixcloud",627:"reactPlayerStreamable",634:"c4f5d8e4",647:"5e95c892",672:"22c5de9a",723:"reactPlayerMux",742:"aba21aa0",814:"b3d9ed0f",887:"reactPlayerFacebook",956:"37c4c8cd",979:"reactPlayerSoundCloud"}[e]||e)+"."+{42:"f8093db6",48:"a35f50c9",66:"090579fc",98:"8ba4f90d",132:"fe654836",173:"e0bb6f48",235:"d3f42fc7",237:"f4ea580f",328:"2786b40e",340:"55782d35",350:"1e9fb8bf",353:"e875752c",392:"fc0f3075",396:"c5feb62f",401:"dc33d5c6",446:"6b5e7fe3",458:"1d2055b5",463:"77229a02",533:"8264f7e5",570:"530faa1a",627:"2e005b20",634:"f29e2922",647:"b6ccb016",672:"773cf82f",723:"155186bf",742:"b00a85c6",814:"f977c0f4",887:"c103ff05",956:"a437f795",979:"199c972c"}[e]+".js",d.miniCssF=e=>{},d.g=function(){if("object"==typeof globalThis)return globalThis;try{return this||new Function("return this")()}catch(e){if("object"==typeof window)return window}}(),d.o=(e,r)=>Object.prototype.hasOwnProperty.call(e,r),t={},o="reftree:",d.l=(e,r,a,c)=>{if(t[e])t[e].push(r);else{var l,n;if(void 0!==a)for(var i=document.getElementsByTagName("script"),f=0;f{l.onerror=l.onload=null,clearTimeout(y);var o=t[e];if(delete t[e],l.parentNode&&l.parentNode.removeChild(l),o&&o.forEach((e=>e(a))),r)return r(a)},y=setTimeout(b.bind(null,void 0,{type:"timeout",target:l}),12e4);l.onerror=b.bind(null,l.onerror),l.onload=b.bind(null,l.onload),n&&document.head.appendChild(l)}},d.r=e=>{"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},d.p="/reftree/",d.gca=function(e){return e={17896441:"401",reactPlayerTwitch:"42",a94703ab:"48",e7ed7793:"66",a7bd4aaa:"98",reactPlayerVimeo:"173",a7456010:"235",reactPlayerDailyMotion:"328",reactPlayerWistia:"340","66c1eed3":"350",reactPlayerPreview:"353",reactPlayerVidyard:"392","9d744931":"396",reactPlayerYouTube:"446",reactPlayerFilePlayer:"458",reactPlayerKaltura:"463","71ffd77d":"533",reactPlayerMixcloud:"570",reactPlayerStreamable:"627",c4f5d8e4:"634","5e95c892":"647","22c5de9a":"672",reactPlayerMux:"723",aba21aa0:"742",b3d9ed0f:"814",reactPlayerFacebook:"887","37c4c8cd":"956",reactPlayerSoundCloud:"979"}[e]||e,d.p+d.u(e)},(()=>{var e={354:0,869:0};d.f.j=(r,a)=>{var t=d.o(e,r)?e[r]:void 0;if(0!==t)if(t)a.push(t[2]);else if(/^(354|869)$/.test(r))e[r]=0;else{var o=new Promise(((a,o)=>t=e[r]=[a,o]));a.push(t[2]=o);var c=d.p+d.u(r),l=new Error;d.l(c,(a=>{if(d.o(e,r)&&(0!==(t=e[r])&&(e[r]=void 0),t)){var o=a&&("load"===a.type?"missing":a.type),c=a&&a.target&&a.target.src;l.message="Loading chunk "+r+" failed.\n("+o+": "+c+")",l.name="ChunkLoadError",l.type=o,l.request=c,t[1](l)}}),"chunk-"+r,r)}},d.O.j=r=>0===e[r];var r=(r,a)=>{var t,o,c=a[0],l=a[1],n=a[2],i=0;if(c.some((r=>0!==e[r]))){for(t in l)d.o(l,t)&&(d.m[t]=l[t]);if(n)var f=n(d)}for(r&&r(a);i{"use strict";var e,r,a,t,o,c={},l={};function d(e){var r=l[e];if(void 0!==r)return r.exports;var a=l[e]={exports:{}};return c[e].call(a.exports,a,a.exports,d),a.exports}d.m=c,e=[],d.O=(r,a,t,o)=>{if(!a){var c=1/0;for(f=0;f=o)&&Object.keys(d.O).every((e=>d.O[e](a[n])))?a.splice(n--,1):(l=!1,o0&&e[f-1][2]>o;f--)e[f]=e[f-1];e[f]=[a,t,o]},d.n=e=>{var r=e&&e.__esModule?()=>e.default:()=>e;return d.d(r,{a:r}),r},a=Object.getPrototypeOf?e=>Object.getPrototypeOf(e):e=>e.__proto__,d.t=function(e,t){if(1&t&&(e=this(e)),8&t)return e;if("object"==typeof e&&e){if(4&t&&e.__esModule)return e;if(16&t&&"function"==typeof e.then)return e}var o=Object.create(null);d.r(o);var c={};r=r||[null,a({}),a([]),a(a)];for(var l=2&t&&e;"object"==typeof l&&!~r.indexOf(l);l=a(l))Object.getOwnPropertyNames(l).forEach((r=>c[r]=()=>e[r]));return c.default=()=>e,d.d(o,c),o},d.d=(e,r)=>{for(var a in r)d.o(r,a)&&!d.o(e,a)&&Object.defineProperty(e,a,{enumerable:!0,get:r[a]})},d.f={},d.e=e=>Promise.all(Object.keys(d.f).reduce(((r,a)=>(d.f[a](e,r),r)),[])),d.u=e=>"assets/js/"+({42:"reactPlayerTwitch",48:"a94703ab",66:"e7ed7793",98:"a7bd4aaa",173:"reactPlayerVimeo",235:"a7456010",328:"reactPlayerDailyMotion",340:"reactPlayerWistia",350:"66c1eed3",353:"reactPlayerPreview",392:"reactPlayerVidyard",396:"9d744931",401:"17896441",446:"reactPlayerYouTube",458:"reactPlayerFilePlayer",463:"reactPlayerKaltura",533:"71ffd77d",570:"reactPlayerMixcloud",627:"reactPlayerStreamable",634:"c4f5d8e4",647:"5e95c892",672:"22c5de9a",723:"reactPlayerMux",742:"aba21aa0",814:"b3d9ed0f",887:"reactPlayerFacebook",956:"37c4c8cd",979:"reactPlayerSoundCloud"}[e]||e)+"."+{42:"f8093db6",48:"a35f50c9",66:"090579fc",98:"8ba4f90d",132:"fe654836",173:"e0bb6f48",235:"d3f42fc7",237:"f4ea580f",328:"2786b40e",340:"55782d35",350:"1e9fb8bf",353:"e875752c",392:"fc0f3075",396:"c5feb62f",401:"dc33d5c6",446:"6b5e7fe3",458:"1d2055b5",463:"77229a02",533:"6ce84eb6",570:"530faa1a",627:"2e005b20",634:"f29e2922",647:"b6ccb016",672:"091aecae",723:"155186bf",742:"b00a85c6",814:"db6a0b09",887:"c103ff05",956:"a437f795",979:"199c972c"}[e]+".js",d.miniCssF=e=>{},d.g=function(){if("object"==typeof globalThis)return globalThis;try{return this||new Function("return this")()}catch(e){if("object"==typeof window)return window}}(),d.o=(e,r)=>Object.prototype.hasOwnProperty.call(e,r),t={},o="reftree:",d.l=(e,r,a,c)=>{if(t[e])t[e].push(r);else{var l,n;if(void 0!==a)for(var i=document.getElementsByTagName("script"),f=0;f{l.onerror=l.onload=null,clearTimeout(y);var o=t[e];if(delete t[e],l.parentNode&&l.parentNode.removeChild(l),o&&o.forEach((e=>e(a))),r)return r(a)},y=setTimeout(b.bind(null,void 0,{type:"timeout",target:l}),12e4);l.onerror=b.bind(null,l.onerror),l.onload=b.bind(null,l.onload),n&&document.head.appendChild(l)}},d.r=e=>{"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},d.p="/reftree/",d.gca=function(e){return e={17896441:"401",reactPlayerTwitch:"42",a94703ab:"48",e7ed7793:"66",a7bd4aaa:"98",reactPlayerVimeo:"173",a7456010:"235",reactPlayerDailyMotion:"328",reactPlayerWistia:"340","66c1eed3":"350",reactPlayerPreview:"353",reactPlayerVidyard:"392","9d744931":"396",reactPlayerYouTube:"446",reactPlayerFilePlayer:"458",reactPlayerKaltura:"463","71ffd77d":"533",reactPlayerMixcloud:"570",reactPlayerStreamable:"627",c4f5d8e4:"634","5e95c892":"647","22c5de9a":"672",reactPlayerMux:"723",aba21aa0:"742",b3d9ed0f:"814",reactPlayerFacebook:"887","37c4c8cd":"956",reactPlayerSoundCloud:"979"}[e]||e,d.p+d.u(e)},(()=>{var e={354:0,869:0};d.f.j=(r,a)=>{var t=d.o(e,r)?e[r]:void 0;if(0!==t)if(t)a.push(t[2]);else if(/^(354|869)$/.test(r))e[r]=0;else{var o=new Promise(((a,o)=>t=e[r]=[a,o]));a.push(t[2]=o);var c=d.p+d.u(r),l=new Error;d.l(c,(a=>{if(d.o(e,r)&&(0!==(t=e[r])&&(e[r]=void 0),t)){var o=a&&("load"===a.type?"missing":a.type),c=a&&a.target&&a.target.src;l.message="Loading chunk "+r+" failed.\n("+o+": "+c+")",l.name="ChunkLoadError",l.type=o,l.request=c,t[1](l)}}),"chunk-"+r,r)}},d.O.j=r=>0===e[r];var r=(r,a)=>{var t,o,c=a[0],l=a[1],n=a[2],i=0;if(c.some((r=>0!==e[r]))){for(t in l)d.o(l,t)&&(d.m[t]=l[t]);if(n)var f=n(d)}for(r&&r(a);i Getting started | RefTree - + @@ -17,9 +17,9 @@

Interactiv

To jump into an interactive session:

$ git clone https://github.com/stanch/reftree
$ cd reftree
$ sbt demo
@ render(List(1, 2, 3))
// display diagram.png with your favorite image viewer

Including in your project

-

You can depend on the library by adding these lines to your build.sbt:

+

reftree is available for Scala 2.12 and 2.13. You can depend on the library by adding these lines to your build.sbt:

-
libraryDependencies += "io.github.stanch" %% "reftree" % "1.5.0"
+
libraryDependencies += "io.github.stanch" %% "reftree" % "1.5.0"

Minimal example

import reftree.render.{Renderer, RenderingOptions}
import reftree.diagram.Diagram
import java.nio.file.Paths
val ImagePath = "images"
diff --git a/docs/Guide/index.html b/docs/Guide/index.html index 8f348e2..ef86717 100644 --- a/docs/Guide/index.html +++ b/docs/Guide/index.html @@ -4,7 +4,7 @@ User guide | RefTree - + diff --git a/docs/index.html b/docs/index.html index 603b64c..9d6f73a 100644 --- a/docs/index.html +++ b/docs/index.html @@ -4,7 +4,7 @@ Overview | RefTree - + diff --git a/docs/talks/Immutability/index.html b/docs/talks/Immutability/index.html index e8b4f54..07ce231 100644 --- a/docs/talks/Immutability/index.html +++ b/docs/talks/Immutability/index.html @@ -4,7 +4,7 @@ Unzipping Immutability | RefTree - + @@ -113,15 +113,15 @@

LensesMonocle library):

-
import monocle.macros.GenLens

val salaryLens = GenLens[Employee](_.salary)
// salaryLens: monocle.package.Lens[Employee, Long] = repl.MdocSession$MdocApp$$anon$1@3c477363

salaryLens.get(startup.founder)
// res10: Long = 4000L
salaryLens.modify(s => s + 10)(startup.founder)
// res11: Employee = Employee(name = "Michael", salary = 4010L)
+
import monocle.macros.GenLens

val salaryLens = GenLens[Employee](_.salary)
// salaryLens: monocle.package.Lens[Employee, Long] = repl.MdocSession$MdocApp$$anon$1@5eedf892

salaryLens.get(startup.founder)
// res10: Long = 4000L
salaryLens.modify(s => s + 10)(startup.founder)
// res11: Employee = Employee(name = "Michael", salary = 4010L)
diagram(OpticFocus(salaryLens, startup.founder)).render("salaryLens")

salaryLens

We can also define a lens that focuses on the startup’s founder:

-
val founderLens = GenLens[Startup](_.founder)
// founderLens: monocle.package.Lens[Startup, Employee] = repl.MdocSession$MdocApp$$anon$2@389ba703

founderLens.get(startup)
// res13: Employee = Employee(name = "Michael", salary = 4000L)
+
val founderLens = GenLens[Startup](_.founder)
// founderLens: monocle.package.Lens[Startup, Employee] = repl.MdocSession$MdocApp$$anon$2@56f23749

founderLens.get(startup)
// res13: Employee = Employee(name = "Michael", salary = 4000L)
diagram(OpticFocus(founderLens, startup)).render("founderLens")

founderLens

It’s not apparent yet how this would help, but the trick is that lenses can be composed:

-
val founderSalaryLens = founderLens composeLens salaryLens
// founderSalaryLens: monocle.PLens[Startup, Startup, Long, Long] = monocle.PLens$$anon$1@21c54dde

founderSalaryLens.get(startup)
// res15: Long = 4000L
founderSalaryLens.modify(s => s + 10)(startup)
// res16: Startup = Startup(
// name = "Acme",
// founder = Employee(name = "Michael", salary = 4010L),
// team = List(
// Employee(name = "Adam", salary = 2100L),
// Employee(name = "Bella", salary = 2100L),
// Employee(name = "Chad", salary = 1980L),
// Employee(name = "Delia", salary = 1850L)
// )
// )
+
val founderSalaryLens = founderLens composeLens salaryLens
// founderSalaryLens: monocle.PLens[Startup, Startup, Long, Long] = monocle.PLens$$anon$1@3b84f3b2

founderSalaryLens.get(startup)
// res15: Long = 4000L
founderSalaryLens.modify(s => s + 10)(startup)
// res16: Startup = Startup(
// name = "Acme",
// founder = Employee(name = "Michael", salary = 4010L),
// team = List(
// Employee(name = "Adam", salary = 2100L),
// Employee(name = "Bella", salary = 2100L),
// Employee(name = "Chad", salary = 1980L),
// Employee(name = "Delia", salary = 1850L)
// )
// )
diagram(OpticFocus(founderSalaryLens, startup)).render("founderSalaryLens")

founderSalaryLens

One interesting thing is that lenses can focus on anything, not just direct attributes of the data. @@ -129,7 +129,7 @@

Lenses
diagram(OpticFocus(vowelTraversal, "example")).render("vowelTraversal")

vowelTraversal

We can use it to give our founder a funny name:

-
val employeeNameLens = GenLens[Employee](_.name)
// employeeNameLens: monocle.package.Lens[Employee, String] = repl.MdocSession$MdocApp$$anon$3@8eeb1fa
val founderVowelTraversal = founderLens composeLens employeeNameLens composeTraversal vowelTraversal
// founderVowelTraversal: monocle.PTraversal[Startup, Startup, Char, Char] = monocle.PTraversal$$anon$2@37aed08b

founderVowelTraversal.modify(v => v.toUpper)(startup)
// res19: Startup = Startup(
// name = "Acme",
// founder = Employee(name = "MIchAEl", salary = 4000L),
// team = List(
// Employee(name = "Adam", salary = 2100L),
// Employee(name = "Bella", salary = 2100L),
// Employee(name = "Chad", salary = 1980L),
// Employee(name = "Delia", salary = 1850L)
// )
// )
+
val employeeNameLens = GenLens[Employee](_.name)
// employeeNameLens: monocle.package.Lens[Employee, String] = repl.MdocSession$MdocApp$$anon$3@47f92c7a
val founderVowelTraversal = founderLens composeLens employeeNameLens composeTraversal vowelTraversal
// founderVowelTraversal: monocle.PTraversal[Startup, Startup, Char, Char] = monocle.PTraversal$$anon$2@73f506c1

founderVowelTraversal.modify(v => v.toUpper)(startup)
// res19: Startup = Startup(
// name = "Acme",
// founder = Employee(name = "MIchAEl", salary = 4000L),
// team = List(
// Employee(name = "Adam", salary = 2100L),
// Employee(name = "Bella", salary = 2100L),
// Employee(name = "Chad", salary = 1980L),
// Employee(name = "Delia", salary = 1850L)
// )
// )
diagram(OpticFocus(founderVowelTraversal, startup)).render("founderVowelTraversal")

founderVowelTraversal

So far we have replaced the copy boilerplate with a number of lens declarations. diff --git a/docs/talks/Visualize/index.html b/docs/talks/Visualize/index.html index 4b93088..f22eeed 100644 --- a/docs/talks/Visualize/index.html +++ b/docs/talks/Visualize/index.html @@ -4,7 +4,7 @@ Visualize your data structures! | RefTree - + @@ -71,14 +71,14 @@

Inside Now, what does it look like? The best way to find out is to visualize a RefTree of a RefTree!

diagram(Shortcuts.refTree(bob)).render("reftree")
-

reftree

+

reftree

As you can see, it contains values (Val) and references (Ref).

How do we get from RefTree to an image though? This is where GraphViz comes in. From a RefTree we can obtain a graph definition that can be rendered by GraphViz:

-
Shortcuts.graph(bob).encode
// res5: String = """digraph "Diagram" {
// graph [ ranksep=0.8 bgcolor="#ffffff00" ]
// node [ shape="plaintext" fontname="Source Code Pro" fontcolor="#000000ff" ]
// edge [ arrowsize=0.7 color="#000000ff" ]
// "-repl.MdocSession$MdocApp$Person560852288" [ id="-repl.MdocSession$MdocApp$Person560852288" label=<<table cellspacing="0" cellpadding="6" cellborder="0" columns="*" bgcolor="#ffffff00" style="rounded"><tr><td port="n" rowspan="2">Person</td><td bgcolor="#ffffff00"><i>name</i></td><td bgcolor="#ffffff00"><i>age</i></td></tr><hr/><tr><td port="0" bgcolor="#ffffff00">&middot;</td><td bgcolor="#ffffff00">42</td></tr></table>> ] [ color="#104e8bff" fontcolor="#104e8bff" ]
// "-java.lang.String1983877187" [ id="-java.lang.String1983877187" label=<<table cellspacing="0" cellpadding="6" cellborder="0" columns="*" bgcolor="#ffffff00" style="rounded"><tr><td port="n" rowspan="2">&quot;Bob&quot;</td></tr></table>> ] [ color="#104e8bff" fontcolor="#104e8bff" ]
// "-repl.MdocSession$MdocApp$Person560852288":"0":"s" -> "-java.lang.String1983877187":"n":"n" [ id="-repl.MdocSession$MdocApp$Person560852288-0-java.lang.String1983877187" ] [ color="#104e8bff" ]
// }"""
+
Shortcuts.graph(bob).encode
// res5: String = """digraph "Diagram" {
// graph [ ranksep=0.8 bgcolor="#ffffff00" ]
// node [ shape="plaintext" fontname="Source Code Pro" fontcolor="#000000ff" ]
// edge [ arrowsize=0.7 color="#000000ff" ]
// "-repl.MdocSession$MdocApp$Person1385763504" [ id="-repl.MdocSession$MdocApp$Person1385763504" label=<<table cellspacing="0" cellpadding="6" cellborder="0" columns="*" bgcolor="#ffffff00" style="rounded"><tr><td port="n" rowspan="2">Person</td><td bgcolor="#ffffff00"><i>name</i></td><td bgcolor="#ffffff00"><i>age</i></td></tr><hr/><tr><td port="0" bgcolor="#ffffff00">&middot;</td><td bgcolor="#ffffff00">42</td></tr></table>> ] [ color="#104e8bff" fontcolor="#104e8bff" ]
// "-java.lang.String78928860" [ id="-java.lang.String78928860" label=<<table cellspacing="0" cellpadding="6" cellborder="0" columns="*" bgcolor="#ffffff00" style="rounded"><tr><td port="n" rowspan="2">&quot;Bob&quot;</td></tr></table>> ] [ color="#104e8bff" fontcolor="#104e8bff" ]
// "-repl.MdocSession$MdocApp$Person1385763504":"0":"s" -> "-java.lang.String78928860":"n":"n" [ id="-repl.MdocSession$MdocApp$Person1385763504-0-java.lang.String78928860" ] [ color="#104e8bff" ]
// }"""

Going even further, we can ask GraphViz for an SVG output:

-
Shortcuts.svg(bob)
// res6: xml.Node = <svg width="171pt" height="168pt" viewBox="0.00 0.00 171.00 168.00" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"><g id="graph0" class="graph"><title>Diagram</title><polygon fill="transparent" stroke="transparent" points="-4,4 -4,-164 167,-164 167,4 -4,4"/><!-- &#45;repl.MdocSession$MdocApp$Person560852288 --><g id="-repl.MdocSession$MdocApp$Person560852288" class="node"><title>-repl.MdocSession$MdocApp$Person560852288</title><path fill="transparent" stroke="transparent" d="M20,-100C20,-100 143,-100 143,-100 149,-100 155,-106 155,-112 155,-112 155,-144 155,-144 155,-150 149,-156 143,-156 143,-156 20,-156 20,-156 14,-156 8,-150 8,-144 8,-144 8,-112 8,-112 8,-106 14,-100 20,-100"/><text text-anchor="start" x="15.5" y="-124.3" font-family="Source Code Pro" font-size="14.00" fill="#104e8b">Person</text><polygon fill="transparent" stroke="transparent" points="71.5,-128 71.5,-155 117.5,-155 117.5,-128 71.5,-128"/><text text-anchor="start" x="77.5" y="-138.8" font-family="Source Code Pro" font-style="italic" font-size="14.00" fill="#104e8b">name</text><polygon fill="transparent" stroke="transparent" points="117.5,-128 117.5,-155 154.5,-155 154.5,-128 117.5,-128"/><text text-anchor="start" x="123.5" y="-138.8" font-family="Source Code Pro" font-style="italic" font-size="14.00" fill="#104e8b">age</text><polygon fill="transparent" stroke="transparent" points="71.5,-101 71.5,-128 117.5,-128 117.5,-101 71.5,-101"/><text text-anchor="start" x="90" y="-110.8" font-family="Source Code Pro" font-size="14.00" fill="#104e8b">·</text><polygon fill="transparent" stroke="transparent" points="117.5,-101 117.5,-128 154.5,-128 154.5,-101 117.5,-101"/><text text-anchor="start" x="127.5" y="-110.8" font-family="Source Code Pro" font-size="14.00" fill="#104e8b">42</text><polygon fill="#104e8b" stroke="#104e8b" points="71.5,-101 71.5,-156 71.5,-156 71.5,-101 71.5,-101"/><polygon fill="#104e8b" stroke="#104e8b" points="117.5,-128 117.5,-156 117.5,-156 117.5,-128 117.5,-128"/><polygon fill="#104e8b" stroke="#104e8b" points="71.5,-128 71.5,-128 117.5,-128 117.5,-128 71.5,-128"/><polygon fill="#104e8b" stroke="#104e8b" points="117.5,-128 117.5,-128 155.5,-128 155.5,-128 117.5,-128"/><polygon fill="#104e8b" stroke="#104e8b" points="117.5,-100 117.5,-128 117.5,-128 117.5,-100 117.5,-100"/><path fill="none" stroke="#104e8b" d="M20,-100C20,-100 143,-100 143,-100 149,-100 155,-106 155,-112 155,-112 155,-144 155,-144 155,-150 149,-156 143,-156 143,-156 20,-156 20,-156 14,-156 8,-150 8,-144 8,-144 8,-112 8,-112 8,-106 14,-100 20,-100"/></g><!-- &#45;java.lang.String1983877187 --><g id="-java.lang.String1983877187" class="node"><title>-java.lang.String1983877187</title><path fill="transparent" stroke="transparent" d="M76.5,-4C76.5,-4 112.5,-4 112.5,-4 117.5,-4 122.5,-9 122.5,-14 122.5,-14 122.5,-24 122.5,-24 122.5,-29 117.5,-34 112.5,-34 112.5,-34 76.5,-34 76.5,-34 71.5,-34 66.5,-29 66.5,-24 66.5,-24 66.5,-14 66.5,-14 66.5,-9 71.5,-4 76.5,-4"/><text text-anchor="start" x="73.5" y="-15.3" font-family="Source Code Pro" font-size="14.00" fill="#104e8b">&quot;Bob&quot;</text><path fill="none" stroke="#104e8b" d="M76.5,-4C76.5,-4 112.5,-4 112.5,-4 117.5,-4 122.5,-9 122.5,-14 122.5,-14 122.5,-24 122.5,-24 122.5,-29 117.5,-34 112.5,-34 112.5,-34 76.5,-34 76.5,-34 71.5,-34 66.5,-29 66.5,-24 66.5,-24 66.5,-14 66.5,-14 66.5,-9 71.5,-4 76.5,-4"/></g><!-- &#45;repl.MdocSession$MdocApp$Person560852288&#45;&gt;&#45;java.lang.String1983877187 --><g id="-repl.MdocSession$MdocApp$Person560852288-0-java.lang.String1983877187" class="edge"><title>-repl.MdocSession$MdocApp$Person560852288:s-&gt;-java.lang.String1983877187:n</title><path fill="none" stroke="#104e8b" d="M94.5,-100C94.5,-73.19 94.5,-64.76 94.5,-41.1"/><polygon fill="#104e8b" stroke="#104e8b" points="96.95,-41 94.5,-34 92.05,-41 96.95,-41"/></g></g></svg>
+
Shortcuts.svg(bob)
// res6: xml.Node = <svg width="171pt" height="168pt" viewBox="0.00 0.00 171.00 168.00" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"><g id="graph0" class="graph"><title>Diagram</title><polygon fill="transparent" stroke="transparent" points="-4,4 -4,-164 167,-164 167,4 -4,4"/><!-- &#45;repl.MdocSession$MdocApp$Person1385763504 --><g id="-repl.MdocSession$MdocApp$Person1385763504" class="node"><title>-repl.MdocSession$MdocApp$Person1385763504</title><path fill="transparent" stroke="transparent" d="M20,-100C20,-100 143,-100 143,-100 149,-100 155,-106 155,-112 155,-112 155,-144 155,-144 155,-150 149,-156 143,-156 143,-156 20,-156 20,-156 14,-156 8,-150 8,-144 8,-144 8,-112 8,-112 8,-106 14,-100 20,-100"/><text text-anchor="start" x="15.5" y="-124.3" font-family="Source Code Pro" font-size="14.00" fill="#104e8b">Person</text><polygon fill="transparent" stroke="transparent" points="71.5,-128 71.5,-155 117.5,-155 117.5,-128 71.5,-128"/><text text-anchor="start" x="77.5" y="-138.8" font-family="Source Code Pro" font-style="italic" font-size="14.00" fill="#104e8b">name</text><polygon fill="transparent" stroke="transparent" points="117.5,-128 117.5,-155 154.5,-155 154.5,-128 117.5,-128"/><text text-anchor="start" x="123.5" y="-138.8" font-family="Source Code Pro" font-style="italic" font-size="14.00" fill="#104e8b">age</text><polygon fill="transparent" stroke="transparent" points="71.5,-101 71.5,-128 117.5,-128 117.5,-101 71.5,-101"/><text text-anchor="start" x="90" y="-110.8" font-family="Source Code Pro" font-size="14.00" fill="#104e8b">·</text><polygon fill="transparent" stroke="transparent" points="117.5,-101 117.5,-128 154.5,-128 154.5,-101 117.5,-101"/><text text-anchor="start" x="127.5" y="-110.8" font-family="Source Code Pro" font-size="14.00" fill="#104e8b">42</text><polygon fill="#104e8b" stroke="#104e8b" points="71.5,-101 71.5,-156 71.5,-156 71.5,-101 71.5,-101"/><polygon fill="#104e8b" stroke="#104e8b" points="117.5,-128 117.5,-156 117.5,-156 117.5,-128 117.5,-128"/><polygon fill="#104e8b" stroke="#104e8b" points="71.5,-128 71.5,-128 117.5,-128 117.5,-128 71.5,-128"/><polygon fill="#104e8b" stroke="#104e8b" points="117.5,-128 117.5,-128 155.5,-128 155.5,-128 117.5,-128"/><polygon fill="#104e8b" stroke="#104e8b" points="117.5,-100 117.5,-128 117.5,-128 117.5,-100 117.5,-100"/><path fill="none" stroke="#104e8b" d="M20,-100C20,-100 143,-100 143,-100 149,-100 155,-106 155,-112 155,-112 155,-144 155,-144 155,-150 149,-156 143,-156 143,-156 20,-156 20,-156 14,-156 8,-150 8,-144 8,-144 8,-112 8,-112 8,-106 14,-100 20,-100"/></g><!-- &#45;java.lang.String78928860 --><g id="-java.lang.String78928860" class="node"><title>-java.lang.String78928860</title><path fill="transparent" stroke="transparent" d="M76.5,-4C76.5,-4 112.5,-4 112.5,-4 117.5,-4 122.5,-9 122.5,-14 122.5,-14 122.5,-24 122.5,-24 122.5,-29 117.5,-34 112.5,-34 112.5,-34 76.5,-34 76.5,-34 71.5,-34 66.5,-29 66.5,-24 66.5,-24 66.5,-14 66.5,-14 66.5,-9 71.5,-4 76.5,-4"/><text text-anchor="start" x="73.5" y="-15.3" font-family="Source Code Pro" font-size="14.00" fill="#104e8b">&quot;Bob&quot;</text><path fill="none" stroke="#104e8b" d="M76.5,-4C76.5,-4 112.5,-4 112.5,-4 117.5,-4 122.5,-9 122.5,-14 122.5,-14 122.5,-24 122.5,-24 122.5,-29 117.5,-34 112.5,-34 112.5,-34 76.5,-34 76.5,-34 71.5,-34 66.5,-29 66.5,-24 66.5,-24 66.5,-14 66.5,-14 66.5,-9 71.5,-4 76.5,-4"/></g><!-- &#45;repl.MdocSession$MdocApp$Person1385763504&#45;&gt;&#45;java.lang.String78928860 --><g id="-repl.MdocSession$MdocApp$Person1385763504-0-java.lang.String78928860" class="edge"><title>-repl.MdocSession$MdocApp$Person1385763504:s-&gt;-java.lang.String78928860:n</title><path fill="none" stroke="#104e8b" d="M94.5,-100C94.5,-73.19 94.5,-64.76 94.5,-41.1"/><polygon fill="#104e8b" stroke="#104e8b" points="96.95,-41 94.5,-34 92.05,-41 96.95,-41"/></g></g></svg>

At this point you might be guessing how we can use this as a basis for our animation approach. Every state of a data structure will be a separate frame in the SVG format. However, an animation consisting of these frames alone would be too jumpy. @@ -114,7 +114,7 @@

Functio inside a data structure of type A and provide read-write access to it. We will use the excellent Monocle library to create lenses and other optics along the way:

-
import monocle.macros.GenLens

val x = GenLens[Point](_.x)
// x: monocle.package.Lens[Point, Double] = repl.MdocSession$MdocApp$$anon$1@270b030c
val y = GenLens[Point](_.y)
// y: monocle.package.Lens[Point, Double] = repl.MdocSession$MdocApp$$anon$2@2fe9f8a4

(diagram(OpticFocus(x, point)).toNamespace("x") +
diagram(OpticFocus(y, point)).toNamespace("y")).render("x+y")
+
import monocle.macros.GenLens

val x = GenLens[Point](_.x)
// x: monocle.package.Lens[Point, Double] = repl.MdocSession$MdocApp$$anon$1@6f44eadb
val y = GenLens[Point](_.y)
// y: monocle.package.Lens[Point, Double] = repl.MdocSession$MdocApp$$anon$2@f82e5b8

(diagram(OpticFocus(x, point)).toNamespace("x") +
diagram(OpticFocus(y, point)).toNamespace("y")).render("x+y")

x+y

Lenses provide several methods to manipulate data:

x.get(point)
// res10: Double = 0.0
y.set(20)(point)
// res11: Point = Point(x = 0.0, y = 20.0)
y.modify(_ + 20)(point)
// res12: Point = Point(x = 0.0, y = 30.0)
@@ -122,12 +122,12 @@

Functio and update the point field by field. We do this by piping Interpolation.double through x and y lenses and combining the resulting interpolations:

-
val pointInterpolation = (
x.interpolateWith(Interpolation.double) +
y.interpolateWith(Interpolation.double))
// pointInterpolation: Interpolation[Point] = reftree.geometry.Interpolation$$anonfun$apply$4@53ecdb48

val points = pointInterpolation.sample(Point(0, 0), Point(10, 20), 5).toList
// points: List[Point] = List(
// Point(x = 0.0, y = 0.0),
// Point(x = 2.5, y = 5.0),
// Point(x = 5.0, y = 10.0),
// Point(x = 7.5, y = 15.0),
// Point(x = 10.0, y = 20.0)
// )

diagram(points).render("points")
+
val pointInterpolation = (
x.interpolateWith(Interpolation.double) +
y.interpolateWith(Interpolation.double))
// pointInterpolation: Interpolation[Point] = reftree.geometry.Interpolation$$anonfun$apply$4@2c57a744

val points = pointInterpolation.sample(Point(0, 0), Point(10, 20), 5).toList
// points: List[Point] = List(
// Point(x = 0.0, y = 0.0),
// Point(x = 2.5, y = 5.0),
// Point(x = 5.0, y = 10.0),
// Point(x = 7.5, y = 15.0),
// Point(x = 10.0, y = 20.0)
// )

diagram(points).render("points")

points

Of course, reftree already defines this as Point.interpolation.

Using the same approach, we can build a polyline interpolator (assuming the polylines being interpolated consist of equal number of points):

-
Data.polyline1
// res14: Polyline = Polyline(
// points = List(Point(x = 0.0, y = 10.0), Point(x = 10.0, y = 20.0))
// )
Data.polyline2
// res15: Polyline = Polyline(
// points = List(Point(x = 20.0, y = 30.0), Point(x = 40.0, y = 50.0))
// )

val polylineInterpolation = (GenLens[Polyline](_.points)
.interpolateEachWith(Point.interpolation))
// polylineInterpolation: Interpolation[Polyline] = reftree.geometry.Interpolation$$anonfun$apply$4@762f7035

val polylines = polylineInterpolation.sample(Data.polyline1, Data.polyline2, 3).toList
// polylines: List[Polyline] = List(
// Polyline(points = List(Point(x = 0.0, y = 10.0), Point(x = 10.0, y = 20.0))),
// Polyline(points = List(Point(x = 10.0, y = 20.0), Point(x = 25.0, y = 35.0))),
// Polyline(points = List(Point(x = 20.0, y = 30.0), Point(x = 40.0, y = 50.0)))
// )

diagram(polylines).render("polylines")
+
Data.polyline1
// res14: Polyline = Polyline(
// points = List(Point(x = 0.0, y = 10.0), Point(x = 10.0, y = 20.0))
// )
Data.polyline2
// res15: Polyline = Polyline(
// points = List(Point(x = 20.0, y = 30.0), Point(x = 40.0, y = 50.0))
// )

val polylineInterpolation = (GenLens[Polyline](_.points)
.interpolateEachWith(Point.interpolation))
// polylineInterpolation: Interpolation[Polyline] = reftree.geometry.Interpolation$$anonfun$apply$4@1e08a7e1

val polylines = polylineInterpolation.sample(Data.polyline1, Data.polyline2, 3).toList
// polylines: List[Polyline] = List(
// Polyline(points = List(Point(x = 0.0, y = 10.0), Point(x = 10.0, y = 20.0))),
// Polyline(points = List(Point(x = 10.0, y = 20.0), Point(x = 25.0, y = 35.0))),
// Polyline(points = List(Point(x = 20.0, y = 30.0), Point(x = 40.0, y = 50.0)))
// )

diagram(polylines).render("polylines")

polylines

We are finally ready to implement our first substantial interpolator: one that morphs graph edges. The following approach is inspired by Mike Bostock’s path tween, @@ -150,20 +150,20 @@

Functio
  • An optic that focuses on an element deep inside XML or any other recursive data structure: Optics.collectFirst. It is actually an Optional, not a Lens, since the element might be missing.
  • -
    val edgePathElement = Optics.collectFirst(XmlSvgApi.select("path"))
    // edgePathElement: monocle.package.Optional[xml.Node, xml.Node] = monocle.Optional$$anon$6@9430e9f

    diagram(OpticFocus(edgePathElement, Data.edge1)).render("edgePathElement")
    +
    val edgePathElement = Optics.collectFirst(XmlSvgApi.select("path"))
    // edgePathElement: monocle.package.Optional[xml.Node, xml.Node] = monocle.Optional$$anon$6@56d284c9

    diagram(OpticFocus(edgePathElement, Data.edge1)).render("edgePathElement")

    edgePathElement

    Next, we need to “descend” to the d attribute. Here is where optics really shine: we can compose Optional[A, B] with Optional[B, C] to get an Optional[A, C]:

    -
    val d = XmlSvgApi.attr("d")
    // d: monocle.package.Optional[xml.Node, String] = monocle.POptional$$anon$1@698ffbcb
    val edgePathString = edgePathElement composeOptional d
    // edgePathString: monocle.POptional[xml.Node, xml.Node, String, String] = monocle.POptional$$anon$1@5c8c4b29

    diagram(OpticFocus(edgePathString, Data.edge1)).render("edgePathString")
    +
    val d = XmlSvgApi.attr("d")
    // d: monocle.package.Optional[xml.Node, String] = monocle.POptional$$anon$1@4ee5148e
    val edgePathString = edgePathElement composeOptional d
    // edgePathString: monocle.POptional[xml.Node, xml.Node, String, String] = monocle.POptional$$anon$1@5f99f791

    diagram(OpticFocus(edgePathString, Data.edge1)).render("edgePathString")

    edgePathString

    Next, we will use an isomorphism, another kind of optic, to view the string as a nice case class:

    -
    Path.stringIso
    // res21: monocle.package.Iso[String, Path] = monocle.PIso$$anon$9@512f2655

    val edgePath = edgePathString composeIso Path.stringIso
    // edgePath: monocle.POptional[xml.Node, xml.Node, Path, Path] = monocle.POptional$$anon$1@4009fd19

    diagram(edgePath.getOption(Data.edge1)).render("edgePath")
    +
    Path.stringIso
    // res21: monocle.package.Iso[String, Path] = monocle.PIso$$anon$9@4f5642c

    val edgePath = edgePathString composeIso Path.stringIso
    // edgePath: monocle.POptional[xml.Node, xml.Node, Path, Path] = monocle.POptional$$anon$1@46435cb7

    diagram(edgePath.getOption(Data.edge1)).render("edgePath")

    edgePath

    And finally, another isomorphism takes us from a Path to its sampled representation as a Polyline. (Purists will say that this is not really an isomorphism because it’s not reversible, but with a lot of points you can get pretty close ;))

    -
    Path.polylineIso(points = 4)
    // res23: monocle.package.Iso[Path, Polyline] = monocle.PIso$$anon$9@56ce1770

    def edgePolyline(points: Int) = edgePath composeIso Path.polylineIso(points)

    diagram(edgePolyline(4).getOption(Data.edge1)).render("edgePolyline")
    +
    Path.polylineIso(points = 4)
    // res23: monocle.package.Iso[Path, Polyline] = monocle.PIso$$anon$9@20647195

    def edgePolyline(points: Int) = edgePath composeIso Path.polylineIso(points)

    diagram(edgePolyline(4).getOption(Data.edge1)).render("edgePolyline")

    edgePolyline

    Let’s interpolate!

    def edgeInterpolation(points: Int) = edgePolyline(points).interpolateWith(Polyline.interpolation)

    def edges(points: Int, frames: Int) = (Data.edge1 +:
    edgeInterpolation(points).sample(Data.edge1, Data.edge2, frames, inclusive = false) :+
    Data.edge2)

    AnimatedGifRenderer.renderFrames(
    edges(4, 4).map(Frame(_)),
    Paths.get(ImagePath, "visualize", "edges-4.gif"),
    RenderingOptions(density = 200),
    AnimationOptions(framesPerSecond = 1)
    )

    AnimatedGifRenderer.renderFrames(
    edges(100, 32).map(Frame(_)),
    Paths.get(ImagePath, "visualize", "edges-100.gif"),
    RenderingOptions(density = 200),
    AnimationOptions(framesPerSecond = 8)
    )
    diff --git a/docs/talks/index.html b/docs/talks/index.html index 15e062e..050ae4e 100644 --- a/docs/talks/index.html +++ b/docs/talks/index.html @@ -4,7 +4,7 @@ Talks / Demos | RefTree - + diff --git a/index.html b/index.html index 1038022..d6aa398 100644 --- a/index.html +++ b/index.html @@ -4,7 +4,7 @@ RefTree - +