Skip to content
Open
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
1 change: 1 addition & 0 deletions src/Transducers.jl
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ export AdHocFoldable,
Reduced,
Replace,
Scan,
Scanx,
ScanEmit,
SequentialEx,
SplitBy,
Expand Down
90 changes: 90 additions & 0 deletions src/library.jl
Original file line number Diff line number Diff line change
Expand Up @@ -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])

Expand Down
17 changes: 17 additions & 0 deletions test/test_library.jl
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down