Skip to content

Expand docstrings #30

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

Merged
merged 1 commit into from
Jan 22, 2022
Merged
Show file tree
Hide file tree
Changes from all 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
4 changes: 3 additions & 1 deletion Project.toml
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,12 @@ version = "0.2.7"

[compat]
julia = "1"
Documenter = "0.27"

[extras]
Documenter = "e30172f5-a6a5-5a46-863b-614d45cd2de4"
Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40"
Zygote = "e88e6eb3-aa80-5325-afca-941959d7151f"

[targets]
test = ["Test", "Zygote"]
test = ["Test", "Documenter", "Zygote"]
11 changes: 9 additions & 2 deletions docs/src/api.md
Original file line number Diff line number Diff line change
@@ -1,8 +1,15 @@
```@docs
Functors.@functor
Functors.fmap
Functors.@functor
```

```@docs
Functors.functor
Functors.children
Functors.isleaf
Functors.fcollect
```

```@docs
Functors.fmapstructure
Functors.fcollect
```
166 changes: 147 additions & 19 deletions src/functor.jl
Original file line number Diff line number Diff line change
@@ -1,3 +1,13 @@
"""
Functors.functor(x) = functor(typeof(x), x)

Returns a tuple containing, first, a `NamedTuple` of the children of `x`
(typically its fields), and second, a reconstruction funciton.
This controls the behaviour of [`fmap`](@ref).

Methods should be added to `functor(::Type{T}, x)` for custom types,
usually using the macro [@functor](@ref).
"""
functor(T, x) = (), _ -> x
functor(x) = functor(typeof(x), x)

Expand Down Expand Up @@ -29,6 +39,47 @@ function functorm(T, fs = nothing)
:(makefunctor(@__MODULE__, $(esc(T)), $(fs...)))
end

"""
@functor T
@functor T (x,)

Adds methods to [`functor`](@ref) allowing recursion into objects of type `T`,
and reconstruction. Assumes that `T` has a constructor accepting all of its fields,
which is true unless you have provided an inner constructor which does not.

By default all fields of `T` are considered [children](@ref);
this can be restricted be restructed by providing a tuple of field names.

# Examples
```jldoctest
julia> struct Foo; x; y; end

julia> @functor Foo

julia> Functors.children(Foo(1,2))
(x = 1, y = 2)

julia> _, re = Functors.functor(Foo(1,2));

julia> re((10, 20))
Foo(10, 20)

julia> struct TwoThirds a; b; c; end

julia> @functor TwoThirds (a, c)

julia> ch2, re3 = Functors.functor(TwoThirds(10,20,30));

julia> ch2
(a = 10, c = 30)

julia> re3(("ten", "thirty"))
TwoThirds("ten", 20, "thirty")

julia> fmap(x -> 10x, TwoThirds(Foo(1,2), Foo(3,4), 56))
TwoThirds(Foo(10, 20), Foo(3, 4), 560)
```
"""
macro functor(args...)
functorm(args...)
end
Expand Down Expand Up @@ -61,14 +112,35 @@ macro flexiblefunctor(args...)
end

"""
isleaf(x)
Functors.isleaf(x)

Return true if `x` has no [`children`](@ref) according to [`functor`](@ref).

# Examples
```jldoctest
julia> Functors.isleaf(1)
true

julia> Functors.isleaf([2, 3, 4])
true

julia> Functors.isleaf(["five", [6, 7]])
false

julia> Functors.isleaf([])
false

julia> Functors.isleaf((8, 9))
false

julia> Functors.isleaf(())
true
```
"""
isleaf(x) = children(x) === ()

"""
children(x)
Functors.children(x)

Return the children of `x` as defined by [`functor`](@ref).
Equivalent to `functor(x)[1]`.
Expand Down Expand Up @@ -100,18 +172,55 @@ end
_default_walk(f, ::Nothing, ::Nothing) = nothing

"""
fmap(f, x; exclude = isleaf, walk = Functors._default_walk)

A structure and type preserving `map` that works for all [`functor`](@ref)s.
fmap(f, x; exclude = Functors.isleaf, walk = Functors._default_walk)

By default, traverses `x` recursively using [`functor`](@ref)
and transforms every leaf node identified by `exclude` with `f`.
Comment on lines -107 to -108
Copy link
Member Author

Choose a reason for hiding this comment

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

It's important to know that exclude is used before functor, it stops the recursion, rather than just controlling whether f is applied. I'm not sure that's what we want, but it's what we have.

Copy link
Member

Choose a reason for hiding this comment

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

Yes, this is deliberate because it's the easiest way to control the depth of recursion. Looking back on #13, I think it might've been better just to call this isleaf: that's literally what it's checking and exclude could imply that f is not being applied to anything the function predicate says is true.

Copy link
Member Author

@mcabbott mcabbott Jan 22, 2022

Choose a reason for hiding this comment

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

Ok. Maybe all names for such things are confusing...

What it sounds like it might do is let you tell fmap(sqrt, (2, Foo(3, sin))) which leaves to apply f to, and which to leave alone, without altering recursion. I guess you can test isleaf first. Maybe all functionality can be written in terms of any other...

