From e74a2770f3883bfc16b9942a95d79192054ee066 Mon Sep 17 00:00:00 2001 From: Michael Abbott <32575566+mcabbott@users.noreply.github.com> Date: Sat, 5 Feb 2022 23:46:04 -0500 Subject: [PATCH 01/13] align fmap(f, x, y) with fmap(f, x), add prone keyword --- src/functor.jl | 23 ++++++----------------- 1 file changed, 6 insertions(+), 17 deletions(-) diff --git a/src/functor.jl b/src/functor.jl index 608dbd3..4be69fd 100644 --- a/src/functor.jl +++ b/src/functor.jl @@ -74,27 +74,16 @@ end ### Vararg forms ### -function fmap(f, x, dx...; cache = IdDict()) - haskey(cache, x) && return cache[x] - cache[x] = isleaf(x) ? f(x, dx...) : _default_walk((x...) -> fmap(f, x..., cache = cache), x, dx...) -end - -function functor_tuple(f, x::Tuple, dx::Tuple) - map(x, dx) do x, x̄ - _default_walk(f, x, x̄) - end +function fmap(f, x, ys...; exclude = isleaf, walk = _default_biwalk, cache = IdDict(), prune = false) + haskey(cache, x) && return prune === false ? cache[x] : prune + cache[x] = exclude(x) ? f(x, ys...) : walk((xy...,) -> fmap(f, xy...; exclude, walk, cache, prune), x, ys...) end -functor_tuple(f, x, dx) = f(x, dx) -functor_tuple(f, x, ::Nothing) = x -function _default_walk(f, x, dx) +function _default_biwalk(f, x, y) func, re = functor(x) - map(func, dx) do x, x̄ - # functor_tuple(f, x, x̄) - f(x, x̄) - end |> re + yfunc, _ = functor(typeof(x), y) + map((x, y) -> f(x, y), func, yfunc) |> re end -_default_walk(f, ::Nothing, ::Nothing) = nothing ### ### FlexibleFunctors.jl From 133032e053049ce2bb8356353efcb52405ed23f3 Mon Sep 17 00:00:00 2001 From: Michael Abbott <32575566+mcabbott@users.noreply.github.com> Date: Sun, 6 Feb 2022 00:20:39 -0500 Subject: [PATCH 02/13] prune keyword for fmap(f, x) too --- src/functor.jl | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/src/functor.jl b/src/functor.jl index 4be69fd..05999eb 100644 --- a/src/functor.jl +++ b/src/functor.jl @@ -43,12 +43,9 @@ function _default_walk(f, x) re(map(f, func)) end -function fmap(f, x; exclude = isleaf, walk = _default_walk, cache = IdDict()) - haskey(cache, x) && return cache[x] - y = exclude(x) ? f(x) : walk(x -> fmap(f, x, exclude = exclude, walk = walk, cache = cache), x) - cache[x] = y - - return y +function fmap(f, x; exclude = isleaf, walk = _default_walk, cache = IdDict(), prune = Base._InitialValue()) + haskey(cache, x) && return prune === Base._InitialValue() ? cache[x] : prune + cache[x] = exclude(x) ? f(x) : walk(x -> fmap(f, x; exclude, walk, cache, prune), x) end ### @@ -74,8 +71,8 @@ end ### Vararg forms ### -function fmap(f, x, ys...; exclude = isleaf, walk = _default_biwalk, cache = IdDict(), prune = false) - haskey(cache, x) && return prune === false ? cache[x] : prune +function fmap(f, x, ys...; exclude = isleaf, walk = _default_biwalk, cache = IdDict(), prune = Base._InitialValue()) + haskey(cache, x) && return prune === Base._InitialValue() ? cache[x] : prune cache[x] = exclude(x) ? f(x, ys...) : walk((xy...,) -> fmap(f, xy...; exclude, walk, cache, prune), x, ys...) end From 7bbfa31be1461448c2ca3be8e069c6891fd4436a Mon Sep 17 00:00:00 2001 From: Michael Abbott <32575566+mcabbott@users.noreply.github.com> Date: Mon, 7 Feb 2022 13:11:45 -0500 Subject: [PATCH 03/13] use INIT to tidy a little --- src/functor.jl | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/src/functor.jl b/src/functor.jl index 05999eb..1ab8287 100644 --- a/src/functor.jl +++ b/src/functor.jl @@ -43,8 +43,10 @@ function _default_walk(f, x) re(map(f, func)) end -function fmap(f, x; exclude = isleaf, walk = _default_walk, cache = IdDict(), prune = Base._InitialValue()) - haskey(cache, x) && return prune === Base._InitialValue() ? cache[x] : prune +const INIT = Base._InitialValue() # sentinel value for keyword not supplied + +function fmap(f, x; exclude = isleaf, walk = _default_walk, cache = IdDict(), prune = INIT) + haskey(cache, x) && return prune === INIT ? cache[x] : prune cache[x] = exclude(x) ? f(x) : walk(x -> fmap(f, x; exclude, walk, cache, prune), x) end @@ -71,15 +73,15 @@ end ### Vararg forms ### -function fmap(f, x, ys...; exclude = isleaf, walk = _default_biwalk, cache = IdDict(), prune = Base._InitialValue()) - haskey(cache, x) && return prune === Base._InitialValue() ? cache[x] : prune +function fmap(f, x, ys...; exclude = isleaf, walk = _default_walk, cache = IdDict(), prune = INIT) + haskey(cache, x) && return prune === INIT ? cache[x] : prune cache[x] = exclude(x) ? f(x, ys...) : walk((xy...,) -> fmap(f, xy...; exclude, walk, cache, prune), x, ys...) end -function _default_biwalk(f, x, y) +function _default_walk(f, x, ys...) func, re = functor(x) - yfunc, _ = functor(typeof(x), y) - map((x, y) -> f(x, y), func, yfunc) |> re + yfuncs = map(y -> functor(typeof(x), y)[1], ys) + re(map(f, func, yfuncs...)) end ### @@ -98,9 +100,7 @@ function makeflexiblefunctor(m::Module, T, pfield) func = NamedTuple{pfields}(map(p -> getproperty(x, p), pfields)) return func, re end - end - end function flexiblefunctorm(T, pfield = :params) From 4adbcf1614af31478bb56665c1fd8f66353e0b35 Mon Sep 17 00:00:00 2001 From: Michael Abbott <32575566+mcabbott@users.noreply.github.com> Date: Mon, 7 Feb 2022 13:12:24 -0500 Subject: [PATCH 04/13] organise the tests to match code, no functional change --- test/basics.jl | 91 +++++++++++++++++++++++++++++++++--------------- test/runtests.jl | 1 - test/update.jl | 23 ------------ 3 files changed, 62 insertions(+), 53 deletions(-) delete mode 100644 test/update.jl diff --git a/test/basics.jl b/test/basics.jl index 21aa445..4a4d231 100644 --- a/test/basics.jl +++ b/test/basics.jl @@ -1,25 +1,14 @@ -struct Foo - x - y -end + +struct Foo; x; y; end @functor Foo -struct Bar - x -end +struct Bar; x; end @functor Bar -struct Baz - x - y - z -end -@functor Baz (y,) +struct OneChild3; x; y; z; end +@functor OneChild3 (y,) -struct NoChildren - x - y -end +struct NoChildren2; x; y; end @static if VERSION >= v"1.6" @testset "ComposedFunction" begin @@ -31,6 +20,10 @@ end end end +### +### Basic functionality +### + @testset "Nested" begin model = Bar(Foo(1, [1, 2, 3])) @@ -53,6 +46,17 @@ end @test fmap(f, x; exclude = x -> x isa AbstractArray) == x end +@testset "Property list" begin + model = OneChild3(1, 2, 3) + model′ = fmap(x -> 2x, model) + + @test (model′.x, model′.y, model′.z) == (1, 4, 3) +end + +### +### Extras +### + @testset "Walk" begin model = Foo((0, Bar([1, 2, 3])), [4, 5]) @@ -60,13 +64,6 @@ end @test model′ == (; x=(0, (; x=[1, 2, 3])), y=[4, 5]) end -@testset "Property list" begin - model = Baz(1, 2, 3) - model′ = fmap(x -> 2x, model) - - @test (model′.x, model′.y, model′.z) == (1, 4, 3) -end - @testset "fcollect" begin m1 = [1, 2, 3] m2 = 1 @@ -78,7 +75,7 @@ end m1 = [1, 2, 3] m2 = Bar(m1) - m0 = NoChildren(:a, :b) + m0 = NoChildren2(:a, :b) m3 = Foo(m2, m0) m4 = Bar(m3) @test all(fcollect(m4) .=== [m4, m3, m2, m1, m0]) @@ -89,6 +86,42 @@ end @test all(fcollect(m3) .=== [m3, m1, m2]) end +### +### Vararg forms +### + +@testset "fmap(f, x, y)" begin + @test true # TODO +end + +@testset "old test update.jl" begin + struct M{F,T,S} + σ::F + W::T + b::S + end + + @functor M + + (m::M)(x) = m.σ.(m.W * x .+ m.b) + + m = M(identity, ones(Float32, 3, 4), zeros(Float32, 3)) + x = ones(Float32, 4, 2) + m̄, _ = gradient((m,x) -> sum(m(x)), m, x) + m̂ = Functors.fmap(m, m̄) do x, y + isnothing(x) && return y + isnothing(y) && return x + x .- 0.1f0 .* y + end + + @test m̂.W ≈ fill(0.8f0, size(m.W)) + @test m̂.b ≈ fill(-0.2f0, size(m.b)) +end + +### +### FlexibleFunctors.jl +### + struct FFoo x y @@ -102,13 +135,13 @@ struct FBar end @flexiblefunctor FBar p -struct FBaz +struct FOneChild4 x y z p end -@flexiblefunctor FBaz p +@flexiblefunctor FOneChild4 p @testset "Flexible Nested" begin model = FBar(FFoo(1, [1, 2, 3], (:y, )), (:x,)) @@ -132,7 +165,7 @@ end end @testset "Flexible Property list" begin - model = FBaz(1, 2, 3, (:x, :z)) + model = FOneChild4(1, 2, 3, (:x, :z)) model′ = fmap(x -> 2x, model) @test (model′.x, model′.y, model′.z) == (2, 2, 6) @@ -147,7 +180,7 @@ end @test all(fcollect(m4, exclude = x -> x isa Array) .=== [m4, m3]) @test all(fcollect(m4, exclude = x -> x isa FFoo) .=== [m4]) - m0 = NoChildren(:a, :b) + m0 = NoChildren2(:a, :b) m1 = [1, 2, 3] m2 = FBar(m1, ()) m3 = FFoo(m2, m0, (:x, :y,)) diff --git a/test/runtests.jl b/test/runtests.jl index 394cfbf..5b853fc 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -5,7 +5,6 @@ using Zygote include("basics.jl") include("base.jl") - include("update.jl") if VERSION < v"1.6" # || VERSION > v"1.7-" @warn "skipping doctests, on Julia $VERSION" diff --git a/test/update.jl b/test/update.jl deleted file mode 100644 index 0ed6bca..0000000 --- a/test/update.jl +++ /dev/null @@ -1,23 +0,0 @@ -@testset "Generalized fmap over equivalent functors" begin - struct M{F,T,S} - σ::F - W::T - b::S - end - - @functor M - - (m::M)(x) = m.σ.(m.W * x .+ m.b) - - m = M(identity, ones(Float32, 3, 4), zeros(Float32, 3)) - x = ones(Float32, 4, 2) - m̄, _ = gradient((m,x) -> sum(m(x)), m, x) - m̂ = Functors.fmap(m, m̄) do x, y - isnothing(x) && return y - isnothing(y) && return x - x .- 0.1f0 .* y - end - - @test m̂.W ≈ fill(0.8f0, size(m.W)) - @test m̂.b ≈ fill(-0.2f0, size(m.b)) -end From 7ab2efdbc0e2b030397b6cd5e8a0ec6071fbcef1 Mon Sep 17 00:00:00 2001 From: Michael Abbott <32575566+mcabbott@users.noreply.github.com> Date: Mon, 7 Feb 2022 14:05:23 -0500 Subject: [PATCH 05/13] add some tests --- src/functor.jl | 5 +++++ test/basics.jl | 42 +++++++++++++++++++++++++++++++++++++++++- 2 files changed, 46 insertions(+), 1 deletion(-) diff --git a/src/functor.jl b/src/functor.jl index 1ab8287..21af8c4 100644 --- a/src/functor.jl +++ b/src/functor.jl @@ -81,6 +81,11 @@ end function _default_walk(f, x, ys...) func, re = functor(x) yfuncs = map(y -> functor(typeof(x), y)[1], ys) + for yf in yfuncs + nx = propertynames(func) + ny = propertynames(yf) + nx == ny || throw(ArgumentError("names of children must agree, got $nx != $ny")) + end re(map(f, func, yfuncs...)) end diff --git a/test/basics.jl b/test/basics.jl index 4a4d231..8ed0a0e 100644 --- a/test/basics.jl +++ b/test/basics.jl @@ -53,6 +53,24 @@ end @test (model′.x, model′.y, model′.z) == (1, 4, 3) end +@testset "cache" begin + shared = [1,2,3] + m1 = Foo(shared, Foo([1,2,3], Foo(shared, [1,2,3]))) + m1f = fmap(float, m1) + @test m1f.x === m1f.y.y.x + @test m1f.x !== m1f.y.x + m1p = fmapstructure(identity, m1; prune = nothing) + @test m1p == (x = [1, 2, 3], y = (x = [1, 2, 3], y = (x = nothing, y = [1, 2, 3]))) + + # A non-leaf node can also be repeated: + m2 = Foo(Foo(shared, 4), Foo(shared, 4)) + @test m2.x === m2.y + m2f = fmap(float, m2) + @test m2f.x.x === m2f.y.x + m2p = fmapstructure(identity, m2; prune = Bar(0)) + @test m2p == (x = (x = [1, 2, 3], y = 4), y = Bar(0)) +end + ### ### Extras ### @@ -91,7 +109,29 @@ end ### @testset "fmap(f, x, y)" begin - @test true # TODO + m1 = (x = [1,2], y = 3) + n1 = (x = [4,5], y = 6) + @test fmap(+, m1, n1) == (x = [5, 7], y = 9) + + # Reconstruction type comes from the first argument + foo1 = Foo([7,8], 9) + @test_broken fmap(+, m1, foo1) == (x = [8, 10], y = 12) # https://github.com/FluxML/Functors.jl/issues/38 + @test fmap(+, foo1, n1) isa Foo + @test fmap(+, foo1, n1).x == [11, 13] + + # Mismatched trees should be an error + m2 = (x = [1,2], y = (a = [3,4], b = 5)) + n2 = (x = [6,7], y = 8) + @test_throws ArgumentError fmap(first∘tuple, m2, n2) + @test_broken @test_throws ArgumentError fmap(first∘tuple, m2, n2) # now (x = [6, 7], y = 8) + + # The cache uses IDs from the first argument + shared = [1,2,3] + m3 = (x = shared, y = [4,5,6], z = shared) + n3 = (x = shared, y = shared, z = [7,8,9]) + @test fmap(+, m3, n3) == (x = [2, 4, 6], y = [5, 7, 9], z = [2, 4, 6]) + z3 = fmap(+, m3, n3) + @test z3.x === z3.z end @testset "old test update.jl" begin From ff39b09d0d6719a1d2924854785f7ca9b812aa29 Mon Sep 17 00:00:00 2001 From: Michael Abbott <32575566+mcabbott@users.noreply.github.com> Date: Mon, 7 Feb 2022 14:41:48 -0500 Subject: [PATCH 06/13] partial fix for #38 --- src/functor.jl | 2 +- test/basics.jl | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/functor.jl b/src/functor.jl index 21af8c4..ed9ca9d 100644 --- a/src/functor.jl +++ b/src/functor.jl @@ -3,7 +3,7 @@ functor(T, x) = (), _ -> x functor(x) = functor(typeof(x), x) functor(::Type{<:Tuple}, x) = x, y -> y -functor(::Type{<:NamedTuple}, x) = x, y -> y +functor(::Type{<:NamedTuple{L}}, x) where L = NamedTuple{L}(map(s -> getproperty(x, s), L)), identity functor(::Type{<:AbstractArray}, x) = x, y -> y functor(::Type{<:AbstractArray{<:Number}}, x) = (), _ -> x diff --git a/test/basics.jl b/test/basics.jl index 8ed0a0e..a45d6c8 100644 --- a/test/basics.jl +++ b/test/basics.jl @@ -115,15 +115,15 @@ end # Reconstruction type comes from the first argument foo1 = Foo([7,8], 9) - @test_broken fmap(+, m1, foo1) == (x = [8, 10], y = 12) # https://github.com/FluxML/Functors.jl/issues/38 + @test fmap(+, m1, foo1) == (x = [8, 10], y = 12) @test fmap(+, foo1, n1) isa Foo @test fmap(+, foo1, n1).x == [11, 13] # Mismatched trees should be an error m2 = (x = [1,2], y = (a = [3,4], b = 5)) n2 = (x = [6,7], y = 8) - @test_throws ArgumentError fmap(first∘tuple, m2, n2) - @test_broken @test_throws ArgumentError fmap(first∘tuple, m2, n2) # now (x = [6, 7], y = 8) + @test_throws Exception fmap(first∘tuple, m2, n2) + @test_throws Exception fmap(first∘tuple, m2, n2) # The cache uses IDs from the first argument shared = [1,2,3] From 2e71bb82adfba912242b9c52a538a0bc1f9431c0 Mon Sep 17 00:00:00 2001 From: Michael Abbott <32575566+mcabbott@users.noreply.github.com> Date: Mon, 7 Feb 2022 15:12:19 -0500 Subject: [PATCH 07/13] add an isbits test --- test/basics.jl | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/test/basics.jl b/test/basics.jl index a45d6c8..0047742 100644 --- a/test/basics.jl +++ b/test/basics.jl @@ -69,6 +69,12 @@ end @test m2f.x.x === m2f.y.x m2p = fmapstructure(identity, m2; prune = Bar(0)) @test m2p == (x = (x = [1, 2, 3], y = 4), y = Bar(0)) + + # Repeated isbits types should not automatically be regarded as shared: + m3 = Foo(Foo(shared, 1:3), Foo(1:3, shared)) + m3p = fmapstructure(identity, m3; prune = 0) + @test m3p.y.y == 0 + @test_broken m3p.y.x == 1:3 end ### From 2522f67c08f288d4622ee8b8144b2162b2bde188 Mon Sep 17 00:00:00 2001 From: Michael Abbott <32575566+mcabbott@users.noreply.github.com> Date: Mon, 7 Feb 2022 15:12:29 -0500 Subject: [PATCH 08/13] remove friendly error --- src/functor.jl | 5 ----- 1 file changed, 5 deletions(-) diff --git a/src/functor.jl b/src/functor.jl index ed9ca9d..f20b1aa 100644 --- a/src/functor.jl +++ b/src/functor.jl @@ -81,11 +81,6 @@ end function _default_walk(f, x, ys...) func, re = functor(x) yfuncs = map(y -> functor(typeof(x), y)[1], ys) - for yf in yfuncs - nx = propertynames(func) - ny = propertynames(yf) - nx == ny || throw(ArgumentError("names of children must agree, got $nx != $ny")) - end re(map(f, func, yfuncs...)) end From 6e0b7bc4a93b393bb55c876b265ed5f626978135 Mon Sep 17 00:00:00 2001 From: Michael Abbott <32575566+mcabbott@users.noreply.github.com> Date: Mon, 7 Feb 2022 16:10:49 -0500 Subject: [PATCH 09/13] explicit keyword not INIT --- src/functor.jl | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/functor.jl b/src/functor.jl index f20b1aa..49f2008 100644 --- a/src/functor.jl +++ b/src/functor.jl @@ -43,10 +43,10 @@ function _default_walk(f, x) re(map(f, func)) end -const INIT = Base._InitialValue() # sentinel value for keyword not supplied +struct NoKeyword end -function fmap(f, x; exclude = isleaf, walk = _default_walk, cache = IdDict(), prune = INIT) - haskey(cache, x) && return prune === INIT ? cache[x] : prune +function fmap(f, x; exclude = isleaf, walk = _default_walk, cache = IdDict(), prune = NoKeyword()) + haskey(cache, x) && return prune isa NoKeyword ? cache[x] : prune cache[x] = exclude(x) ? f(x) : walk(x -> fmap(f, x; exclude, walk, cache, prune), x) end @@ -73,8 +73,8 @@ end ### Vararg forms ### -function fmap(f, x, ys...; exclude = isleaf, walk = _default_walk, cache = IdDict(), prune = INIT) - haskey(cache, x) && return prune === INIT ? cache[x] : prune +function fmap(f, x, ys...; exclude = isleaf, walk = _default_walk, cache = IdDict(), prune = NoKeyword()) + haskey(cache, x) && return prune isa NoKeyword ? cache[x] : prune cache[x] = exclude(x) ? f(x, ys...) : walk((xy...,) -> fmap(f, xy...; exclude, walk, cache, prune), x, ys...) end From a057d088db7b57b57c560eb4c358983afcbc72df Mon Sep 17 00:00:00 2001 From: Michael Abbott <32575566+mcabbott@users.noreply.github.com> Date: Mon, 7 Feb 2022 17:51:18 -0500 Subject: [PATCH 10/13] more tests --- test/basics.jl | 47 ++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 46 insertions(+), 1 deletion(-) diff --git a/test/basics.jl b/test/basics.jl index 0047742..89eadd6 100644 --- a/test/basics.jl +++ b/test/basics.jl @@ -1,4 +1,6 @@ +using Functors: functor + struct Foo; x; y; end @functor Foo @@ -77,6 +79,38 @@ end @test_broken m3p.y.x == 1:3 end +@testset "functor(typeof(x), y) from @functor" begin + nt1, re1 = functor(Foo, (x=1, y=2, z=3)) + @test nt1 == (x = 1, y = 2) + @test re1((x = 10, y = 20)) == Foo(10, 20) + re1((y = 22, x = 11)) # gives Foo(22, 11), is that a bug? + + nt2, re2 = functor(Foo, (z=33, x=1, y=2)) + @test nt2 == (x = 1, y = 2) + @test re2((x = 10, y = 20)) == Foo(10, 20) + + @test_throws Exception functor(Foo, (z=33, x=1)) # type NamedTuple has no field y + + nt3, re3 = functor(OneChild3, (x=1, y=2, z=3)) + @test nt3 == (y = 2,) + @test re3((y = 20,)) == OneChild3(1, 20, 3) + re3(22) # gives OneChild3(1, 22, 3), is that a bug? +end + +@testset "functor(typeof(x), y) for Base types" begin + nt11, re11 = functor(NamedTuple{(:x, :y)}, (x=1, y=2, z=3)) + @test nt11 == (x = 1, y = 2) + @test re11((x = 10, y = 20)) == (x = 10, y = 20) + re11((y = 22, x = 11)) + re11((11, 22)) # passes right through + + nt12, re12 = functor(NamedTuple{(:x, :y)}, (z=33, x=1, y=2)) + @test nt12 == (x = 1, y = 2) + @test re12((x = 10, y = 20)) == (x = 10, y = 20) + + @test_throws Exception functor(NamedTuple{(:x, :y)}, (z=33, x=1)) +end + ### ### Extras ### @@ -128,7 +162,7 @@ end # Mismatched trees should be an error m2 = (x = [1,2], y = (a = [3,4], b = 5)) n2 = (x = [6,7], y = 8) - @test_throws Exception fmap(first∘tuple, m2, n2) + @test_throws Exception fmap(first∘tuple, m2, n2) # ERROR: type Int64 has no field a @test_throws Exception fmap(first∘tuple, m2, n2) # The cache uses IDs from the first argument @@ -138,6 +172,17 @@ end @test fmap(+, m3, n3) == (x = [2, 4, 6], y = [5, 7, 9], z = [2, 4, 6]) z3 = fmap(+, m3, n3) @test z3.x === z3.z + + # Pruning of duplicates: + @test fmap(+, m3, n3; prune = nothing) == (x = [2,4,6], y = [5,7,9], z = nothing) + + # More than two arguments: + z4 = fmap(+, m3, n3, m3, n3) + @test z4 == fmap(x -> 2x, z3) + @test z4.x === z4.z + + @test fmap(+, foo1, m1, n1) isa Foo + @test fmap(.*, m1, foo1, n1) == (x = [4*7, 2*5*8], y = 3*6*9) end @testset "old test update.jl" begin From 2d2c40ed49d1aa3dc632de3248f749f4d7e0c02d Mon Sep 17 00:00:00 2001 From: Michael Abbott <32575566+mcabbott@users.noreply.github.com> Date: Mon, 7 Feb 2022 18:21:52 -0500 Subject: [PATCH 11/13] also add a docstring note --- src/Functors.jl | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/Functors.jl b/src/Functors.jl index dcf21f8..a3acd2b 100644 --- a/src/Functors.jl +++ b/src/Functors.jl @@ -182,6 +182,16 @@ This function walks (maps) over `xs` calling the continuation `f'` to continue t julia> fmap(x -> 10x, m, walk=(f, x) -> x isa Bar ? x : Functors._default_walk(f, x)) Foo(Bar([1, 2, 3]), (40, 50, Bar(Foo(6, 7)))) ``` + +The behaviour when the same node appears twice can be altered by giving a value +to the `prune` keyword, which is then used in place of all but the first: + +```jldoctest +julia> twice = [1, 2]; + +julia> fmap(float, (x = twice, y = [1,2], z = twice); prune = missing) +(x = [1.0, 2.0], y = [1.0, 2.0], z = missing) +``` """ fmap From 38b2aec15618c3a1ca53627ae441d4384a7bb697 Mon Sep 17 00:00:00 2001 From: Michael Abbott <32575566+mcabbott@users.noreply.github.com> Date: Mon, 7 Feb 2022 18:22:46 -0500 Subject: [PATCH 12/13] bump version, lower bound, testing --- .github/workflows/ci.yml | 4 ++-- Project.toml | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index bfec232..5663e5c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -17,8 +17,8 @@ jobs: fail-fast: false matrix: version: - - '1.5' # Replace this with the minimum Julia version that your package supports. - # - '1' # automatically expands to the latest stable 1.x release of Julia + - '1.6' # Replace this with the minimum Julia version that your package supports. + - '1' # automatically expands to the latest stable 1.x release of Julia - 'nightly' os: - ubuntu-latest diff --git a/Project.toml b/Project.toml index 1f8b8a9..0b40e07 100644 --- a/Project.toml +++ b/Project.toml @@ -1,10 +1,10 @@ name = "Functors" uuid = "d9f16b24-f501-4c13-a1f2-28368ffc5196" authors = ["Mike J Innes "] -version = "0.2.7" +version = "0.2.8" [compat] -julia = "1" +julia = "1.6" Documenter = "0.27" [extras] From 077d6e59640ba771c9392df19f66cbe2c7966eb2 Mon Sep 17 00:00:00 2001 From: Michael Abbott <32575566+mcabbott@users.noreply.github.com> Date: Tue, 8 Feb 2022 23:29:05 -0500 Subject: [PATCH 13/13] fix for 1.0 --- .github/workflows/ci.yml | 1 + Project.toml | 2 +- src/Functors.jl | 2 ++ src/functor.jl | 4 ++-- test/basics.jl | 6 +++++- 5 files changed, 11 insertions(+), 4 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 5663e5c..2a4cf15 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -17,6 +17,7 @@ jobs: fail-fast: false matrix: version: + - '1.0' - '1.6' # Replace this with the minimum Julia version that your package supports. - '1' # automatically expands to the latest stable 1.x release of Julia - 'nightly' diff --git a/Project.toml b/Project.toml index 0b40e07..84dc030 100644 --- a/Project.toml +++ b/Project.toml @@ -4,8 +4,8 @@ authors = ["Mike J Innes "] version = "0.2.8" [compat] -julia = "1.6" Documenter = "0.27" +julia = "1" [extras] Documenter = "e30172f5-a6a5-5a46-863b-614d45cd2de4" diff --git a/src/Functors.jl b/src/Functors.jl index a3acd2b..5247df1 100644 --- a/src/Functors.jl +++ b/src/Functors.jl @@ -23,6 +23,7 @@ usually using the macro [@functor](@ref). """ functor +@static if VERSION >= v"1.5" # var"@functor" doesn't work on 1.0, temporarily disable """ @functor T @functor T (x,) @@ -65,6 +66,7 @@ TwoThirds(Foo(10, 20), Foo(3, 4), 560) ``` """ var"@functor" +end # VERSION """ Functors.isleaf(x) diff --git a/src/functor.jl b/src/functor.jl index 49f2008..b8cb8f5 100644 --- a/src/functor.jl +++ b/src/functor.jl @@ -47,7 +47,7 @@ struct NoKeyword end function fmap(f, x; exclude = isleaf, walk = _default_walk, cache = IdDict(), prune = NoKeyword()) haskey(cache, x) && return prune isa NoKeyword ? cache[x] : prune - cache[x] = exclude(x) ? f(x) : walk(x -> fmap(f, x; exclude, walk, cache, prune), x) + cache[x] = exclude(x) ? f(x) : walk(x -> fmap(f, x; exclude=exclude, walk=walk, cache=cache, prune=prune), x) end ### @@ -75,7 +75,7 @@ end function fmap(f, x, ys...; exclude = isleaf, walk = _default_walk, cache = IdDict(), prune = NoKeyword()) haskey(cache, x) && return prune isa NoKeyword ? cache[x] : prune - cache[x] = exclude(x) ? f(x, ys...) : walk((xy...,) -> fmap(f, xy...; exclude, walk, cache, prune), x, ys...) + cache[x] = exclude(x) ? f(x, ys...) : walk((xy...,) -> fmap(f, xy...; exclude=exclude, walk=walk, cache=cache, prune=prune), x, ys...) end function _default_walk(f, x, ys...) diff --git a/test/basics.jl b/test/basics.jl index 89eadd6..914a5cd 100644 --- a/test/basics.jl +++ b/test/basics.jl @@ -182,9 +182,12 @@ end @test z4.x === z4.z @test fmap(+, foo1, m1, n1) isa Foo - @test fmap(.*, m1, foo1, n1) == (x = [4*7, 2*5*8], y = 3*6*9) + @static if VERSION >= v"1.6" # fails on Julia 1.0 + @test fmap(.*, m1, foo1, n1) == (x = [4*7, 2*5*8], y = 3*6*9) + end end +@static if VERSION >= v"1.6" # Julia 1.0: LoadError: error compiling top-level scope: type definition not allowed inside a local scope @testset "old test update.jl" begin struct M{F,T,S} σ::F @@ -208,6 +211,7 @@ end @test m̂.W ≈ fill(0.8f0, size(m.W)) @test m̂.b ≈ fill(-0.2f0, size(m.b)) end +end # VERSION ### ### FlexibleFunctors.jl