Skip to content

Commit 4183248

Browse files
add KeyPath function
add to docs fmap_with_keypath simplify names add fmapstructure_with_path update fmapstructure to handle multiple arguments fixes Update src/Functors.jl Co-authored-by: Kyle Daruwalla <[email protected]> Update src/Functors.jl Co-authored-by: Kyle Daruwalla <[email protected]> Update src/Functors.jl Co-authored-by: Kyle Daruwalla <[email protected]>
1 parent 6778507 commit 4183248

File tree

8 files changed

+359
-10
lines changed

8 files changed

+359
-10
lines changed

docs/src/api.md

+6
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
```@docs
22
Functors.fmap
3+
Functors.fmap_with_path
34
Functors.@functor
45
Functors.@leaf
56
```
@@ -24,6 +25,11 @@ Functors.IterateWalk
2425

2526
```@docs
2627
Functors.fmapstructure
28+
Functors.fmapstructure_with_path
2729
Functors.fcollect
2830
Functors.fleaves
2931
```
32+
33+
```@docs
34+
Functors.KeyPath
35+
```

src/Functors.jl

+52-3
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
module Functors
22

3-
export @functor, @flexiblefunctor, fmap, fmapstructure, fcollect, execute, fleaves
3+
export @functor, @flexiblefunctor, fmap, fmapstructure, fcollect, execute, fleaves,
4+
KeyPath, fmap_with_path, fmapstructure_with_path
45

56
include("functor.jl")
7+
include("keypath.jl")
68
include("walks.jl")
79
include("maps.jl")
810
include("base.jl")
@@ -104,7 +106,7 @@ Equivalent to `functor(x)[1]`.
104106
children
105107

