Skip to content
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

Separate walks out from fmap and add #39 to fcollect #43

Merged
merged 7 commits into from
Nov 4, 2022
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions docs/src/api.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,16 @@ Functors.children
Functors.isleaf
```

```@docs
Functors.AbstractWalk
Functors.DefaultWalk
Functors.StructuralWalk
Functors.ExcludeWalk
Functors.CachedWalk
Functors.CollectWalk
Functors.AnonymousWalk
```

```@docs
Functors.fmapstructure
Functors.fcollect
Expand Down
24 changes: 19 additions & 5 deletions src/Functors.jl
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ module Functors
export @functor, @flexiblefunctor, fmap, fmapstructure, fcollect

include("functor.jl")
include("walks.jl")
include("maps.jl")
include("base.jl")

###
Expand Down Expand Up @@ -102,7 +104,8 @@ Equivalent to `functor(x)[1]`.
children

"""
fmap(f, x; exclude = Functors.isleaf, walk = Functors._default_walk)
fmap(f, x, ys...; exclude = Functors.isleaf, walk = Functors.DefaultWalk()[, prune])
fmap(walk, f, x, ys...)

A structure and type preserving `map`.

Expand Down Expand Up @@ -176,12 +179,23 @@ Foo("Bar([1, 2, 3])", (4, 5, "Bar(Foo(6, 7))"))
To recurse into custom types without reconstructing them afterwards,
use [`fmapstructure`](@ref).

For advanced customization of the traversal behaviour, pass a custom `walk` function of the form `(f', xs) -> ...`.
This function walks (maps) over `xs` calling the continuation `f'` to continue traversal.
For advanced customization of the traversal behaviour,
pass a custom `walk` function that subtypes [`Functors.AbstractWalk`](ref).
The form `fmap(walk, f, x, ys...)` can be called for custom walks.
The simpler form `fmap(f, x, ys...; walk = mywalk)` will wrap `mywalk` in
[`ExcludeWalk`](@ref) then [`CachedWalk`](@ref).

```jldoctest withfoo
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))))
julia> struct MyWalk <: Functors.AbstractWalk end

julia> (::MyWalk)(recurse, x) = x isa Bar ? "hello" :
Functors.DefaultWalk()(recurse, x)

julia> fmap(x -> 10x, m; walk = MyWalk())
Foo("hello", (40, 50, "hello"))

julia> fmap(MyWalk(), x -> 10x, m)
Foo("hello", (4, 5, "hello"))
```

The behaviour when the same node appears twice can be altered by giving a value
Expand Down
75 changes: 0 additions & 75 deletions src/functor.jl
Original file line number Diff line number Diff line change
Expand Up @@ -34,81 +34,6 @@ isleaf(x) = children(x) === ()

children(x) = functor(x)[1]

function _default_walk(f, x)
func, re = functor(x)
re(map(f, func))
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

struct NoKeyword end

function fmap(f, x; exclude = isleaf, walk = _default_walk, cache = anymutable(x) ? IdDict() : nothing, prune = NoKeyword())
if usecache(cache, x) && haskey(cache, x)
return prune isa NoKeyword ? cache[x] : prune
end
ret = if exclude(x)
f(x)
else
walk(x -> fmap(f, x; exclude, walk, cache, prune), x)
end
if usecache(cache, x)
cache[x] = ret
end
ret
end

###
### Extras
###

fmapstructure(f, x; kwargs...) = fmap(f, x; walk = (f, x) -> map(f, children(x)), kwargs...)

function fcollect(x; output = [], cache = Base.IdSet(), exclude = v -> false)
# 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!).
x in cache && return output
if !exclude(x)
push!(cache, x)
push!(output, x)
foreach(y -> fcollect(y; cache=cache, output=output, exclude=exclude), children(x))
end
return output
end

###
### Vararg forms
###

function fmap(f, x, ys...; exclude = isleaf, walk = _default_walk, cache = anymutable(x) ? IdDict() : nothing, prune = NoKeyword())
if usecache(cache, x) && haskey(cache, x)
return prune isa NoKeyword ? cache[x] : prune
end
ret = if exclude(x)
f(x, ys...)
else
walk((xy...,) -> fmap(f, xy...; exclude, walk, cache, prune), x, ys...)
end
if usecache(cache, x)
cache[x] = ret
end
ret
end

function _default_walk(f, x, ys...)
func, re = functor(x)
yfuncs = map(y -> functor(typeof(x), y)[1], ys)
re(map(f, func, yfuncs...))
end

###
### FlexibleFunctors.jl
###
Expand Down
18 changes: 18 additions & 0 deletions src/maps.jl
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
darsnack marked this conversation as resolved.
Show resolved Hide resolved
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)
159 changes: 159 additions & 0 deletions src/walks.jl
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
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
struct AnonymousWalk{F} <: AbstractWalk
struct AnonymousWalk{F<:AbstractWalk} <: AbstractWalk

Is there any reason not to constrain these type parameters, at least for now?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The reason this type exists is to wrap F that isn't a AbstractWalk but still satisfies the AbstractWalk interface.

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