Repository: FluxML/Optimisers.jl (julia)

Issue:
Using destructure with functions or anything without (trainable) parameters
```
julia> Optimisers.destructure(x->x)

ERROR: ArgumentError: reducing over an empty collection is not allowed
Stacktrace:
  [1] _empty_reduce_error()
    @ Base ./reduce.jl:301
  [2] mapreduce_empty(f::Function, op::Function, T::Type)
    @ Base ./reduce.jl:344
  [3] reduce_empty(op::Base.MappingRF{typeof(eltype), typeof(promote_type)}, #unused#::Type{AbstractVector})
    @ Base ./reduce.jl:331
  [4] reduce_empty_iter
    @ ./reduce.jl:357 [inlined]
  [5] mapreduce_empty_iter(f::Function, op::Function, itr::Vector{AbstractVector}, ItrEltype::Base.HasEltype)
    @ Base ./reduce.jl:353
  [6] _mapreduce(f::typeof(eltype), op::typeof(promote_type), #unused#::IndexLinear, A::Vector{AbstractVector})
    @ Base ./reduce.jl:402
  [7] _mapreduce_dim
    @ ./reducedim.jl:330 [inlined]
  [8] #mapreduce#725
    @ ./reducedim.jl:322 [inlined]
  [9] mapreduce
    @ ./reducedim.jl:322 [inlined]
 [10] reduce(#unused#::typeof(vcat), A::Vector{AbstractVector})
    @ Base ./abstractarray.jl:1621
 [11] _flatten
    @ ~/.julia/packages/Optimisers/cLLV1/src/destructure.jl:67 [inlined]
 [12] destructure(x::Function)
    @ Optimisers ~/.julia/packages/Optimisers/cLLV1/src/destructure.jl:22
 [13] top-level scope
    @ REPL[23]:1
 [14] top-level scope
    @ ~/.julia/packages/CUDA/Uurn4/src/initialization.jl:52
```
Same for `Chain(x->x)` or e.g. `Dropout(0)`

Is it sufficient to add `isempty(arrays) && return [], off, len[]` above https://github.com/FluxML/Optimisers.jl/blob/master/src/destructure.jl#L67 ?

Results into
```
julia> Optimisers.destructure(Chain(x->x, Dropout(0)))
(Any[], Restructure(Chain, ..., 0))
```

Pull request description:
Closes #67

Patch:
diff --git a/Project.toml b/Project.toml
index dc42e636..172ed31e 100644
--- a/Project.toml
+++ b/Project.toml
@@ -1,7 +1,7 @@
 name = "Optimisers"
 uuid = "3bd65402-5787-11e9-1adc-39752487f4e2"
 authors = ["Mike J Innes <mike.j.innes@gmail.com>"]
-version = "0.2.2" 
+version = "0.2.3"
 
 [deps]
 ChainRulesCore = "d360d2e6-b24c-11e9-a2a3-2a2ae2dbcce4"
diff --git a/src/Optimisers.jl b/src/Optimisers.jl
index 417b90d4..56968182 100644
--- a/src/Optimisers.jl
+++ b/src/Optimisers.jl
@@ -6,7 +6,7 @@ using LinearAlgebra
 include("interface.jl")
 
 include("destructure.jl")
-export destructure, total, total2
+export destructure
 
 include("rules.jl")
 export Descent, ADAM, Momentum, Nesterov, RMSProp,
diff --git a/src/destructure.jl b/src/destructure.jl
index d000ff75..2dd4b1c5 100644
--- a/src/destructure.jl
+++ b/src/destructure.jl
@@ -64,6 +64,7 @@ function _flatten(x)
     len[] = o + length(y)
     o
   end
+  isempty(arrays) && return Bool[], off, 0
   reduce(vcat, arrays), off, len[]
 end
 


Test patch:
diff --git a/test/destructure.jl b/test/destructure.jl
index d20f4f30..0b3a482a 100644
--- a/test/destructure.jl
+++ b/test/destructure.jl
@@ -220,3 +220,24 @@ tmp1
 @test tmp2 isa Vector{<:ForwardDiff.Dual}
 
 =#
+
+@testset "empty, issue 67" begin
+    m0 = (nothing, missing, isempty)
+    @test destructure(m0)[1] isa Vector{<:Real}
+    v0, re0 = destructure(m0)
+    @test re0(Float32[]) === m0
+    @test_throws DimensionMismatch re0([1])
+
+    # This is an elaborate way of checking that it doesn't cause promotions, even of small floats:
+    m01 = [(x=nothing, y=0), (x=Float16[1, 2], y=Float16[3])]
+    v01, _ = destructure(m01)
+    v012 = vcat(destructure(m01[1])[1], destructure(m01[2])[1])
+    @test v01 == v012
+    @test v012 isa Vector{Float16}
+
+    y, bk = Zygote.pullback(x -> sum(destructure(x)[1]), ("a", :beta))
+    @test bk(1.0) == (nothing,)
+    # Zygote regards 3,4 as differentiable, but Optimisers does not regard them as parameters:
+    y, bk = Zygote.pullback(x -> sum(destructure(x)[1]), (3, 4))
+    @test bk(1.0) == (nothing,)
+end