Skip to content

Commit 100291a

Browse files
functor by default (#51)
1 parent b597d47 commit 100291a

15 files changed

+330
-189
lines changed

.gitignore

+4
Original file line numberDiff line numberDiff line change
@@ -3,3 +3,7 @@
33
Manifest.toml
44
build
55
.vscode
6+
benchmarks*.json
7+
results*.json
8+
*.tmp
9+

Project.toml

+9-3
Original file line numberDiff line numberDiff line change
@@ -4,17 +4,23 @@ authors = ["Mike J Innes <[email protected]>"]
44
version = "0.4.12"
55

66
[deps]
7+
Compat = "34da2185-b29b-5c13-b0c7-acf172513d20"
8+
ConstructionBase = "187b0558-2788-49d3-abe0-74a17ed4e7c9"
79
LinearAlgebra = "37e2e46d-f89d-539d-b4ee-838fcccc9c8e"
810

911
[compat]
10-
Documenter = "1"
12+
Compat = "4.16"
13+
ConstructionBase = "1.4"
14+
Measurements = "2"
15+
OrderedCollections = "1.6"
1116
julia = "1.6"
1217

1318
[extras]
14-
Documenter = "e30172f5-a6a5-5a46-863b-614d45cd2de4"
19+
Measurements = "eff96d63-e80a-5855-80a2-b1b0885c5ab7"
20+
OrderedCollections = "bac558e1-5e72-5ebc-8fee-abe8a469f55d"
1521
StaticArrays = "90137ffa-7385-5640-81b9-e52037218182"
1622
Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40"
1723
Zygote = "e88e6eb3-aa80-5325-afca-941959d7151f"
1824

1925
[targets]
20-
test = ["Test", "Documenter", "StaticArrays", "Zygote"]
26+
test = ["Test", "OrderedCollections", "StaticArrays", "Zygote", "Measurements"]

README.md

+15-9
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,9 @@
1313
[action-img]: https://github.com/FluxML/Functors.jl/workflows/CI/badge.svg
1414
[action-url]: https://github.com/FluxML/Functors.jl/actions
1515

16-
Functors.jl provides tools to express a powerful design pattern for dealing with large/ nested structures, as in machine learning and optimisation. For large machine learning models it can be cumbersome or inefficient to work with parameters as one big, flat vector, and structs help manage complexity; but it is also desirable to easily operate over all parameters at once, e.g. for changing precision or applying an optimiser update step.
16+
Functors.jl provides tools to express a powerful design pattern for dealing with large / nested structures, as in machine learning and optimisation. For large machine learning models it can be cumbersome or inefficient to work with parameters as one big, flat vector, and structs help manage complexity; but it is also desirable to easily operate over all parameters at once, e.g. for changing precision or applying an optimiser update step.
17+
18+
## Basic Usage
1719

1820
Functors.jl provides `fmap` to make those things easy, acting as a 'map over parameters':
1921

@@ -25,8 +27,6 @@ julia> struct Foo
2527
y
2628
end
2729

28-
julia> @functor Foo
29-
3030
julia> model = Foo(1, [1, 2, 3])
3131
Foo(1, [1, 2, 3])
3232

@@ -41,26 +41,32 @@ julia> struct Bar
4141
x
4242
end
4343

44-
julia> @functor Bar
45-
4644
julia> model = Bar(Foo(1, [1, 2, 3]))
4745
Bar(Foo(1, [1, 2, 3]))
4846

4947
julia> fmap(float, model)
5048
Bar(Foo(1.0, [1.0, 2.0, 3.0]))
5149
```
5250

51+
> [!NOTE]
52+
> Up to to v0.4, Functors.jl's functionality had to be opted in on custom types via the `@functor Foo` macro call.
53+
> With v0.5 instead, this is no longer necessary: by default any type is recursively traversed up to the leaves
54+
> and `ConstructionBase.constructorof` is used to reconstruct it.
55+
> In order to opt-out of this behaviour and make a type non traversable you can use `@leaf Foo`.
56+
57+
## Further Details
58+
5359
The workhorse of `fmap` is actually a lower level function, `functor`:
5460

5561
```julia
56-
julia> xs, re = functor(Foo(1, [1, 2, 3]))
57-
((x = 1, y = [1, 2, 3]), var"#21#22"())
62+
julia> children, reconstruct = Functors.functor(Foo(1, [1, 2, 3]))
63+
((x = 1, y = [1, 2, 3]), Functors.var"#3#6"{DataType}(Foo))
5864

59-
julia> re(map(float, xs))
65+
julia> reconstruct(map(float, children))
6066
Foo(1.0, [1.0, 2.0, 3.0])
6167
```
6268

63-
`functor` returns the parts of the object that can be inspected, as well as a `re` function that takes those values and restructures them back into an object of the original type.
69+
`functor` returns the parts of the object that can be inspected, as well as a `reconstruct` function that takes those values and restructures them back into an object of the original type.
6470

6571
To include only certain fields, pass a tuple of field names to `@functor`:
6672

benchmark/Project.toml

+9
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
[deps]
2+
AirspeedVelocity = "1c8270ee-6884-45cc-9545-60fa71ec23e4"
3+
BenchmarkPlots = "ab8c0f59-4072-4e0d-8f91-a91e1495eb26"
4+
BenchmarkTools = "6e4b80f9-dd63-53aa-95a3-0cdb28fa8baf"
5+
ConcreteStructs = "2569d6c7-a4a2-43d3-a901-331e8e4be471"
6+
Flux = "587475ba-b771-5e3f-ad9e-33799f191a9c"
7+
Functors = "d9f16b24-f501-4c13-a1f2-28368ffc5196"
8+
Statistics = "10745b16-79ce-11e8-11f9-7d13ad32a3b2"
9+
StatsPlots = "f3b207a7-027a-5e70-b257-86293d7955fd"

benchmark/benchmarks.jl

+57
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
# We run the benchmarks using AirspeedVelocity.jl
2+
3+
# To run benchmarks locally, first install AirspeedVelocity.jl:
4+
# julia> using Pkg; Pkg.add("AirspeedVelocity"); Pkg.build("AirspeedVelocity")
5+
# and make sure .julia/bin is in your PATH.
6+
7+
# Then commit the changes and run:
8+
# $ benchpkg Functors --rev=mybranch,master --bench-on=mybranch
9+
10+
11+
using BenchmarkTools: BenchmarkTools, BenchmarkGroup, @benchmarkable, @btime, @benchmark, judge
12+
using ConcreteStructs: @concrete
13+
using Flux: Dense, Chain
14+
using LinearAlgebra: BLAS
15+
using Functors
16+
using Statistics: median
17+
18+
const SUITE = BenchmarkGroup()
19+
const BENCHMARK_CPU_THREADS = Threads.nthreads()
20+
BLAS.set_num_threads(BENCHMARK_CPU_THREADS)
21+
22+
23+
@concrete struct A
24+
w
25+
b
26+
σ
27+
end
28+
29+
struct B
30+
w
31+
b
32+
σ
33+
end
34+
35+
function setup_fmap_bench!(suite)
36+
a = A(rand(5,5), rand(5), tanh)
37+
suite["fmap"]["concrete struct"] = @benchmarkable fmap(identity, $a)
38+
39+
a = B(rand(5,5), rand(5), tanh)
40+
suite["fmap"]["non-concrete struct"] = @benchmarkable fmap(identity, $a)
41+
42+
a = Dense(5, 5, tanh)
43+
suite["fmap"]["flux dense"] = @benchmarkable fmap(identity, $a)
44+
45+
a = Chain(Dense(5, 5, tanh), Dense(5, 5, tanh))
46+
suite["fmap"]["flux dense chain"] = @benchmarkable fmap(identity, $a)
47+
48+
nt = (layers=(w= rand(5,5), b=rand(5), σ=tanh),)
49+
suite["fmap"]["named tuple"] = @benchmarkable fmap(identity, $nt)
50+
51+
return suite
52+
end
53+
54+
setup_fmap_bench!(SUITE)
55+
56+
## AirspeedVelocity.jl will automatically run the benchmarks and save the results
57+
# results = BenchmarkTools.run(SUITE; verbose=true)

docs/src/index.md

+29-11
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,15 @@ Functors.jl provides a set of tools to represent [functors](https://en.wikipedia
44

55
The most straightforward use is to traverse a complicated nested structure as a tree, and apply a function `f` to every field it encounters along the way.
66

7-
For large models it can be cumbersome or inefficient to work with parameters as one big, flat vector, and structs help manage complexity; but it may be desirable to easily operate over all parameters at once, e.g. for changing precision or applying an optimiser update step.
7+
For large machine learning models it can be cumbersome or inefficient to work with parameters as one big, flat vector, and structs help manage complexity; but it may be desirable to easily operate over all parameters at once, e.g. for changing precision or applying an optimiser update step.
88

99
## Basic Usage and Implementation
1010

11-
When one marks a structure as [`@functor`](@ref) it means that Functors.jl is allowed to look into the fields of the instances of the struct and modify them. This is achieved through [`Functors.fmap`](@ref).
11+
By default, julia types are marked as [`@functor`](@ref Functors.functor)s, meaning that Functors.jl is allowed to look into the fields of the instances of the struct and modify them. This is achieved through [`fmap`](@ref). To opt-out of this behaviour, use [`@leaf`](@ref) on your custom type.
1212

13-
The workhorse of fmap is actually a lower level function, functor:
13+
```julia-repl
14+
15+
The workhorse of `fmap` is actually a lower level function, [`functor`](@ref Functors.functor):
1416
1517
```julia-repl
1618
julia> using Functors
@@ -20,8 +22,6 @@ julia> struct Foo
2022
y
2123
end
2224
23-
julia> @functor Foo
24-
2525
julia> foo = Foo(1, [1, 2, 3]) # notice all the elements are integers
2626
2727
julia> xs, re = Functors.functor(foo)
@@ -50,13 +50,31 @@ julia> fmap(float, model)
5050
Baz(1.0, 2)
5151
```
5252

53-
Any field not in the list will be passed through as-is during reconstruction. This is done by invoking the default constructor, so structs that define custom inner constructors are expected to provide one that acts like the default.
53+
Any field not in the list will be passed through as-is during reconstruction. This is done by invoking the default constructor accepting all fields as arguments, so structs that define custom inner constructors are expected to provide one that acts like the default.
54+
55+
The use of `@functor` with no fields argument as in `@functor Baz` is equivalent to `@functor Baz fieldnames(Baz)` and also equivalent to avoiding `@functor` altogether.
56+
57+
Using [`@leaf`](@ref) instead of [`@functor`](@ref) will prevent the fields of a struct from being traversed.
58+
59+
!!! warning "Change to opt-out behaviour in v0.5"
60+
Previous releases of functors, up to v0.4, used an opt-in behaviour where structs were leaves functors unless marked with `@functor`. This was changed in v0.5 to an opt-out behaviour where structs are functors unless marked with `@leaf`.
61+
62+
## Which types are leaves?
63+
64+
By default all composite types in are functors and can be traversed, unless marked with [`@leaf`](@ref).
65+
66+
The following types instead are explicitly marked as leaves in Functors.jl:
67+
- `Number`.
68+
- `AbstractArray{<:Number}`, except for the wrappers `Transpose`, `Adjoint`, and `PermutedDimsArray`.
69+
- `AbstractString`.
5470

55-
## Appropriate Use
71+
This is because in typical application the internals of these are abstracted away and it is not desirable to traverse them.
5672

57-
!!! warning "Not everything should be a functor!"
58-
Due to its generic nature it is very attractive to mark several structures as [`@functor`](@ref) when it may not be quite safe to do so.
73+
## What if I get an error?
5974

60-
Typically, since any function `f` is applied to the leaves of the tree, but it is possible for some functions to require dispatching on the specific type of the fields causing some methods to be missed entirely.
75+
Since by default Functors.jl tries to traverse most types e.g. when using [`fmap`](@ref), it is possible it fails in case the type has not an appropriate constructor. If use experience this issue, you have a few alternatives:
76+
- Mark the type as a leaf using [`@leaf`](@ref)
77+
- Use the `@functor` macro to specify which fields to traverse.
78+
- Define an appropriate constructor for the type.
6179

62-
Examples of this include element types of arrays which typically have their own mathematical operations defined. Adding a [`@functor`](@ref) to such a type would end up missing methods such as `+(::MyElementType, ::MyElementType)`. Think `RGB` from Colors.jl.
80+
If you are not able to traverse types in julia Base, please open an issue.

0 commit comments

Comments
 (0)