diff --git a/src/Transducers.jl b/src/Transducers.jl index 6aa599a8..03c7a0f3 100644 --- a/src/Transducers.jl +++ b/src/Transducers.jl @@ -37,6 +37,7 @@ export AdHocFoldable, Reduced, Replace, Scan, + Scanx, ScanEmit, SequentialEx, SplitBy, diff --git a/src/library.jl b/src/library.jl index 6f7a504f..2556e80b 100644 --- a/src/library.jl +++ b/src/library.jl @@ -1254,6 +1254,96 @@ function next(rf::R_{Scan}, result, input) end end +""" + Scanx(f, [init = Init]) + +Accumulate input with binary function `f` and pass the accumulated +result so far to the inner reduction step. + +The inner reducing step receives the sequence `y₁, y₂, y₃, ..., yₙ₊₁, ...` +when the sequence `x₁, x₂, x₃, ..., xₙ, ...` is fed to `Scanx(f, [init])`. + + y₁ = init + y₂ = f(init, x₁) + y₃ = f(y₁, x₂) + ... + yₙ₊₁ = f(yₙ₋₁, xₙ) + +This is a generalized version of the +[_prefix sum_](https://en.wikipedia.org/wiki/Prefix_sum) and is the _exclusive scan_ counterpart to [`Scan`](@ref). Note that this implementation _includes_ the both the final element and the initial element. See `DropLast` to combine transducers to remove the final element. + +Note that the associativity of `f` is not required when the transducer +is used in a process that gurantee an order, such as [`foldl`](@ref). + +Unless `f` is a function with known identity element such as `+` or `*`, the initial state `init` must be +provided. + +$_use_initializer + +See also: [`ScanxEmit`](@ref), [`Iterated`](@ref). + +# Examples +```jldoctest +julia> using Transducers + +julia> collect(Scanx(*), 1:3) +4-element Vector{Int64}: + 1 + 1 + 2 + 6 + +julia> collect(Scanx((a, b) -> a + b,0), 1:3) +4-element Vector{Int64}: + 0 + 1 + 3 + 6 + +julia> collect(Scanx(*, 10), 1:3) +4-element Vector{Int64}: + 10 + 10 + 20 + 60 +``` +""" +struct Scanx{F, T} <: Transducer + f::F + init::T +end + +Scanx(f) = Scanx(_asmonoid(f), Init) # TODO: DefaultInit? + +isexpansive(::Scanx) = false + +function start(rf::R_{Scanx}, result) + return wrap(rf, start(xform(rf).f, Unseen()), start(inner(rf), result)) +# For now, using `start` on `rf.f` is only for invoking `initialize` +# on `rf.init`. But maybe it's better to support `reducingfunction`? +# For example, use `unwrap_all` before feeding the accumulator to the +# inner reducing function? +end + +complete(rf::R_{Scanx}, result) = complete(inner(rf), unwrap(rf, result)[2]) + +function next(rf::R_{Scanx}, result, input) + wrapping(rf, result) do acc, iresult + if acc isa Unseen + ival = start(xform(rf).f, xform(rf).init) + acc = xform(rf).f(ival, input) + cur, n = acc, next(inner(rf), push!!(iresult,convert(typeof(input),ival)),input) + else + acc = xform(rf).f(acc, input) + + cur, n = acc, next(inner(rf), iresult, acc) + end + # TODO: Don't call inner when `acc` is an `InitialValue`? + # What about when `Reduced`? + return cur, n + end +end + """ ScanEmit(f, init[, onlast]) diff --git a/test/test_library.jl b/test/test_library.jl index 01241be1..a8175068 100644 --- a/test/test_library.jl +++ b/test/test_library.jl @@ -61,6 +61,23 @@ end end end +@testset "Scanx" begin + @testset for xs in iterator_variants(1:10) + xs isa Base.Generator && continue + @test collect(Scanx(+), xs) == [0;cumsum(xs)] + @test collect(Scanx(*), xs) == [1;cumprod(xs)] + @test collect(Scanx((a, b) -> a + b,0), xs) == [0;cumsum(xs)] + end + + xs0 = [0, -1, 3, -2, 1] + @testset for xs in [xs0, collect(xs0)] + @test collect(Scanx(max,typemin(eltype(xs))), xs) == [typemin(eltype(xs)),0, 0, 3, 3,3] + @test collect(Scanx(min,typemax(eltype(xs))), xs) == [typemax(eltype(xs)),0, -1, -1, -2,-2] + end +end + + + @testset "ScanEmit" begin @testset for xs in iterator_variants(1:3) @test collect(ScanEmit(tuple, 0), xs) == 0:2