-
-
Notifications
You must be signed in to change notification settings - Fork 16
Separate walks out from fmap
and add #39 to fcollect
#43
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 5 commits
e051e21
3f11d5a
27a8f8c
cb9a0f2
31cb97a
e402dad
ffd6ae9
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,18 @@ | ||
fmap(walk::AbstractWalk, f, x, ys...) = walk((xs...) -> fmap(walk, f, xs...), x, ys...) | ||
|
||
function fmap(f, x, ys...; exclude = isleaf, | ||
walk = DefaultWalk(), | ||
cache = IdDict(), | ||
prune = NoKeyword()) | ||
_walk = if isnothing(cache) | ||
ExcludeWalk(AnonymousWalk(walk), f, exclude) | ||
else | ||
CachedWalk(ExcludeWalk(AnonymousWalk(walk), f, exclude), prune, cache) | ||
end | ||
fmap(_walk, f, x, ys...) | ||
end | ||
|
||
fmapstructure(f, x; kwargs...) = fmap(f, x; walk = StructuralWalk(), kwargs...) | ||
|
||
fcollect(x; exclude = v -> false) = | ||
fmap(ExcludeWalk(CollectWalk(), _ -> nothing, exclude), _ -> nothing, x) |
Original file line number | Diff line number | Diff line change | ||||
---|---|---|---|---|---|---|
@@ -0,0 +1,159 @@ | ||||||
""" | ||||||
AbstractWalk | ||||||
|
||||||
Any walk for use with [`fmap`](@ref) should inherit from this type. | ||||||
A walk subtyping `AbstractWalk` must satisfy the walk function interface: | ||||||
```julia | ||||||
struct MyWalk <: AbstractWalk end | ||||||
|
||||||
function (::MyWalk)(recurse, x, ys...) | ||||||
# implement this | ||||||
end | ||||||
``` | ||||||
The walk function is called on a node `x` in a Functors tree. | ||||||
It may also be passed associated nodes `ys...` in other Functors trees. | ||||||
The walk function recurses further into `(x, ys...)` by calling | ||||||
`recurse` on the child nodes. | ||||||
The choice of which nodes to recurse and in what order is custom to the walk. | ||||||
""" | ||||||
abstract type AbstractWalk end | ||||||
|
||||||
""" | ||||||
AnonymousWalk(walk_fn) | ||||||
|
||||||
Wrap a `walk_fn` so that `AnonymousWalk(walk_fn) isa AbstractWalk`. | ||||||
This type only exists for backwards compatability and should be directly used. | ||||||
Attempting to wrap an existing `AbstractWalk` is a no-op (i.e. it is not wrapped). | ||||||
""" | ||||||
struct AnonymousWalk{F} <: AbstractWalk | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
Is there any reason not to constrain these type parameters, at least for now? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The reason this type exists is to wrap |
||||||
walk::F | ||||||
|
||||||
function AnonymousWalk(walk::F) where F | ||||||
Base.depwarn("Wrapping a custom walk function as an `AnonymousWalk`. Future versions will only support custom walks that explicitly subtyle `AbstractWalk`.", :AnonymousWalk) | ||||||
return new{F}(walk) | ||||||
end | ||||||
end | ||||||
# do not wrap an AbstractWalk | ||||||
AnonymousWalk(walk::AbstractWalk) = walk | ||||||
|
||||||
(walk::AnonymousWalk)(recurse, x, ys...) = walk.walk(recurse, x, ys...) | ||||||
|
||||||
""" | ||||||
DefaultWalk() | ||||||
|
||||||
The default walk behavior for Functors.jl. | ||||||
Walks all the [`Functors.children`](@ref) of trees `(x, ys...)` based on | ||||||
the structure of `x`. | ||||||
The resulting mapped child nodes are restructured into the type of `x`. | ||||||
|
||||||
See [`fmap`](@ref) for more information. | ||||||
""" | ||||||
struct DefaultWalk <: AbstractWalk end | ||||||
|
||||||
function (::DefaultWalk)(recurse, x, ys...) | ||||||
func, re = functor(x) | ||||||
yfuncs = map(y -> functor(typeof(x), y)[1], ys) | ||||||
re(map(recurse, func, yfuncs...)) | ||||||
end | ||||||
|
||||||
""" | ||||||
StructuralWalk() | ||||||
|
||||||
A structural variant of [`Functors.DefaultWalk`](@ref). | ||||||
The recursion behavior is identical, but the mapped children are not restructured. | ||||||
|
||||||
See [`fmapstructure`](@ref) for more information. | ||||||
""" | ||||||
struct StructuralWalk <: AbstractWalk end | ||||||
|
||||||
(::StructuralWalk)(recurse, x) = map(recurse, children(x)) | ||||||
|
||||||
""" | ||||||
ExcludeWalk(walk, fn, exclude) | ||||||
|
||||||
A walk that recurses nodes `(x, ys...)` according to `walk`, | ||||||
except when `exclude(x)` is true. | ||||||
Then, `fn(x, ys...)` is applied instead of recursing further. | ||||||
|
||||||
Typically wraps an existing `walk` for use with [`fmap`](@ref). | ||||||
""" | ||||||
struct ExcludeWalk{T, F, G} <: AbstractWalk | ||||||
walk::T | ||||||
fn::F | ||||||
exclude::G | ||||||
end | ||||||
|
||||||
(walk::ExcludeWalk)(recurse, x, ys...) = | ||||||
walk.exclude(x) ? walk.fn(x, ys...) : walk.walk(recurse, x, ys...) | ||||||
|
||||||
struct NoKeyword end | ||||||
|
||||||
usecache(::AbstractDict, x) = isleaf(x) ? anymutable(x) : ismutable(x) | ||||||
usecache(::Nothing, x) = false | ||||||
|
||||||
@generated function anymutable(x::T) where {T} | ||||||
ismutabletype(T) && return true | ||||||
subs = [:(anymutable(getfield(x, $f))) for f in QuoteNode.(fieldnames(T))] | ||||||
return Expr(:(||), subs...) | ||||||
end | ||||||
|
||||||
""" | ||||||
CachedWalk(walk[; prune]) | ||||||
|
||||||
A walk that recurses nodes `(x, ys...)` according to `walk` and storing the | ||||||
output of the recursion in a cache indexed by `x` (based on object ID). | ||||||
Whenever the cache already contains `x`, either: | ||||||
- `prune` is specified, then it is returned, or | ||||||
- `prune` is unspecified, and the previously cached recursion of `(x, ys...)` | ||||||
returned. | ||||||
|
||||||
Typically wraps an existing `walk` for use with [`fmap`](@ref). | ||||||
""" | ||||||
struct CachedWalk{T, S} <: AbstractWalk | ||||||
walk::T | ||||||
prune::S | ||||||
cache::IdDict{Any, Any} | ||||||
end | ||||||
CachedWalk(walk; prune = NoKeyword(), cache = IdDict()) = | ||||||
CachedWalk(walk, prune, cache) | ||||||
|
||||||
function (walk::CachedWalk)(recurse, x, ys...) | ||||||
should_cache = usecache(walk.cache, x) | ||||||
if should_cache && haskey(walk.cache, x) | ||||||
return walk.prune isa NoKeyword ? walk.cache[x] : walk.prune | ||||||
else | ||||||
ret = walk.walk(recurse, x, ys...) | ||||||
if should_cache | ||||||
walk.cache[x] = ret | ||||||
end | ||||||
return ret | ||||||
end | ||||||
end | ||||||
|
||||||
""" | ||||||
CollectWalk() | ||||||
|
||||||
A walk that recurses into a node `x` via [`Functors.children`](@ref), | ||||||
storing the recursion history in a cache. | ||||||
The resulting ordered recursion history is returned. | ||||||
|
||||||
See [`fcollect`](@ref) for more information. | ||||||
""" | ||||||
struct CollectWalk <: AbstractWalk | ||||||
cache::Base.IdSet{Any} | ||||||
output::Vector{Any} | ||||||
end | ||||||
CollectWalk() = CollectWalk(Base.IdSet(), Any[]) | ||||||
|
||||||
# note: we don't have an `OrderedIdSet`, so we use an `IdSet` for the cache | ||||||
# (to ensure we get exactly 1 copy of each distinct array), and a usual `Vector` | ||||||
# for the results, to preserve traversal order (important downstream!). | ||||||
function (walk::CollectWalk)(recurse, x) | ||||||
x in walk.cache && return walk.output | ||||||
darsnack marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||
# to exclude, we wrap this walk in ExcludeWalk | ||||||
push!(walk.cache, x) | ||||||
push!(walk.output, x) | ||||||
map(recurse, children(x)) | ||||||
|
||||||
return walk.output | ||||||
end |
Uh oh!
There was an error while loading. Please reload this page.