Copy link
Member

@ToucheSir ToucheSir Jan 22, 2022

Choose a reason for hiding this comment

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

It's a little confusing because f is also used as a name for not the original user callback, but in essence Functors will try to recurse into anything "left alone" by isleaf/exclude. It's just that the fallback definition of functor returns an empty tuple of children, so recursion naturally stops if the value in question can't be unpacked.

A structure and type preserving `map`.

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.
By default it transforms every leaf node (identified by `exclude`, default [`isleaf`](@ref))
by applying `f`, and otherwise traverses `x` recursively using [`functor`](@ref).

# Examples
```jldoctest
julia> fmap(string, (x=1, y=(2, 3)))
(x = "1", y = ("2", "3"))

julia> nt = (a = [1,2], b = [23, (45,), (x=6//7, y=())], c = [8,9]);

julia> fmap(println, nt)
[1, 2]
23
45
6//7
()
[8, 9]
(a = nothing, b = Any[nothing, (nothing,), (x = nothing, y = nothing)], c = nothing)

julia> fmap(println, nt; exclude = x -> x isa Array)
[1, 2]
Any[23, (45,), (x = 6//7, y = ())]
[8, 9]
(a = nothing, b = nothing, c = nothing)

julia> twice = [1, 2];

julia> fmap(println, (i = twice, ii = 34, iii = [5, 6], iv = (twice, 34), v = 34.0))
[1, 2]
34
[5, 6]
34.0
(i = nothing, ii = nothing, iii = nothing, iv = (nothing, nothing), v = nothing)
```

If the same node (same according to `===`) appears more than once,
it will only be handled once, and only be transformed once with `f`.
Thus the result will also have this relationship.

By default, `Tuple`s, `NamedTuple`s, and some other container-like types in Base have
children to recurse into. Arrays of numbers do not.
To enable recursion into new types, you must provide a method of [`functor`](@ref),
which can be done using the macro [`@functor`](@ref):

```jldoctest withfoo
julia> struct Foo; x; y; end

julia> @functor Foo
Expand All @@ -120,19 +229,27 @@ julia> struct Bar; x; end

julia> @functor Bar

julia> m = Foo(Bar([1,2,3]), (4, 5));
julia> m = Foo(Bar([1,2,3]), (4, 5, Bar(Foo(6, 7))));

julia> fmap(x -> 2x, m)
Foo(Bar([2, 4, 6]), (8, 10))
julia> fmap(x -> 10x, m)
Foo(Bar([10, 20, 30]), (40, 50, Bar(Foo(60, 70))))

julia> fmap(string, m)
Foo(Bar("[1, 2, 3]"), ("4", "5"))
Foo(Bar("[1, 2, 3]"), ("4", "5", Bar(Foo("6", "7"))))

julia> fmap(string, m, exclude = v -> v isa Bar)
Foo("Bar([1, 2, 3])", (4, 5))
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.

julia> fmap(x -> 2x, m, walk=(f, x) -> x isa Bar ? x : Functors._default_walk(f, x))
Foo(Bar([1, 2, 3]), (8, 10))
```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))))
```
"""
function fmap(f, x; exclude = isleaf, walk = _default_walk, cache = IdDict())
Expand All @@ -146,7 +263,9 @@ end
"""
fmapstructure(f, x; exclude = isleaf)

Like [`fmap`](@ref), but doesn't preserve the type of custom structs. Instead, it returns a (potentially nested) `NamedTuple`.
Like [`fmap`](@ref), but doesn't preserve the type of custom structs.
Instead, it returns a `NamedTuple` (or a `Tuple`, or an array),
or a nested set of these.

Useful for when the output must not contain custom structs.

Expand All @@ -156,10 +275,19 @@ julia> struct Foo; x; y; end

julia> @functor Foo

julia> m = Foo([1,2,3], (4, 5));
julia> m = Foo([1,2,3], [4, (5, 6), Foo(7, 8)]);

julia> fmapstructure(x -> 2x, m)
(x = [2, 4, 6], y = (8, 10))
(x = [2, 4, 6], y = Any[8, (10, 12), (x = 14, y = 16)])

julia> fmapstructure(println, m)
[1, 2, 3]
4
5
6
7
8
(x = nothing, y = Any[nothing, (nothing, nothing), (x = nothing, y = nothing)])
```
"""
fmapstructure(f, x; kwargs...) = fmap(f, x; walk = (f, x) -> map(f, children(x)), kwargs...)
Expand Down
11 changes: 11 additions & 0 deletions test/runtests.jl
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,16 @@ using Zygote
@testset "Functors.jl" begin

include("basics.jl")
include("base.jl")
include("update.jl")

if VERSION < v"1.6" # || VERSION > v"1.7-"
@warn "skipping doctests, on Julia $VERSION"
else
using Documenter
@testset "doctests" begin
DocMeta.setdocmeta!(Functors, :DocTestSetup, :(using Functors); recursive=true)
doctest(Functors, manual=true)
end
end
end