Skip to content

Commit 981c866

Browse files
authored
Use the cache less often (#39)
* simplest isbits usecache * apply the cache only to leaf nodes * adopt a better rule, using anymutable * improve inference * fix 1.6 * use clever || idea * rm commented-out compat code
1 parent 1fdf055 commit 981c866

File tree

4 files changed

+99
-20
lines changed

4 files changed

+99
-20
lines changed

Project.toml

Lines changed: 1 addition & 1 deletion
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 <mike.j.innes@gmail.com>"]
4-
version = "0.3.0"
4+
version = "0.4.0"
55

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

src/Functors.jl

Lines changed: 6 additions & 5 deletions
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,19 +131,21 @@ 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]
139138
34
140139
[5, 6]
140+
34
141141
34.0
142142
(i = nothing, ii = nothing, iii = nothing, iv = (nothing, nothing), v = nothing)
143143
```
144144
145-
If the same node (same according to `===`) appears more than once,
146-
it will only be handled once, and only be transformed once with `f`.
147-
Thus the result will also have this relationship.
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.
148149
149150
By default, `Tuple`s, `NamedTuple`s, and some other container-like types in Base have
150151
children to recurse into. Arrays of numbers do not.

src/functor.jl

Lines changed: 48 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,10 @@
22
functor(T, x) = (), _ -> x
33
functor(x) = functor(typeof(x), x)
44

5-
functor(::Type{<:Tuple}, x) = x, y -> y
5+
functor(::Type{<:Tuple}, x) = x, identity
66
functor(::Type{<:NamedTuple{L}}, x) where L = NamedTuple{L}(map(s -> getproperty(x, s), L)), identity
77

8-
functor(::Type{<:AbstractArray}, x) = x, y -> y
8+
functor(::Type{<:AbstractArray}, x) = x, identity
99
functor(::Type{<:AbstractArray{<:Number}}, x) = (), _ -> x
1010

1111
function makefunctor(m::Module, T, fs = fieldnames(T))
@@ -39,13 +39,32 @@ function _default_walk(f, x)
3939
re(map(f, func))
4040
end
4141

42-
struct NoKeyword end
42+
usecache(::AbstractDict, x) = isleaf(x) ? anymutable(x) : ismutable(x)
43+
usecache(::Nothing, x) = false
4344

44-
function fmap(f, x; exclude = isleaf, walk = _default_walk, cache = IdDict(), prune = NoKeyword())
45-
haskey(cache, x) && return prune isa NoKeyword ? cache[x] : prune
46-
cache[x] = exclude(x) ? f(x) : walk(x -> fmap(f, x; exclude=exclude, walk=walk, cache=cache, prune=prune), x)
45+
@generated function anymutable(x::T) where {T}
46+
ismutabletype(T) && return true
47+
subs = [:(anymutable(getfield(x, $f))) for f in QuoteNode.(fieldnames(T))]
48+
return Expr(:(||), subs...)
4749
end
4850

51+
struct NoKeyword end
52+
53+
function fmap(f, x; exclude = isleaf, walk = _default_walk, cache = anymutable(x) ? IdDict() : nothing, prune = NoKeyword())
54+
if usecache(cache, x) && haskey(cache, x)
55+
return prune isa NoKeyword ? cache[x] : prune
56+
end
57+
ret = if exclude(x)
58+
f(x)
59+
else
60+
walk(x -> fmap(f, x; exclude, walk, cache, prune), x)
61+
end
62+
if usecache(cache, x)
63+
cache[x] = ret
64+
end
65+
ret
66+
end
67+
4968
###
5069
### Extras
5170
###
@@ -69,9 +88,19 @@ end
6988
### Vararg forms
7089
###
7190

72-
function fmap(f, x, ys...; exclude = isleaf, walk = _default_walk, cache = IdDict(), prune = NoKeyword())
73-
haskey(cache, x) && return prune isa NoKeyword ? cache[x] : prune
74-
cache[x] = exclude(x) ? f(x, ys...) : walk((xy...,) -> fmap(f, xy...; exclude=exclude, walk=walk, cache=cache, prune=prune), x, ys...)
91+
function fmap(f, x, ys...; exclude = isleaf, walk = _default_walk, cache = anymutable(x) ? IdDict() : nothing, prune = NoKeyword())
92+
if usecache(cache, x) && haskey(cache, x)
93+
return prune isa NoKeyword ? cache[x] : prune
94+
end
95+
ret = if exclude(x)
96+
f(x, ys...)
97+
else
98+
walk((xy...,) -> fmap(f, xy...; exclude, walk, cache, prune), x, ys...)
99+
end
100+
if usecache(cache, x)
101+
cache[x] = ret
102+
end
103+
ret
75104
end
76105

77106
function _default_walk(f, x, ys...)
@@ -108,3 +137,13 @@ end
108137
macro flexiblefunctor(args...)
109138
flexiblefunctorm(args...)
110139
end
140+
141+
###
142+
### Compat
143+
###
144+
145+
if VERSION < v"1.7"
146+
# Function in 1.7 checks t.name.flags & 0x2 == 0x2,
147+
# but for 1.6 this seems to work instead:
148+
ismutabletype(@nospecialize t) = t.mutable
149+
end

test/basics.jl

Lines changed: 44 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11

2-
using Functors: functor
2+
using Functors: functor, usecache
33

44
struct Foo; x; y; end
55
@functor Foo
@@ -14,6 +14,7 @@ struct NoChildren2; x; y; end
1414

1515
struct NoChild{T}; x::T; end
1616

17+
1718
###
1819
### Basic functionality
1920
###
@@ -47,28 +48,66 @@ end
4748
@test (model′.x, model′.y, model′.z) == (1, 4, 3)
4849
end
4950

50-
@testset "cache" begin
51+
@testset "Sharing" begin
5152
shared = [1,2,3]
5253
m1 = Foo(shared, Foo([1,2,3], Foo(shared, [1,2,3])))
5354
m1f = fmap(float, m1)
5455
@test m1f.x === m1f.y.y.x
5556
@test m1f.x !== m1f.y.x
5657
m1p = fmapstructure(identity, m1; prune = nothing)
5758
@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
5861

59-
# A non-leaf node can also be repeated:
62+
# Here "4" is not shared, because Foo isn't leaf:
6063
m2 = Foo(Foo(shared, 4), Foo(shared, 4))
6164
@test m2.x === m2.y
6265
m2f = fmap(float, m2)
6366
@test m2f.x.x === m2f.y.x
6467
m2p = fmapstructure(identity, m2; prune = Bar(0))
65-
@test m2p == (x = (x = [1, 2, 3], y = 4), y = Bar(0))
68+
@test m2p == (x = (x = [1, 2, 3], y = 4), y = (x = Bar{Int64}(0), y = 4))
6669

6770
# Repeated isbits types should not automatically be regarded as shared:
6871
m3 = Foo(Foo(shared, 1:3), Foo(1:3, shared))
6972
m3p = fmapstructure(identity, m3; prune = 0)
7073
@test m3p.y.y == 0
71-
@test_broken m3p.y.x == 1:3
74+
@test m3p.y.x == 1:3
75+
76+
# All-isbits trees need not create a cache at all:
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
87+
88+
@testset "usecache" begin
89+
d = IdDict()
90+
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)
110+
end
72111
end
73112

74113
@testset "functor(typeof(x), y) from @functor" begin

0 commit comments

Comments
 (0)