106108
"""
107-
fmap(f, x, ys...; exclude = Functors.isleaf, walk = Functors.DefaultWalk()[, prune])
109+
fmap(f, x, ys...; exclude = Functors.isleaf, walk = Functors.DefaultWalk(), [prune])
108110
109111
A structure and type preserving `map`.
110112
@@ -113,6 +115,8 @@ by applying `f`, and otherwise traverses `x` recursively using [`functor`](@ref)
113115
Optionally, it may also be associated with objects `ys` with the same tree structure.
114116
In that case, `f` is applied to the corresponding leaf nodes in `x` and `ys`.
115117
118+
See also [`fmap_with_path`](@ref) and [`fmapstructure`](@ref).
119+
116120
# Examples
117121
```jldoctest
118122
julia> fmap(string, (x=1, y=(2, 3)))
@@ -222,14 +226,16 @@ fmap
222226

223227

224228
"""
225-
fmapstructure(f, x; exclude = isleaf)
229+
fmapstructure(f, x, ys...; exclude = isleaf, [prune])
226230
227231
Like [`fmap`](@ref), but doesn't preserve the type of custom structs.
228232
Instead, it returns a `NamedTuple` (or a `Tuple`, or an array),
229233
or a nested set of these.
230234
231235
Useful for when the output must not contain custom structs.
232236
237+
See also [`fmap`](@ref) and [`fmapstructure_with_path`](@ref).
238+
233239
# Examples
234240
```jldoctest
235241
julia> struct Foo; x; y; end
@@ -304,6 +310,47 @@ julia> fcollect(m, exclude = v -> Functors.isleaf(v))
304310
fcollect
305311

306312

313+
""""
314+
fmap_with_path(f, x, ys...; exclude = isleaf, walk = DefaultWalkWithPath(), [prune])
315+
316+
Like [`fmap`](@ref), but also passes a [`KeyPath`](@ref Functors.KeyPath) to `f` for each node in the
317+
recursion. The `KeyPath` is a tuple of the indices used to reach the current
318+
node from the root of the recursion. The `KeyPath` is constructed by the
319+
`walk` function, and can be used to reconstruct the path to the current node
320+
from the root of the recursion.
321+
322+
`f` has to accept two arguments: the associated `KeyPath` and the value of the current node.
323+
324+
`exclude` also receives the `KeyPath` as its first argument and a node as its second.
325+
It should return `true` if the recursion should not continue on its children and `f` applied to it.
326+
327+
`prune` is used to control the behaviour when the same node appears twice, see [`fmap`](@ref)
328+
for more information.
329+
330+
# Examples
331+
332+
```jldoctest
333+
julia> x = ([1, 2, 3], 4, (a=5, b=Dict("A"=>6, "B"=>7), c=Dict("C"=>8, "D"=>9)));
334+
335+
julia> fexclude(kp, x) = kp == KeyPath(3, :c) || Functors.isleaf(x);
336+
337+
julia> fmap_with_path((kp, x) -> x isa Dict ? nothing : x.^2, x; exclude = fexclude)
338+
([1, 4, 9], 16, (a = 25, b = Dict("B" => 49, "A" => 36), c = nothing))
339+
```
340+
"""
341+
fmap_with_path
342+
343+
"""
344+
fmapstructure_with_path(f, x, ys...; [exclude, prune])
345+
346+
Like [`fmap_with_path`](@ref), but doesn't preserve the type of custom structs.
347+
Instead, it returns a named tuple, a tuple, an array, a dict,
348+
or a nested set of these.
349+
350+
See also [`fmapstructure`](@ref).
351+
"""
352+
fmapstructure_with_path
353+
307354

308355
"""
309356
fleaves(x; exclude = isleaf)
@@ -332,7 +379,9 @@ julia> fleaves(m)
332379
2-element Vector{Any}:
333380
[1, 2, 3]
334381
TypeWithNoChildren(:a, :b)
382+
```
335383
"""
336384
fleaves
337385

338386
end # module
387+

src/keypath.jl

+47
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
KeyT = Union{Symbol, AbstractString, Integer}
2+
3+
"""
4+
KeyPath(keys...)
5+
6+
A type for representing a path of keys to a value in a nested structure.
7+
Can be constructed with a sequence of keys, or by concatenating other `KeyPath`s.
8+
Keys can be of type `Symbol`, `String`, or `Int`.
9+
10+
# Examples
11+
12+
```jldoctest
13+
julia> kp = KeyPath(:b, 3)
14+
KeyPath(:b, 3)
15+
16+
julia> KeyPath(:a, kp, :c, 4)
17+
KeyPath(:a, :b, 3, :c, 4)
18+
```
19+
"""
20+
struct KeyPath{T<:Tuple}
21+
keys::T
22+
end
23+
24+
@functor KeyPath
25+
isleaf(::KeyPath, @nospecialize(x)) = isleaf(x)
26+
27+
function KeyPath(keys::Union{KeyT, KeyPath}...)
28+
ks = (k isa KeyPath ? (k.keys...,) : (k,) for k in keys)
29+
return KeyPath(((ks...)...,))
30+
end
31+
32+
Base.getindex(kp::KeyPath, i::Int) = kp.keys[i]
33+
Base.length(kp::KeyPath) = length(kp.keys)
34+
Base.iterate(kp::KeyPath, state=1) = iterate(kp.keys, state)
35+
Base.:(==)(kp1::KeyPath, kp2::KeyPath) = kp1.keys == kp2.keys
36+
37+
function Base.show(io::IO, kp::KeyPath)
38+
compat = get(io, :compact, false)
39+
if compat
40+
print(io, keypathstr(kp))
41+
else
42+
print(io, "KeyPath$(kp.keys)")
43+
end
44+
end
45+
46+
keypathstr(kp::KeyPath) = join(kp.keys, ".")
47+

src/maps.jl

+16-1
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,22 @@ function fmap(f, x, ys...; exclude = isleaf,
1111
execute(_walk, x, ys...)
1212
end
1313

14-
fmapstructure(f, x; kwargs...) = fmap(f, x; walk = StructuralWalk(), kwargs...)
14+
function fmap_with_path(f, x, ys...;
15+
exclude = isleaf,
16+
walk = DefaultWalkWithPath(),
17+
cache = IdDict(),
18+
prune = NoKeyword())
19+
20+
_walk = ExcludeWalkWithKeyPath(walk, f, exclude)
21+
if !isnothing(cache)
22+
_walk = CachedWalkWithPath(_walk, prune, cache)
23+
end
24+
return execute(_walk, KeyPath(), x, ys...)
25+
end
26+
27+
fmapstructure(f, x, ys...; kwargs...) = fmap(f, x, ys...; walk = StructuralWalk(), kwargs...)
28+
29+
fmapstructure_with_path(f, x, ys...; kwargs...) = fmap_with_path(f, x, ys...; walk = StructuralWalkWithPath(), kwargs...)
1530

1631
fcollect(x; exclude = v -> false) =
1732
execute(ExcludeWalk(CollectWalk(), _ -> nothing, exclude), x)

src/walks.jl

+72-2
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,24 @@
1-
_map(f, x...) = map(f, x...)
1+
function _map(f, x, ys...)
2+
check_lenghts(x, ys...) || error("all arguments must have at least the same length of the firs one")
3+
map(f, x, ys...)
4+
end
5+
6+
function check_lenghts(x, ys...)
7+
n = length(x)
8+
return all(y -> length(y) >= n, ys)
9+
end
10+
211
_map(f, x::Dict, ys...) = Dict(k => f(v, (y[k] for y in ys)...) for (k, v) in x)
312

413
_values(x) = x
514
_values(x::Dict) = values(x)
615

16+
_keys(x::Dict) = Dict(k => k for k in keys(x))
17+
_keys(x::Tuple) = (keys(x)...,)
18+
_keys(x::AbstractArray) = collect(keys(x))
19+
_keys(x::NamedTuple{Ks}) where Ks = NamedTuple{Ks}(Ks)
20+
21+
722
"""
823
AbstractWalk
924
@@ -76,6 +91,16 @@ function (::DefaultWalk)(recurse, x, ys...)
7691
re(_map(recurse, func, yfuncs...))
7792
end
7893

