├── .gitignore ├── REQUIRE ├── static ├── variance.png └── recurrent.png ├── src ├── DataFlow.jl ├── graph │ ├── conversions.jl │ ├── set.jl │ ├── graph.jl │ ├── ilgraph.jl │ └── dlgraph.jl ├── fuzz.jl ├── operations.jl ├── syntax │ ├── dump.jl │ ├── syntax.jl │ ├── read.jl │ └── sugar.jl └── interpreter.jl ├── docs ├── async.jl └── vertices.md ├── .travis.yml ├── LICENSE.md ├── appveyor.yml ├── test └── runtests.jl └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | *.jl.cov 2 | *.jl.*.cov 3 | *.jl.mem 4 | -------------------------------------------------------------------------------- /REQUIRE: -------------------------------------------------------------------------------- 1 | julia 0.4 2 | Lazy 3 | MacroTools 4 | Juno 5 | -------------------------------------------------------------------------------- /static/variance.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dpsanders/DataFlow.jl/master/static/variance.png -------------------------------------------------------------------------------- /static/recurrent.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dpsanders/DataFlow.jl/master/static/recurrent.png -------------------------------------------------------------------------------- /src/DataFlow.jl: -------------------------------------------------------------------------------- 1 | module DataFlow 2 | 3 | using Lazy, MacroTools, Juno 4 | 5 | include("graph/graph.jl") 6 | include("syntax/syntax.jl") 7 | include("operations.jl") 8 | include("interpreter.jl") 9 | include("fuzz.jl") 10 | 11 | end # module 12 | -------------------------------------------------------------------------------- /src/graph/conversions.jl: -------------------------------------------------------------------------------- 1 | import Base: convert 2 | 3 | for (V, W) in [(DVertex, IVertex), (IVertex, DVertex)] 4 | @eval function convert{T}(::Type{$W{T}}, v::$V, cache = ODict()) 5 | haskey(cache, v) && return cache[v] 6 | w = cache[v] = $W{T}(value(v)) 7 | thread!(w, [convert($W{T}, v′, cache) for v′ in inputs(v)]...) 8 | end 9 | @eval convert(::Type{$W}, v::Vertex) = convert($W{eltype(v)}, v) 10 | end 11 | -------------------------------------------------------------------------------- /docs/async.jl: -------------------------------------------------------------------------------- 1 | function foo(username) 2 | @join begin 3 | account = login(username) 4 | last = getlastlogin(account) 5 | dms = getdms(account) 6 | who = getuser(account, dms[end]) 7 | followers = publicinfo(username)[:followers] 8 | end 9 | 10 | # Do something with followers, last, who etc. 11 | end 12 | 13 | followers = @fork publicinfo(username)[:followers] 14 | account = @fork login(username) 15 | last = @fork getlastlogin(fetch(account)) 16 | dms = getdms(fetch(account)) 17 | who = getuser(fetch(account), dms[end]) 18 | fetchall(last, who, followers) 19 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | # Documentation: http://docs.travis-ci.com/user/languages/julia/ 2 | language: julia 3 | os: 4 | - linux 5 | - osx 6 | julia: 7 | # - release 8 | - nightly 9 | notifications: 10 | email: false 11 | # uncomment the following lines to override the default test script 12 | script: 13 | - if [[ -a .git/shallow ]]; then git fetch --unshallow; fi 14 | - julia -e 'Pkg.clone("MacroTools"); Pkg.clone("Lazy"); Pkg.clone(pwd()); Pkg.build("DataFlow"); Pkg.test("DataFlow"; coverage=true)' 15 | after_success: 16 | - julia -e 'cd(Pkg.dir("DataFlow")); Pkg.add("Coverage"); using Coverage; Coveralls.submit(process_folder())' 17 | -------------------------------------------------------------------------------- /src/fuzz.jl: -------------------------------------------------------------------------------- 1 | module Fuzz 2 | 3 | using ..DataFlow 4 | import DataFlow: thread! 5 | 6 | export grow 7 | 8 | grow{T<:Vertex}(::Type{T}, value) = T(value()) 9 | 10 | function grow{T<:Vertex}(::Type{T}, nodes::Integer, edges::Integer = nodes) 11 | vs = [grow(T, ()->i) for i = 1:nodes] 12 | for _ = 1:edges 13 | thread!(rand(vs), rand(vs)) 14 | end 15 | return rand(vs) 16 | end 17 | 18 | # using Atom 19 | # function testcase(test, tries, cap = 10) 20 | # @progress for nodes = 1:cap 21 | # for _ = 1:tries 22 | # g = grow(DVertex, nodes) 23 | # test(g) || return g 24 | # end 25 | # end 26 | # end 27 | 28 | end 29 | -------------------------------------------------------------------------------- /src/operations.jl: -------------------------------------------------------------------------------- 1 | cse(v::IVertex, cache = Dict{typeof(v),typeof(v)}()) = 2 | postwalk(x -> get!(cache, x, x), v) 3 | 4 | cse(v::Vertex, cache = d()) = cse(il(v), cache) 5 | 6 | function cse(vs::Vector) 7 | cache = d() 8 | [cse(v, cache) for v in vs] 9 | end 10 | 11 | function duplicates(v::IVertex) 12 | cache = Dict{typeof(v),Int}() 13 | prewalk(v) do v 14 | cache[v] = get!(cache, v, 0) + 1 15 | v 16 | end 17 | cache = filter((_,n) -> n > 1, cache) 18 | cache = filter((v,_) -> !any(v′ -> v′ ≠ v && contains(v′, v), keys(cache)), cache) 19 | end 20 | 21 | function Base.contains(haystack::IVertex, needle::IVertex) 22 | result = false 23 | prewalk(haystack) do v 24 | result |= v == needle 25 | v 26 | end 27 | return result 28 | end 29 | 30 | Base.contains(v::Vertex, w::Vertex) = contains(il(v), il(w)) 31 | 32 | function common(v::IVertex, w::IVertex, seen = OSet()) 33 | w in seen && return Set{typeof(w)}() 34 | push!(seen, w) 35 | if contains(v, w) 36 | Set{typeof(w)}((w,)) 37 | else 38 | union((common(v, w′, seen) for w′ in inputs(w))...) 39 | end 40 | end 41 | 42 | common(v::Vertex, w::Vertex) = common(il(v), il(w)) 43 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The DataFlow.jl package is licensed under the MIT "Expat" License: 2 | 3 | > Copyright (c) 2016: Mike Innes. 4 | > 5 | > Permission is hereby granted, free of charge, to any person obtaining 6 | > a copy of this software and associated documentation files (the 7 | > "Software"), to deal in the Software without restriction, including 8 | > without limitation the rights to use, copy, modify, merge, publish, 9 | > distribute, sublicense, and/or sell copies of the Software, and to 10 | > permit persons to whom the Software is furnished to do so, subject to 11 | > the following conditions: 12 | > 13 | > The above copyright notice and this permission notice shall be 14 | > included in all copies or substantial portions of the Software. 15 | > 16 | > THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | > EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | > MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 19 | > IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 20 | > CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 21 | > TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 22 | > SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /appveyor.yml: -------------------------------------------------------------------------------- 1 | environment: 2 | matrix: 3 | - JULIAVERSION: "julialang/bin/winnt/x86/0.4/julia-0.4-latest-win32.exe" 4 | - JULIAVERSION: "julialang/bin/winnt/x64/0.4/julia-0.4-latest-win64.exe" 5 | - JULIAVERSION: "julianightlies/bin/winnt/x86/julia-latest-win32.exe" 6 | - JULIAVERSION: "julianightlies/bin/winnt/x64/julia-latest-win64.exe" 7 | 8 | branches: 9 | only: 10 | - master 11 | - /release-.*/ 12 | 13 | notifications: 14 | - provider: Email 15 | on_build_success: false 16 | on_build_failure: false 17 | on_build_status_changed: false 18 | 19 | install: 20 | # Download most recent Julia Windows binary 21 | - ps: (new-object net.webclient).DownloadFile( 22 | $("http://s3.amazonaws.com/"+$env:JULIAVERSION), 23 | "C:\projects\julia-binary.exe") 24 | # Run installer silently, output to C:\projects\julia 25 | - C:\projects\julia-binary.exe /S /D=C:\projects\julia 26 | 27 | build_script: 28 | # Need to convert from shallow to complete for Pkg.clone to work 29 | - IF EXIST .git\shallow (git fetch --unshallow) 30 | - C:\projects\julia\bin\julia -e "versioninfo(); 31 | Pkg.clone(pwd(), \"DataFlow\"); Pkg.build(\"DataFlow\")" 32 | 33 | test_script: 34 | - C:\projects\julia\bin\julia --check-bounds=yes -e "Pkg.test(\"DataFlow\")" 35 | -------------------------------------------------------------------------------- /test/runtests.jl: -------------------------------------------------------------------------------- 1 | using DataFlow, DataFlow.Fuzz 2 | using MacroTools, Lazy, Base.Test 3 | 4 | import DataFlow: graphm, syntax, cse, dvertex, constant, prewalk 5 | 6 | for nodes = 1:10, tries = 1:1_000 7 | 8 | dl = grow(DVertex, nodes) 9 | 10 | @test dl == @> dl syntax(bindconst = true) graphm 11 | 12 | @test copy(dl) == dl 13 | 14 | il = grow(IVertex, nodes) 15 | 16 | @test il == @> il DataFlow.dl() DataFlow.il() 17 | 18 | @test copy(il) == il == prewalk(identity, il) 19 | 20 | end 21 | 22 | @flow function recurrent(xs) 23 | hidden = σ( Wxh*xs + Whh*hidden + bh ) 24 | σ( Wxy*x + Why*hidden + by ) 25 | end 26 | 27 | @test @capture syntax(recurrent.output) begin 28 | h_Symbol = σ( Wxh*xs + Whh*h_Symbol + bh ) 29 | σ( Wxy*x + Why*h_Symbol + by ) 30 | end 31 | 32 | @flow function var(xs) 33 | mean = sum(xs)/length(xs) 34 | meansqr = sumabs2(xs)/length(xs) 35 | meansqr - mean^2 36 | end 37 | 38 | @test @capture syntax(var.output) begin 39 | sumabs2(xs)/length(xs) - (sum(xs) / length(xs)) ^ 2 40 | end 41 | 42 | @test contains(sprint(show, var), 43 | string(:(sumabs2(xs)/length(xs) - (sum(xs) / length(xs)) ^ 2))) 44 | 45 | @test cse(var.output) == convert(IVertex, @flow begin 46 | n = length(xs) 47 | sumabs2(xs)/n - (sum(xs) / n) ^ 2 48 | end) 49 | 50 | let x = :(2+2) 51 | @test @flow(foo($x)) == dvertex(:foo, constant(x)) 52 | end 53 | -------------------------------------------------------------------------------- /src/graph/set.jl: -------------------------------------------------------------------------------- 1 | typealias ASet{T} Base.AbstractSet{T} 2 | 3 | typealias ODict ObjectIdDict 4 | 5 | immutable ObjectIdSet{T} <: ASet{T} 6 | dict::ObjectIdDict 7 | ObjectIdSet() = new(ObjectIdDict()) 8 | end 9 | 10 | Base.eltype{T}(::ObjectIdSet{T}) = T 11 | 12 | ObjectIdSet() = ObjectIdSet{Any}() 13 | 14 | Base.push!{T}(s::ObjectIdSet{T}, x::T) = (s.dict[x] = nothing; s) 15 | Base.delete!{T}(s::ObjectIdSet{T}, x::T) = (delete!(s.dict, x); s) 16 | Base.in(x, s::ObjectIdSet) = haskey(s.dict, x) 17 | 18 | (::Type{ObjectIdSet{T}}){T}(xs) = push!(ObjectIdSet{T}(), xs...) 19 | 20 | ObjectIdSet(xs) = ObjectIdSet{eltype(xs)}(xs) 21 | 22 | Base.collect(s::ObjectIdSet) = collect(keys(s.dict)) 23 | Base.similar(s::ObjectIdSet, T::Type) = ObjectIdSet{T}() 24 | 25 | @forward ObjectIdSet.dict Base.length 26 | 27 | @iter xs::ObjectIdSet -> keys(xs.dict) 28 | 29 | typealias OSet ObjectIdSet 30 | 31 | immutable ObjectArraySet{T} <: ASet{T} 32 | xs::Vector{T} 33 | ObjectArraySet() = new(T[]) 34 | end 35 | 36 | Base.in{T}(x::T, s::ObjectArraySet{T}) = any(y -> x ≡ y, s.xs) 37 | Base.push!(s::ObjectArraySet, x) = (x ∉ s && push!(s.xs, x); s) 38 | 39 | (::Type{ObjectArraySet{T}}){T}(xs) = push!(ObjectArraySet{T}(), xs...) 40 | 41 | ObjectArraySet(xs) = ObjectArraySet{eltype(xs)}(xs) 42 | 43 | Base.collect(xs::ObjectArraySet) = xs.xs 44 | Base.similar(s::ObjectArraySet, T::Type) = ObjectArraySet{T}() 45 | 46 | @forward ObjectArraySet.xs Base.length 47 | 48 | @iter xs::ObjectArraySet -> xs.xs 49 | 50 | typealias OASet{T} ObjectArraySet{T} 51 | -------------------------------------------------------------------------------- /src/syntax/dump.jl: -------------------------------------------------------------------------------- 1 | # Graph → Syntax 2 | 3 | tocall(f, a...) = :($f($(a...))) 4 | 5 | binding(bindings::Associative, v) = 6 | haskey(bindings, v) ? bindings[v] : (bindings[v] = gensym("edge")) 7 | 8 | function syntax(head::DVertex; bindconst = !isfinal(head)) 9 | vs = topo(head) 10 | ex, bs = :(;), ObjectIdDict() 11 | for v in vs 12 | x = tocall(value(v), [binding(bs, n) for n in inputs(v)]...) 13 | if !bindconst && isconstant(v) && nout(v) > 1 14 | bs[v] = value(v).value 15 | elseif nout(v) > 1 || (!isfinal(head) && v ≡ head) 16 | edge = binding(bs, v) 17 | push!(ex.args, :($edge = $x)) 18 | elseif haskey(bs, v) 19 | if MacroTools.inexpr(ex, bs[v]) 20 | ex = MacroTools.replace(ex, bs[v], x) 21 | else 22 | push!(ex.args, :($(bs[v]) = $x)) 23 | end 24 | else 25 | isfinal(v) ? push!(ex.args, x) : (bs[v] = x) 26 | end 27 | end 28 | head ≢ vs[end] && push!(ex.args, binding(bs, head)) 29 | return ex 30 | end 31 | 32 | function constructor(g) 33 | vertex = isa(g, DVertex) ? :dvertex : :vertex 34 | g = mapv(g) do v 35 | if isconstant(v) 36 | prethread!(v, typeof(v)(value(v))) 37 | v.value = :constant 38 | else 39 | prethread!(v, typeof(v)(Constant(value(v)))) 40 | v.value = vertex 41 | end 42 | v 43 | end 44 | ex = syntax(g) 45 | decls, exs = [], [] 46 | for x in block(ex).args 47 | if @capture(x, v_ = $vertex(f_, a__)) 48 | push!(decls, :($v = $vertex($f))) 49 | push!(exs, :(thread!($v, $(a...)))) 50 | else 51 | push!(exs, x) 52 | end 53 | end 54 | return :($(decls...);$(exs...)) 55 | end 56 | -------------------------------------------------------------------------------- /src/graph/graph.jl: -------------------------------------------------------------------------------- 1 | export Vertex, DVertex, IVertex, vertex, dvertex 2 | 3 | import Base: copy, hash, ==, <, << 4 | 5 | abstract Vertex{T} 6 | 7 | Base.eltype{T}(::Vertex{T}) = T 8 | 9 | Base.show{T<:Vertex}(io::IO, ::Type{T}) = 10 | print(io, T.name.name, (T.parameters[1] == Any ? [] : ["{", T.parameters[1], "}"])...) 11 | 12 | include("set.jl") 13 | include("dlgraph.jl") 14 | include("ilgraph.jl") 15 | include("conversions.jl") 16 | 17 | thread!(to::Vertex, from) = thread!(to, convert(typeof(to), from)) 18 | 19 | thread!(v::Vertex, xs...) = foldl(thread!, v, xs) 20 | 21 | (::Type{T}){T<:Vertex}(x, args...) = thread!(T(x), args...) 22 | 23 | Base.getindex(v::Vertex, i::Integer) = inputs(v)[i] 24 | Base.getindex(v::Vertex, is::Integer...) = foldl(getindex, v, is) 25 | 26 | function collectv(v::Vertex, vs = OASet{typeof(v)}()) 27 | v ∈ vs && return collect(vs) 28 | push!(vs, v) 29 | foreach(v′ -> collectv(v′, vs), inputs(v)) 30 | foreach(v′ -> collectv(v′, vs), outputs(v)) 31 | return collect(vs) 32 | end 33 | 34 | function topo_up(v::Vertex, vs, seen) 35 | v ∈ seen && return vs 36 | push!(seen, v) 37 | foreach(v′ -> topo_up(v′, vs, seen), inputs(v)) 38 | push!(vs, v) 39 | end 40 | 41 | function topo(v::Vertex) 42 | seen, vs = OSet{typeof(v)}(), typeof(v)[] 43 | for v in sort!(collectv(v), by = x -> x ≡ v) 44 | topo_up(v, vs, seen) 45 | end 46 | return vs 47 | end 48 | 49 | function isreaching(from::Vertex, to::Vertex, seen = OSet()) 50 | to ∈ seen && return false 51 | push!(seen, to) 52 | any(v -> v ≡ from || isreaching(from, v, seen), inputs(to)) 53 | end 54 | 55 | Base.isless(a::Vertex, b::Vertex) = isreaching(a, b) 56 | 57 | <<(a::Vertex, b::Vertex) = a < b && !(a > b) 58 | 59 | ↺(v::Vertex) = v < v 60 | ↺(a::Vertex, b::Vertex) = a < b && b < a 61 | -------------------------------------------------------------------------------- /src/syntax/syntax.jl: -------------------------------------------------------------------------------- 1 | import Base: @get! 2 | 3 | include("read.jl") 4 | include("dump.jl") 5 | include("sugar.jl") 6 | 7 | # Display 8 | 9 | syntax(v::Vertex) = syntax(dl(v)) 10 | 11 | function Base.show(io::IO, v::Vertex) 12 | print(io, typeof(v)) 13 | print(io, "(") 14 | s = MacroTools.alias_gensyms(syntax(v)) 15 | if length(s.args) == 1 16 | print(io, sprint(print, s.args[1])) 17 | else 18 | foreach(x -> (println(io); print(io, sprint(print, x))), s.args) 19 | end 20 | print(io, ")") 21 | end 22 | 23 | import Juno: Row, Tree 24 | 25 | code(x) = Juno.Model(Dict(:type=>"code",:text=>x)) 26 | 27 | @render Juno.Inline v::Vertex begin 28 | s = MacroTools.alias_gensyms(syntax(v)) 29 | Tree(typeof(v), map(s -> code(string(s)), s.args)) 30 | end 31 | 32 | # Function / expression macros 33 | 34 | export @flow, @iflow, @vtx, @ivtx 35 | 36 | function inputsm(args) 37 | bindings = d() 38 | for arg in args 39 | isa(arg, Symbol) || error("invalid argument $arg") 40 | bindings[arg] = constant(arg) 41 | end 42 | return bindings 43 | end 44 | 45 | type SyntaxGraph 46 | args::Vector{Symbol} 47 | output::DVertex{Any} 48 | end 49 | 50 | function flow_func(ex) 51 | @capture(shortdef(ex), name_(args__) = exs__) 52 | bs = inputsm(args) 53 | output = graphm(bs, exs) 54 | :($(esc(name)) = $(SyntaxGraph(args, output))) 55 | end 56 | 57 | function flowm(ex, f = dl) 58 | isdef(ex) && return flow_func(ex) 59 | g = graphm(block(ex)) 60 | g = mapconst(x -> isexpr(x, :$) ? esc(x.args[1]) : Expr(:quote, x), g) 61 | constructor(f(g)) 62 | end 63 | 64 | macro flow(ex) 65 | flowm(ex) 66 | end 67 | 68 | macro iflow(ex) 69 | flowm(ex, il) 70 | end 71 | 72 | function vtxm(ex, f = dl) 73 | exs = graphm(block(ex)) 74 | @>> exs graphm mapconst(esc) f constructor 75 | end 76 | 77 | macro vtx(ex) 78 | vtxm(ex) 79 | end 80 | 81 | macro ivtx(ex) 82 | vtxm(ex, il) 83 | end 84 | -------------------------------------------------------------------------------- /src/graph/ilgraph.jl: -------------------------------------------------------------------------------- 1 | type IVertex{T} <: Vertex{T} 2 | value::T 3 | inputs::Vector{IVertex{T}} 4 | 5 | IVertex(x) = new(x, []) 6 | end 7 | 8 | IVertex(x) = IVertex{typeof(x)}(x) 9 | 10 | value(v::IVertex) = v.value 11 | inputs(v::IVertex) = v.inputs 12 | outputs(v::IVertex) = () 13 | nout(v::IVertex) = 0 14 | 15 | function thread!(to::IVertex, from::IVertex) 16 | push!(inputs(to), from) 17 | return to 18 | end 19 | 20 | function prethread!(to::IVertex, from::IVertex) 21 | unshift!(inputs(to), from) 22 | return to 23 | end 24 | 25 | il(v::Vertex) = convert(IVertex, v) 26 | 27 | vertex(a...) = IVertex{Any}(a...) 28 | 29 | vertex(x::Vertex) = convert(IVertex{Any}, x) 30 | 31 | function walk!(v::IVertex, pre, post, cache = ODict()) 32 | haskey(cache, v) && return cache[v]::typeof(v) 33 | cache[v] = v′ = pre(v) 34 | map!(v -> walk!(v, pre, post, cache), v′.inputs, v′.inputs) 35 | cache[v] = post(v′) 36 | end 37 | 38 | prewalk!(f, v::IVertex) = walk!(v, f, identity) 39 | postwalk!(f, v::IVertex) = walk!(v, identity, f) 40 | 41 | copy1(v::IVertex) = typeof(v)(v.value, v.inputs...) 42 | 43 | walk(v::IVertex, pre, post) = walk!(v, v -> copy1(pre(v)), post) 44 | 45 | prewalk(f, v::IVertex) = walk(v, f, identity) 46 | postwalk(f, v::IVertex) = walk(v, identity, f) 47 | 48 | copy(v::IVertex) = walk(v, identity, identity) 49 | 50 | Base.map(f, v::IVertex) = prewalk(v -> typeof(v)(f(value(v)), inputs(v)...), v) 51 | 52 | Base.replace(v::IVertex, pat, r) = prewalk(v -> v == pat ? r : v, v) 53 | 54 | prefor(f, v) = prewalk!(v -> (f(v); v), v) 55 | 56 | # TODO: check we don't get equivalent hashes for different graphs 57 | 58 | function hash(v::IVertex, h::UInt = UInt(0), seen = OSet()) 59 | h = hash(value(v), h) 60 | v in seen ? (return h) : push!(seen, v) 61 | for n in inputs(v) 62 | h $= hash(n, h, seen) 63 | end 64 | return h 65 | end 66 | 67 | function iscyclic(v::IVertex) 68 | is = false 69 | prefor(v -> is |= ↺(v), v) 70 | return is 71 | end 72 | -------------------------------------------------------------------------------- /src/syntax/read.jl: -------------------------------------------------------------------------------- 1 | # Syntax → Graph 2 | 3 | type LateVertex{T} 4 | val::DVertex{T} 5 | args::Vector{Any} 6 | end 7 | 8 | function bindings(ex) 9 | bs = [] 10 | for ex in ex.args 11 | @capture(ex, x_ = _) && push!(bs, x) 12 | end 13 | return bs 14 | end 15 | 16 | function normedges(ex) 17 | map!(ex.args, ex.args) do ex 18 | isline(ex) ? ex : 19 | @capture(ex, _ = _) ? ex : 20 | :($(gensym("edge")) = $ex) 21 | end 22 | return ex 23 | end 24 | 25 | normalise(ex) = 26 | @> ex normsplits normclosures normedges normlines desugar 27 | 28 | function latenodes(exs) 29 | bindings = d() 30 | for ex in exs 31 | @capture(ex, b_Symbol = (f_(a__) | f_)) || error("invalid flow binding `$ex`") 32 | bindings[b] = a == nothing ? constant(f) : LateVertex(dvertex(f), a) 33 | end 34 | return bindings 35 | end 36 | 37 | graphm(bindings, node) = constant(node) 38 | graphm(bindings, node::Vertex) = node 39 | graphm(bindings, ex::Symbol) = 40 | haskey(bindings, ex) ? graphm(bindings, bindings[ex]) : constant(ex) 41 | graphm(bindings, node::LateVertex) = node.val 42 | 43 | function graphm(bindings, ex::Expr) 44 | isexpr(ex, :block) && return graphm(bindings, ex.args) 45 | @capture(ex, f_(args__)) || return constant(ex) 46 | dvertex(f, map(ex -> graphm(bindings, ex), args)...) 47 | end 48 | 49 | function fillnodes!(bindings) 50 | for (b, node) in bindings 51 | if isa(node, Vertex) && isconstant(node) && haskey(bindings, value(node).value) 52 | alias = bindings[value(node).value] 53 | isa(alias, LateVertex) && (alias = alias.val) 54 | bindings[b] = alias 55 | end 56 | end 57 | for (b, node) in bindings 58 | isa(node, LateVertex) || continue 59 | for arg in node.args 60 | thread!(node.val, graphm(bindings, arg)) 61 | end 62 | bindings[b] = node.val 63 | end 64 | return bindings 65 | end 66 | 67 | function graphm(bindings, exs::Vector) 68 | exs = normalise(:($(exs...);)).args 69 | @capture(exs[end], result_Symbol = _) 70 | merge!(bindings, latenodes(exs)) 71 | fillnodes!(bindings) 72 | output = graphm(bindings, result) 73 | end 74 | 75 | graphm(x) = graphm(d(), x) 76 | -------------------------------------------------------------------------------- /src/graph/dlgraph.jl: -------------------------------------------------------------------------------- 1 | # Construction 2 | 3 | type DVertex{T} <: Vertex{T} 4 | value::T 5 | inputs::Vector{DVertex{T}} 6 | outputs::OASet{DVertex{T}} 7 | 8 | DVertex(x) = new(x, [], OASet{DVertex{T}}()) 9 | end 10 | 11 | DVertex(x) = DVertex{typeof(x)}(x) 12 | 13 | value(v::DVertex) = v.value 14 | inputs(v::DVertex) = v.inputs 15 | outputs(v::DVertex) = v.outputs 16 | 17 | function thread!(to::DVertex, from::DVertex) 18 | push!(inputs(to), from) 19 | push!(outputs(from), to) 20 | return to 21 | end 22 | 23 | function prethread!(to::DVertex, from::DVertex) 24 | unshift!(inputs(to), from) 25 | push!(outputs(from), to) 26 | return to 27 | end 28 | 29 | dvertex(a...) = DVertex{Any}(a...) 30 | 31 | dvertex(x::Vertex) = convert(DVertex{Any}, x) 32 | 33 | dl(v::Vertex) = convert(DVertex, v) 34 | 35 | nin(v::Vertex) = length(inputs(v)) 36 | 37 | function nout(v::Vertex) 38 | n = 0 39 | for o in outputs(v), i in inputs(o) 40 | i ≡ v && (n += 1) 41 | end 42 | return n 43 | end 44 | 45 | isfinal(v::Vertex) = nout(v) == 0 46 | 47 | function equal(a::Vertex, b::Vertex, seen = OSet()) 48 | (a, b) ∈ seen && return true 49 | (value(a) == value(b) && 50 | length(inputs(a)) == length(inputs(b)) && 51 | length(outputs(a)) == length(outputs(b))) || return false 52 | push!(seen, (a, b)) 53 | for (i, j) in zip(inputs(a), inputs(b)) 54 | equal(i, j, seen) || return false 55 | end 56 | @assert @>> a outputs map(value) allunique 57 | @assert @>> b outputs map(value) allunique 58 | for o in outputs(a) 59 | p = filter(p -> value(p) == value(o), outputs(b)) 60 | isempty(p) && return false 61 | equal(o, first(p), seen) || return false 62 | end 63 | return true 64 | end 65 | 66 | import Base: == 67 | 68 | x::Vertex == y::Vertex = equal(x, y) 69 | 70 | function mapv(f, v::Vertex; cache = ODict()) 71 | haskey(cache, v) && return cache[v] 72 | node = cache[v] = typeof(v)(value(v)) 73 | for out in outputs(v) 74 | push!(node.outputs, mapv(f, out, cache = cache)) 75 | end 76 | for in in inputs(v) 77 | push!(node.inputs, mapv(f, in, cache = cache)) 78 | end 79 | return f(node) 80 | end 81 | 82 | Base.map(f, v::DVertex) = mapv(v -> (v.value = f(v.value); v), v) 83 | 84 | copy(v::DVertex) = mapv(identity, v) 85 | -------------------------------------------------------------------------------- /src/interpreter.jl: -------------------------------------------------------------------------------- 1 | mux(f) = f 2 | mux(m, f) = (xs...) -> m(f, xs...) 3 | mux(ms...) = foldr(mux, ms) 4 | 5 | type Context{T} 6 | interp::T 7 | cache::ObjectIdDict 8 | stack::Vector{Any} 9 | data::Dict{Symbol,Any} 10 | end 11 | 12 | Context(interp; kws...) = Context(interp, ObjectIdDict(), [], Dict{Symbol,Any}(kws)) 13 | 14 | Base.getindex(ctx::Context, k::Symbol) = ctx.data[k] 15 | Base.setindex!(ctx::Context, v, k::Symbol) = ctx.data[k] = v 16 | 17 | function stack(c::Context) 18 | stk = [] 19 | isempty(c.stack) && return stk 20 | frame = nothing 21 | for i = 1:length(c.stack) 22 | isa(c.stack[i], Frame) || continue 23 | i > 1 && isa(c.stack[i-1], Line) && unshift!(stk, (frame, c.stack[i-1])) 24 | frame = c.stack[i].f 25 | end 26 | isa(c.stack[end], Line) && unshift!(stk, (frame, c.stack[end])) 27 | return stk 28 | end 29 | 30 | function interpv(ctx::Context, graph::IVertex) 31 | haskey(ctx.cache, graph) && return ctx.cache[graph] 32 | ctx.cache[graph] = ctx.interp(ctx, value(graph), inputs(graph)...) 33 | end 34 | 35 | interpv(ctx::Context, xs::Tuple) = map(x -> interpv(ctx, x), xs) 36 | 37 | function interpret(ctx::Context, graph::IVertex, args::IVertex...) 38 | graph = spliceinputs(graph, args...) 39 | interpv(ctx, graph) 40 | end 41 | 42 | interpret(ctx::Context, graph::IVertex, args...) = 43 | interpret(ctx, graph, map(constant, args)...) 44 | 45 | # The `ifoo` convention denotes a piece of interpreter middleware 46 | 47 | iconst(f, ctx::Context, x::Constant) = x.value 48 | 49 | function iline(f, ctx::Context, l::Union{Line,Frame}, v) 50 | push!(ctx.stack, l) 51 | val = interpv(ctx, v) 52 | pop!(ctx.stack) 53 | return val 54 | end 55 | 56 | ilinev(f, ctx::Context, l::Union{Line,Frame}, v) = vertex(l, iline(f, ctx, l, v)) 57 | 58 | ilambda(f, ctx::Context, ::Flosure, body, vars...) = 59 | (xs...) -> interpret(ctx, flopen(body), vars..., xs...) 60 | 61 | iargs(cb, ctx::Context, f, xs...) = cb(f, interpv(ctx, xs)...) 62 | 63 | function ituple(f, s::Split, xs) 64 | isa(xs, Vertex) && value(xs) == tuple ? inputs(xs)[s.n] : 65 | isa(xs, Tuple) ? xs[s.n] : 66 | f(s, constant(xs)) 67 | end 68 | 69 | for m in :[iconst, iline, ilinev, ilambda, ituple].args 70 | @eval $m(f, args...) = f(args...) 71 | end 72 | 73 | interpeval = mux(iline, ilambda, iconst, iargs, ituple, (f, xs...) -> f(xs...)) 74 | 75 | interpret(graph::IVertex, args...) = 76 | interpret(Context(interpeval), graph, args...) 77 | 78 | # Error Handling 79 | 80 | import Juno: errmsg, errtrace 81 | 82 | framename(f::Function) = typeof(f).name.mt.name 83 | framename(f::Void) = Symbol("") 84 | framename(x) = symbol(string(typeof(x))) 85 | 86 | totrace(stack) = [StackFrame(framename(f), Symbol(line.file), line.line) 87 | for (f, line) in stack] 88 | 89 | type Exception{T} 90 | err::T 91 | trace::StackTrace 92 | end 93 | 94 | errmsg(e::Exception) = errmsg(e.err) 95 | errtrace(e::Exception, bt) = errtrace(e.err, [e.trace..., bt...]) 96 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # DataFlow.jl 2 | 3 | [![Build Status](https://travis-ci.org/MikeInnes/DataFlow.jl.svg?branch=master)](https://travis-ci.org/MikeInnes/DataFlow.jl) [![Coverage Status](https://coveralls.io/repos/github/MikeInnes/DataFlow.jl/badge.svg?branch=master)](https://coveralls.io/github/MikeInnes/DataFlow.jl?branch=master) 4 | 5 | DataFlow.jl is a bit like [MacroTools](https://github.com/MikeInnes/MacroTools.jl), but instead of working with programs as expression trees, it works with them as dataflow graphs. 6 | 7 | A data flow graph is a bit like an expression tree without variables; functions always refer to their inputs directly. Underneath it's a directed graph linking the output of one function call to the input of another. DataFlow.jl provides functions like `prewalk` and `postwalk` which allow you to do crazy graph-restructuring operations with minimal code, *even on cyclic graphs*. Think algorithms like common subexpression elimination implemented in [one line](https://github.com/MikeInnes/DataFlow.jl/blob/d5899a47ed052190e655afdf1510e021ad95d09d/src/operations.jl#L2) rather than hundreds. 8 | 9 | DataFlow.jl also provides a common syntax for representing dataflow graphs. This can be used by other packages (like [Flux](https://github.com/MikeInnes/Flux.jl)) to provide a common, intuitive way to work with embedded graphical DSLs. This approach could be applied to an extremely wide range of domains, like graphical modelling in statistics and machine learning, parallel and distributed computing or hardware modelling and simulation. 10 | 11 | ## Basic Examples 12 | 13 | Consider a simple function for calculating variance: 14 | 15 | ```julia 16 | @flow function var(xs) 17 | mean = sum(xs)/length(xs) 18 | meansqr = sumabs2(xs)/length(xs) 19 | meansqr - mean^2 20 | end 21 | ``` 22 | 23 | This looks like (and is) perfectly valid Julia code, but the `@flow` annotation out front makes a big difference; instead of being stored internally as an AST, the code is stored as a directed graph like this: 24 | 25 | ![](static/variance.png) 26 | 27 | The variables are stripped out and we directly model how data moves between different operation. Notice that, for one thing, this makes opportunities for parallelism structurally obvious. 28 | 29 | We can run common subexpression elimination on the graph as follows: 30 | 31 | ```julia 32 | julia> DataFlow.cse(var.output) 33 | DataFlow.IVertex{Any} 34 | chamois = length(xs) 35 | sumabs2(xs) / chamois - (sum(xs) / chamois) ^ 2 36 | ``` 37 | 38 | Multiple things have happened to transform our original code. `mean` and `meansqr` did not need to be assigned variables, so they weren't. Conversely, `length(xs)` *is* assigned a variable name because the result is used more than once. Another thing you can try is modifying `var` to contain an unused variable, and noticing that it gets stripped out. This seems like a very complex syntax operation, but `cse` is implemented in only a couple of lines. 39 | 40 | Another unusual feature of DataFlow is that it supports cycles, for example: 41 | 42 | ```julia 43 | @flow function recurrent(x) 44 | hidden = σ( Wxh*x + Whh*hidden ) 45 | y = σ( Why*hidden + Wxy*x ) 46 | end 47 | ``` 48 | 49 | This is not valid Julia, since `hidden` must be defined before it is used. In DataFlow.jl this is simply represented as a graph like the following: 50 | 51 | ![](static/recurrent.png) 52 | 53 | Applications that build on DataFlow.jl can decide what meaning to give to structures like this. For example, an ANN library might unroll the network a given number of steps at a cycle, enabling recurrent neural network architectures to be easily expressed. 54 | -------------------------------------------------------------------------------- /src/syntax/sugar.jl: -------------------------------------------------------------------------------- 1 | import Base: == 2 | 3 | # Basic julia sugar 4 | 5 | function desugar(ex) 6 | MacroTools.prewalk(ex) do ex 7 | @capture(ex, (xs__,)) ? :(tuple($(xs...))) : 8 | @capture(ex, xs_[i__]) ? :(getindex($xs, $(i...))) : 9 | ex 10 | end 11 | end 12 | 13 | # Constants 14 | 15 | immutable Constant{T} 16 | value::T 17 | end 18 | 19 | tocall(c::Constant) = c.value 20 | 21 | isconstant(v::Vertex) = isa(value(v), Constant) 22 | 23 | mapconst(f, g) = map(x -> isa(x, Constant) ? Constant(f(x.value)) : f(x), g) 24 | 25 | a::Constant == b::Constant = a.value == b.value 26 | 27 | Base.hash(c::Constant, h::UInt = UInt(0)) = hash((Constant, c.value), h) 28 | 29 | for (c, v) in [(:constant, :vertex), (:dconstant, :dvertex)] 30 | @eval $c(x) = $v(Constant(x)) 31 | @eval $c(v::Vertex) = $v(v) 32 | end 33 | 34 | type Do end 35 | 36 | tocall(::Do, a...) = :($(a...);) 37 | 38 | # Line Numbers 39 | 40 | immutable Line 41 | file::String 42 | line::Int 43 | end 44 | 45 | const noline = Line("", -1) 46 | 47 | function Line(ex::Expr) 48 | @assert ex.head == :line 49 | Line(string(ex.args[2]), ex.args[1]) 50 | end 51 | 52 | function normlines(ex) 53 | line = noline 54 | ex′ = :(;) 55 | for ex in ex.args 56 | isline(ex) && (line = Line(ex); continue) 57 | line == noline && (push!(ex′.args, ex); continue) 58 | @assert @capture(ex, var_ = val_) 59 | push!(ex′.args, :($var = $line($val))) 60 | end 61 | return ex′ 62 | end 63 | 64 | function applylines(ex) 65 | ex′ = :(;) 66 | for ex in ex.args 67 | @capture(ex, (var_ = val_) | val_) 68 | val = MacroTools.postwalk(val) do ex 69 | @capture(ex, l_Frame(x_)) && return x # Ignore frames for now 70 | @capture(ex, l_Line(x_)) || return ex 71 | push!(ex′.args, Expr(:line, l.line, symbol(l.file))) 72 | @gensym edge 73 | push!(ex′.args, :($edge = $x)) 74 | return edge 75 | end 76 | isexpr(val, Symbol) ? (ex′.args[end].args[1] = val) : 77 | push!(ex′.args, var == nothing ? :($var = $val) : val) 78 | end 79 | return ex′ 80 | end 81 | 82 | immutable Frame{T} 83 | f::T 84 | end 85 | 86 | immutable SkipFrame end 87 | 88 | # Static tuples 89 | 90 | # TODO: just use `getindex` and `tuple` to represent these? 91 | immutable Split 92 | n::Int 93 | end 94 | 95 | # TODO: printing 96 | function normsplits(ex) 97 | MacroTools.prewalk(ex) do ex 98 | @capture(ex, (xs__,) = y_) || return ex 99 | @gensym edge 100 | quote 101 | $edge = $y 102 | $((:($(xs[i]) = $(Split(i))($edge)) for i = 1:length(xs))...) 103 | end 104 | end |> MacroTools.flatten |> block 105 | end 106 | 107 | tocall(::typeof(tuple), args...) = :($(args...),) 108 | 109 | tocall(s::Split, x) = :($x[$(s.n)]) 110 | 111 | group(xs...) = vertex(tuple, xs...) 112 | 113 | function detuple(v::IVertex) 114 | postwalk(v) do v 115 | if isa(value(v), Split) && value(v[1]) == tuple 116 | v[1][value(v).n] 117 | else 118 | v 119 | end 120 | end 121 | end 122 | 123 | # Bindings 124 | 125 | immutable Bind 126 | name::Symbol 127 | end 128 | 129 | # TODO: printing 130 | function insertbinds(ex) 131 | ls = map(ex.args) do l 132 | @capture(l, x_ = y_) || return l 133 | :($x = $(Bind(x))($y)) 134 | end 135 | :($(ls...);) 136 | end 137 | 138 | # Inputs 139 | 140 | immutable Input end 141 | 142 | splitnode(v, n) = vertex(Split(n), v) 143 | 144 | inputnode(n) = splitnode(constant(Input()), n) 145 | 146 | isinput(v::IVertex) = isa(value(v), Split) && value(v[1]) == Constant(Input()) 147 | 148 | function bumpinputs(v::IVertex) 149 | prewalk(v) do v 150 | isinput(v) ? 151 | inputnode(value(v).n + 1) : 152 | v 153 | end 154 | end 155 | 156 | function spliceinput(v::IVertex, input::IVertex) 157 | postwalk(v) do v 158 | value(v) == Constant(Input()) ? input : v 159 | end 160 | end 161 | 162 | spliceinputs(v::IVertex, inputs::Vertex...) = 163 | spliceinput(v, group(inputs...)) 164 | 165 | function graphinputs(v::IVertex) 166 | n = 0 167 | prewalk(v) do v 168 | isinput(v) && (n = max(n, value(v).n)) 169 | v 170 | end 171 | return n 172 | end 173 | 174 | # Closures 175 | 176 | immutable Flosure end 177 | immutable LooseEnd end 178 | 179 | # TODO: scope 180 | function normclosures(ex) 181 | bs = bindings(ex) 182 | MacroTools.prewalk(shortdef(ex)) do ex 183 | @capture(ex, (args__,) -> body_) || return ex 184 | @assert all(arg -> isa(arg, Symbol), args) 185 | closed = filter(x -> inexpr(body, x), bs) 186 | vars = vcat(closed, args) 187 | body = MacroTools.prewalk(body) do ex 188 | ex in vars ? 189 | Expr(:call, Split(findfirst(x->x==ex, vars)), LooseEnd()) : 190 | ex 191 | end 192 | :($(Flosure())($body, $(closed...))) 193 | end |> MacroTools.flatten |> block 194 | end 195 | 196 | flopen(v::IVertex) = mapconst(x->x==LooseEnd()?Input():x,v) 197 | -------------------------------------------------------------------------------- /docs/vertices.md: -------------------------------------------------------------------------------- 1 | DataFlow provides two things, a graph data structure and a common syntax for describing graphs. You're not tied down to using either of these things; you could use the syntax and immediately convert graphs to an adjacency matrix for processing, for example, or you could generate the graphs through other means while taking advantage of DataFlow's library of common graph operations. 2 | 3 | DataFlow explicitly keeps the data structure very simple and doesn't try to attach any kind of meaning to it. The graphs could represent straightforward Julia programs, or Bayesian networks, or an electrical circuit. Libraries using DataFlow will probably want to extend the syntax and manipulate the graph in order to generate appropriate code for the application. 4 | 5 | ## Data Structures 6 | 7 | DataFlow actually comes with two related data structures, the `DVertex` and the `IVertex`. Both represent nodes in a graph with inputs/outputs to/from other nodes in the graph. `IVertex` is input-linked, somewhat like a linked list – it keeps a reference to nodes which serve as input. `DVertex` is doubly-linked, analogous to a doubly-linked list – it refers to its input as well as all the nodes which take it as input. DVertex are technically more expressive but are also much harder to work with, so it's usually best to convert to input-linked as soon as possible (via `DataFlow.il()` for example). 8 | 9 | ```julia 10 | type IVertex{T} <: Vertex{T} 11 | value::T 12 | inputs::Vector{IVertex{T}} 13 | # outputs::Set{IVertex{T}} # DVertex has this in addition 14 | end 15 | ``` 16 | 17 | `IVertex` can be seen as very similar to an `Expr` object in Julia. For example, the expression `x+length(xs)` will be stored in a very similar way: 18 | 19 | ```julia 20 | Expr(:call, :+, x, Expr(:call, :length, :xs)) 21 | IVertex(:+, IVertex(:x), IVertex(:length, IVertex(:xs))) 22 | ``` 23 | 24 | The key difference is that *object identity* is important in DataFlow graphs. Say we build an expression tree like this: 25 | 26 | ```julia 27 | foo = Expr(:call, :length, :xs) 28 | Expr(:call, :+, foo, foo) 29 | ``` 30 | 31 | This prints as `length(xs)+length(xs)` regardless of the fact that we reused the `length(xs)` expression object. In DataFlow the reuse makes a big difference: 32 | 33 | ```julia 34 | g = IVertex{Any} 35 | 36 | g(:+, g(:length, g(:xs)), g(:length, g(:xs))) 37 | > IVertex(length(xs) + length(xs)) 38 | 39 | foo = g(:length, g(:xs)) 40 | g(:+, foo, foo) 41 | > IVertex( 42 | gazelle = length(xs) 43 | gazelle + gazelle) 44 | ``` 45 | 46 | The reuse is now encoded in the program graph. Note that the data structure above has no conception of a "variable" since the flow of data is directly represented; instead, variables will be generated for us if and when they are needed in the syntax conversion. 47 | 48 | ## Algorithms 49 | 50 | The basic approach to working with DataFlow graphs is to use the same techniques as are used for trees in functional programming. That is, you can write algorithms which generate a new graph by recursively walking over the old one. This is packaged up in functions like `prewalk` and `postwalk` which allow you apply a function to each node in the graph. 51 | 52 | For example: 53 | 54 | ```julia 55 | foo = g(:+, g(:length, g(:xs)), g(:length, g(:ys))) 56 | > IVertex{Any}(length(xs)+length(ys)) 57 | 58 | postwalk(foo) do v 59 | value(v) == :length && value(v[1]) == :xs ? g(:lx) : v 60 | end 61 | > IVertex(lx + length(ys)) 62 | ``` 63 | 64 | (The difference between `pre`- and `postwalk` is the order of traversal, which you can see using `@show`.) In this way you can do things like find and replace on graphs, as well as more complex structural transformations. At this point we also have everything we need to implement common subexpression elimination: 65 | 66 | ```julia 67 | cse(v::IVertex, cache = Dict()) = 68 | postwalk(x -> get!(cache, x, x), v) 69 | ``` 70 | 71 | We replace each node in the graph by retrieving it from a dict where values refer to themselves. This ensures that any values that are `==` will also be `===` in the resulting graph, so that common expressions are reused. 72 | 73 | ```julia 74 | foo = @flow length(xs)+length(xs) 75 | > DVertex(length(xs) + length(xs)) 76 | 77 | cse(foo) 78 | > IVertex( 79 | ram = length(xs) 80 | ram + ram) 81 | ``` 82 | 83 | Generally you should be able to stick to using DataFlow's high-level operations like `postwalk`, but in some cases you may need to write a recursive algorithm from scratch. This looks exactly like writing the same algorithm over a tree, with the caveats that (1) identical nodes may be reached by more than one route down the tree and (2) there may be cycles in the graph which cause infinite loops for naive recursion. This sounds like a nightmare but in fact we can kill these two tricky birds with a single stone; we simply memoize the function so that visiting repeated nodes ends the recursion. Make sure to cache the result of the current call *before* recursing. 84 | 85 | ```julia 86 | function replace_xs(g, cache = ObjectIdDict()) 87 | # Early exit if we've already processed this node 88 | haskey(cache, g) && return cache[g] 89 | # Create the new (empty) node and cache it 90 | cache[g] = g′ = typeof(g)(value(g) == :xs ? :foo : value(g)) 91 | # For each input of the original node, process it and push 92 | # the result into the new node 93 | thread!(g′, (replace_xs(v, cache) for v in inputs(g))...) 94 | end 95 | 96 | foo = DataFlow.cse(@flow length(xs)+length(xs)) 97 | > IVertex( 98 | ant = length(xs) 99 | ant + ant) 100 | 101 | replace_xs(foo) 102 | > IVertex( 103 | manatee = length(foo) 104 | manatee + manatee) 105 | ``` 106 | 107 | In this case forgetting the cache would result in a fairly un-disastrous `length(foo)+length(foo)`, but in other cases it could result in a hang. 108 | --------------------------------------------------------------------------------