Skip to content

Commit a1050b2

Browse files
committed
adopt a better rule, using anymutable
1 parent f1acb51 commit a1050b2

File tree

4 files changed

+93
-49
lines changed

4 files changed

+93
-49
lines changed

Project.toml

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
name = "Functors"
22
uuid = "d9f16b24-f501-4c13-a1f2-28368ffc5196"
33
authors = ["Mike J Innes <[email protected]>"]
4-
version = "0.3.0"
4+
version = "0.4.0"
55

66
[deps]
77
LinearAlgebra = "37e2e46d-f89d-539d-b4ee-838fcccc9c8e"

src/Functors.jl

+5-6
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@ export @functor, @flexiblefunctor, fmap, fmapstructure, fcollect
55
include("functor.jl")
66
include("base.jl")
77

8-
98
###
109
### Docstrings for basic functionality
1110
###
@@ -132,7 +131,7 @@ Any[23, (45,), (x = 6//7, y = ())]
132131
[8, 9]
133132
(a = nothing, b = nothing, c = nothing)
134133
135-
julia> twice = [1, 2];
134+
julia> twice = [1, 2]; # println only acts once on this
136135
137136
julia> fmap(println, (i = twice, ii = 34, iii = [5, 6], iv = (twice, 34), v = 34.0))
138137
[1, 2]
@@ -143,10 +142,10 @@ julia> fmap(println, (i = twice, ii = 34, iii = [5, 6], iv = (twice, 34), v = 34
143142
(i = nothing, ii = nothing, iii = nothing, iv = (nothing, nothing), v = nothing)
144143
```
145144
146-
If the same object appears more than once, it will only be handled once, and only be
147-
transformed once with `f`. Thus the result will also have this relationship.
148-
Here "same" means `===` for non-`isbits` types. The same number (e.g. `34 === 34`) at
149-
different nodes is taken to be a coincidence, and `f` applies twice.
145+
Mutable objects which appear more than once are only handled once (by caching `f(x)` in an `IdDict`).
146+
Thus the relationship `x.i === x.iv[1]` will be preserved.
147+
An immutable object which appears twice is not stored in the cache, thus `f(34)` will be called twice,
148+
and the results will agree only if `f` is pure.
150149
151150
By default, `Tuple`s, `NamedTuple`s, and some other container-like types in Base have
152151
children to recurse into. Arrays of numbers do not.

src/functor.jl

+53-27
Original file line numberDiff line numberDiff line change
@@ -39,26 +39,39 @@ function _default_walk(f, x)
3939
re(map(f, func))
4040
end
4141

42-
usecache(x) = !isbits(x)
43-
usecache(x::Union{String, Symbol}) = false
42+
usecache(::AbstractDict, x) = isleaf(x) ? anymutable(x) : ismutable(x)
43+
usecache(::Nothing, x) = false
44+
45+
# function _anymutable(x::T) where {T}
46+
# ismutable(x) && return true
47+
# fs = fieldnames(T)
48+
# isempty(fs) && return false
49+
# return any(f -> anymutable(getfield(x, f)), fs)
50+
# end
51+
@generated function anymutable(x::T) where {T}
52+
ismutabletype(T) && return true
53+
fs = fieldnames(T)
54+
isempty(fs) && return false
55+
subs = [:(anymutable(getfield(x, $f))) for f in QuoteNode.(fs)]
56+
return :(|($(subs...)))
57+
end
4458

4559
struct NoKeyword end
4660

47-
function fmap(f, x; exclude = isleaf, walk = _default_walk, cache = usecache(x) ? IdDict() : nothing, prune = NoKeyword())
48-
if exclude(x)
49-
if usecache(x)
50-
if haskey(cache, x)
51-
prune isa NoKeyword ? cache[x] : prune
52-
else
53-
cache[x] = f(x)
54-
end
55-
else
56-
f(x)
57-
end
61+
function fmap(f, x; exclude = isleaf, walk = _default_walk, cache = anymutable(x) ? IdDict() : nothing, prune = NoKeyword())
62+
if usecache(cache, x) && haskey(cache, x)
63+
return prune isa NoKeyword ? cache[x] : prune
64+
end
65+
ret = if exclude(x)
66+
f(x)
5867
else
59-
walk(x -> fmap(f, x; exclude = exclude, walk = walk, cache = cache, prune = prune), x)
68+
walk(x -> fmap(f, x; exclude, walk, cache, prune), x)
6069
end
61-
end
70+
if usecache(cache, x)
71+
cache[x] = ret
72+
end
73+
ret
74+
end
6275

6376
###
6477
### Extras
@@ -83,20 +96,19 @@ end
8396
### Vararg forms
8497
###
8598

86-
function fmap(f, x, ys...; exclude = isleaf, walk = _default_walk, cache = usecache(x) ? IdDict() : nothing, prune = NoKeyword())
87-
if exclude(x)
88-
if usecache(x)
89-
if haskey(cache, x)
90-
prune isa NoKeyword ? cache[x] : prune
91-
else
92-
cache[x] = f(x, ys...)
93-
end
94-
else
95-
f(x, ys...)
96-
end
99+
function fmap(f, x, ys...; exclude = isleaf, walk = _default_walk, cache = anymutable(x) ? IdDict() : nothing, prune = NoKeyword())
100+
if usecache(cache, x) && haskey(cache, x)
101+
return prune isa NoKeyword ? cache[x] : prune
102+
end
103+
ret = if exclude(x)
104+
f(x, ys...)
97105
else
98-
walk((xy...,) -> fmap(f, xy...; exclude = exclude, walk = walk, cache = cache, prune = prune), x, ys...)
106+
walk((xy...,) -> fmap(f, xy...; exclude, walk, cache, prune), x, ys...)
107+
end
108+
if usecache(cache, x)
109+
cache[x] = ret
99110
end
111+
ret
100112
end
101113

102114
function _default_walk(f, x, ys...)
@@ -133,3 +145,17 @@ end
133145
macro flexiblefunctor(args...)
134146
flexiblefunctorm(args...)
135147
end
148+
149+
###
150+
### Compat
151+
###
152+
153+
if VERSION < v"1.7"
154+
# Copied verbatim from Base:
155+
function ismutabletype(@nospecialize t)
156+
@_total_meta
157+
t = unwrap_unionall(t)
158+
# TODO: what to do for `Union`?
159+
return isa(t, DataType) && t.name.flags & 0x2 == 0x2
160+
end
161+
end

test/basics.jl

+34-15
Original file line numberDiff line numberDiff line change
@@ -48,16 +48,18 @@ end
4848
@test (model′.x, model′.y, model′.z) == (1, 4, 3)
4949
end
5050

51-
@testset "cache" begin
51+
@testset "Sharing" begin
5252
shared = [1,2,3]
5353
m1 = Foo(shared, Foo([1,2,3], Foo(shared, [1,2,3])))
5454
m1f = fmap(float, m1)
5555
@test m1f.x === m1f.y.y.x
5656
@test m1f.x !== m1f.y.x
5757
m1p = fmapstructure(identity, m1; prune = nothing)
5858
@test m1p == (x = [1, 2, 3], y = (x = [1, 2, 3], y = (x = nothing, y = [1, 2, 3])))
59+
m1no = fmap(float, m1; cache = nothing) # disable the cache by hand
60+
@test m1no.x !== m1no.y.y.x
5961

60-
# The cache applies only to leaf nodes, so that "4" is not shared:
62+
# Here "4" is not shared, because Foo isn't leaf:
6163
m2 = Foo(Foo(shared, 4), Foo(shared, 4))
6264
@test m2.x === m2.y
6365
m2f = fmap(float, m2)
@@ -72,22 +74,39 @@ end
7274
@test m3p.y.x == 1:3
7375

7476
# All-isbits trees need not create a cache at all:
75-
@test isbits(fmap(float, (x=1, y=(2, 3), z=4:5)))
76-
@test_skip 0 == @allocated fmap(float, (x=1, y=(2, 3), z=4:5))
77+
m4 = (x=1, y=(2, 3), z=4:5)
78+
@test isbits(fmap(float, m4))
79+
@test_skip 0 == @allocated fmap(float, m4) # true, but fails in tests
80+
81+
# Shared mutable containers are preserved, even if all children are isbits:
82+
ref = Ref(1)
83+
m5 = (x = ref, y = ref, z = Ref(1))
84+
m5f = fmap(x -> x/2, m5)
85+
@test m5f.x === m5f.y
86+
@test m5f.x !== m5f.z
7787

7888
@testset "usecache" begin
79-
# Leaf types:
80-
@test usecache([1,2])
81-
@test !usecache(4.0)
82-
@test usecache(NoChild([1,2]))
83-
@test !usecache(NoChild((3,4)))
89+
d = IdDict()
8490

85-
# Not leaf by default, but `exclude` can change that:
86-
@test usecache(Ref(3))
87-
@test !usecache((5, 6.0))
88-
@test !usecache((a = 2pi, b = missing))
89-
90-
@test usecache((x = [1,2,3], y = 4))
91+
# Leaf types:
92+
@test usecache(d, [1,2])
93+
@test !usecache(d, 4.0)
94+
@test usecache(d, NoChild([1,2]))
95+
@test !usecache(d, NoChild((3,4)))
96+
97+
# Not leaf:
98+
@test usecache(d, Ref(3)) # mutable container
99+
@test !usecache(d, (5, 6.0))
100+
@test !usecache(d, (a = 2pi, b = missing))
101+
102+
@test !usecache(d, (5, [6.0]')) # contains mutable
103+
@test !usecache(d, (x = [1,2,3], y = 4))
104+
105+
usecache(d, OneChild3([1,2], 3, nothing)) # mutable isn't a child, do we care?
106+
107+
# No dictionary:
108+
@test !usecache(nothing, [1,2])
109+
@test !usecache(nothing, 3)
91110
end
92111
end
93112

0 commit comments

Comments
 (0)