94+
struct DefaultWalkWithPath <: AbstractWalk end
95+
96+
function (::DefaultWalkWithPath)(recurse, kp::KeyPath, x, ys...)
97+
x_children, re = functor(x)
98+
kps = _map(c -> KeyPath(kp, c), _keys(x_children)) # use _keys and _map to preserve x_children type
99+
ys_children = map(children, ys)
100+
re(_map(recurse, kps, x_children, ys_children...))
101+
end
102+
103+
79104
"""
80105
StructuralWalk()
81106
@@ -86,7 +111,20 @@ See [`fmapstructure`](@ref) for more information.
86111
"""
87112
struct StructuralWalk <: AbstractWalk end
88113

89-
(::StructuralWalk)(recurse, x) = _map(recurse, children(x))
114+
function (::StructuralWalk)(recurse, x, ys...)
115+
x_children = children(x)
116+
ys_children = map(children, ys)
117+
return _map(recurse, x_children, ys_children...)
118+
end
119+
120+
struct StructuralWalkWithPath <: AbstractWalk end
121+
122+
function (::StructuralWalkWithPath)(recurse, kp::KeyPath, x, ys...)
123+
x_children = children(x)
124+
kps = _map(c -> KeyPath(kp, c), _keys(x_children)) # use _keys and _map to preserve x_children type
125+
ys_children = map(children, ys)
126+
return _map(recurse, kps, x_children, ys_children...)
127+
end
90128

91129
"""
92130
ExcludeWalk(walk, fn, exclude)
@@ -106,6 +144,16 @@ end
106144
(walk::ExcludeWalk)(recurse, x, ys...) =
107145
walk.exclude(x) ? walk.fn(x, ys...) : walk.walk(recurse, x, ys...)
108146

147+
struct ExcludeWalkWithKeyPath{T, F, G} <: AbstractWalk
148+
walk::T
149+
fn::F
150+
exclude::G
151+
end
152+
153+
(walk::ExcludeWalkWithKeyPath)(recurse, kp::KeyPath, x, ys...) =
154+
walk.exclude(kp, x) ? walk.fn(kp, x, ys...) : walk.walk(recurse, kp, x, ys...)
155+
156+
109157
struct NoKeyword end
110158

111159
usecache(::Union{AbstractDict, AbstractSet}, x) =
@@ -152,6 +200,28 @@ function (walk::CachedWalk)(recurse, x, ys...)
152200
end
153201
end
154202

203+
struct CachedWalkWithPath{T, S} <: AbstractWalk
204+
walk::T
205+
prune::S
206+
cache::IdDict{Any, Any}
207+
end
208+
209+
CachedWalkWithPath(walk; prune = NoKeyword(), cache = IdDict()) =
210+
CachedWalkWithPath(walk, prune, cache)
211+
212+
function (walk::CachedWalkWithPath)(recurse, kp::KeyPath, x, ys...)
213+
should_cache = usecache(walk.cache, x)
214+
if should_cache && haskey(walk.cache, x)
215+
return walk.prune isa NoKeyword ? walk.cache[x] : walk.prune
216+
else
217+
ret = walk.walk(recurse, kp, x, ys...)
218+
if should_cache
219+
walk.cache[x] = ret
220+
end
221+
return ret
222+
end
223+
end
224+
155225
"""
156226
CollectWalk()
157227

0 commit comments

Comments
 (0)