Repository: JuliaFolds2/OhMyThreads.jl (julia)

Issue:
Empty collections in `tmap`
For `tmap` (and thus also `tcollect`) we currently have
```julia
julia> tmap(sin, 11:10)
ERROR: ArgumentError: You must either indicate the desired number of chunks (n) or the target size of a chunk (size).
Stacktrace:
  [1] missing_input_err()
    @ ChunkSplitters ~/.julia/packages/ChunkSplitters/R5lnx/src/ChunkSplitters.jl:118
  [2] getchunk(itr::UnitRange{Int64}, ichunk::Int64; n::Int64, size::Int64, split::Symbol)
    @ ChunkSplitters ~/.julia/packages/ChunkSplitters/R5lnx/src/ChunkSplitters.jl:274
  [3] getchunk
    @ ~/.julia/packages/ChunkSplitters/R5lnx/src/ChunkSplitters.jl:273 [inlined]
  [4] getchunk
    @ ~/.julia/packages/ChunkSplitters/R5lnx/src/ChunkSplitters.jl:293 [inlined]
  [5] iterate
    @ ~/.julia/packages/ChunkSplitters/R5lnx/src/ChunkSplitters.jl:142 [inlined]
  [6] iterate
    @ ~/.julia/packages/ChunkSplitters/R5lnx/src/ChunkSplitters.jl:141 [inlined]
  [7] copyto!(dest::Vector{StepRange{Int64, Int64}}, src::ChunkSplitters.Chunk{UnitRange{Int64}, ChunkSplitters.FixedCount})
    @ Base ~/.julia/juliaup/julia-1.10.2+0.aarch64.apple.darwin14/share/julia/base/abstractarray.jl:943
  [8] _collect
    @ ./array.jl:765 [inlined]
  [9] collect
    @ ./array.jl:759 [inlined]
 [10] _tmap(::DynamicScheduler{OhMyThreads.Schedulers.FixedCount}, ::Function, ::UnitRange{Int64})
    @ OhMyThreads.Implementation ~/repos/OhMyThreads.jl/src/implementation.jl:385
 [11] #tmap#81
    @ ~/repos/OhMyThreads.jl/src/implementation.jl:307 [inlined]
 [12] tmap(::Function, ::UnitRange{Int64})
    @ OhMyThreads.Implementation ~/repos/OhMyThreads.jl/src/implementation.jl:282
 [13] top-level scope
    @ REPL[91]:1
```

For comparison:
```julia
julia> map(sin, 11:10)
Float64[]

julia> map(x->im*x, 11:10)
Complex{Int64}[]

julia> map(x->im*x, 11.0:10.0)
ComplexF64[]
```

This is similar to #86 (`tforeach`) which led me to (https://github.com/JuliaFolds2/OhMyThreads.jl/pull/88) handle empty collections for `tmapreduce` (and thus also `tforeach`) explicitly (throw error if no `init` is supplied). I tried to align with the serial `mapreduce` / `foreach` behavior which, I think, is also what we should aim for here.

Pull request:
Extension of #88.
Closes #87.

With this PR, we now explicitly fall back to the serial `Base` functions if the input `isempty`.

Shouldn't be breaking because we pretty much always error'ed before.

Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 4a0c5db3..bf92e1a7 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,6 +1,10 @@
 OhMyThreads.jl Changelog
 =========================
 
+Version 0.5.2
+-------------
+- ![Enhancement][badge-enhancement] For empty input (e.g. `Float64[]` or `11:10`) behavior is now aligned with the serial functions in `Base`.
+
 Version 0.5.1
 -------------
 - ![Feature][badge-feature] Within a parallel `@tasks` block one can now mark a region with `@one_by_one`. This region will be run by one task at a time ("critical region").
diff --git a/src/implementation.jl b/src/implementation.jl
index 217f67d7..88786cbe 100644
--- a/src/implementation.jl
+++ b/src/implementation.jl
@@ -60,13 +60,9 @@ function tmapreduce(f, op, Arrs...;
         kwargs...)
     mapreduce_kwargs = isgiven(init) ? (; init) : (;)
     _scheduler = _scheduler_from_userinput(scheduler; kwargs...)
-    if isempty(first(Arrs))
-        isempty(mapreduce_kwargs) && reduce_empty_err()
-        return mapreduce_kwargs.init
-    end
 
-    # @show _scheduler
-    if _scheduler isa SerialScheduler
+    if _scheduler isa SerialScheduler || isempty(first(Arrs))
+        # empty input collection → align with Base.mapreduce behavior
         mapreduce(f, op, Arrs...; mapreduce_kwargs...)
     else
         @noinline _tmapreduce(f, op, Arrs, outputtype, _scheduler, mapreduce_kwargs)
@@ -78,10 +74,6 @@ end
     throw(ArgumentError("Providing an explicit scheduler as well as direct keyword arguments (e.g. $(kwargstr)) is currently not supported."))
 end
 
-@noinline function reduce_empty_err()
-    throw(ArgumentError("reducing over an empty collection is not allowed; consider supplying `init`"))
-end
-
 treducemap(op, f, A...; kwargs...) = tmapreduce(f, op, A...; kwargs...)
 
 # DynamicScheduler: AbstractArray/Generic
@@ -357,7 +349,8 @@ function tmap(f,
     end
 
     Arrs = (A, _Arrs...)
-    if _scheduler isa SerialScheduler
+    if _scheduler isa SerialScheduler || isempty(A)
+        # empty input collection → align with Base.map behavior
         map(f, Arrs...; kwargs...)
     else
         check_all_have_same_indices(Arrs)


Test patch:
diff --git a/test/runtests.jl b/test/runtests.jl
index 035dbb65..fcac91b7 100644
--- a/test/runtests.jl
+++ b/test/runtests.jl
@@ -364,14 +364,30 @@ end;
 end;
 
 @testset "empty collections" begin
+    @static if VERSION < v"1.11.0-"
+        err = MethodError
+    else
+        err = ArgumentError
+    end
     for empty_coll in (11:9, Float64[])
         for f in (sin, x -> im * x, identity)
             for op in (+, *, min)
-                @test_throws ArgumentError tmapreduce(f, op, empty_coll)
+                # mapreduce
                 for init in (0.0, 0, 0.0 * im, 0.0f0)
                     @test tmapreduce(f, op, empty_coll; init) == init
                 end
+                # foreach
                 @test tforeach(f, empty_coll) |> isnothing
+                # reduce
+                if op != min
+                    @test treduce(op, empty_coll) == reduce(op, empty_coll)
+                else
+                    @test_throws err treduce(op, empty_coll)
+                end
+                # map
+                @test tmap(f, empty_coll) == map(f, empty_coll)
+                # collect
+                @test tcollect(empty_coll) == collect(empty_coll)
             end
         end
     end