Hi, I’m happy to announce that Transducers 0.3 is released. Let me share some highlights.
If you are new to Transducers checkout the last discussion ANN: Transducers.jl, efficient and composable algorithms for map- and reduce-like operations
and maybe my talk from yesterday
and the slide Transducers
Fusible group-by
There are many Julia packages provide a way to fuse group-by and reduction-per-group: SplitApplyCombine.groupreduce
, IndexedTables.groupreduce
, OnlineStatsBase.GroupBy
, etc. However, I couldn’t find a package that can fuse group-by, reduction, and termination. It turned out this is very easy to implement as a transducer. Here is an example. Following snippet tries to find if there is a group with a sum greater than 3 and stop the computation as soon as it is found:
julia> result = transduce(
GroupBy(
string,
Map(last) |> Scan(+) |> ReduceIf(x -> x > 3),
),
right,
nothing,
[1, 2, 1, 2, 3],
);
julia> result isa Reduced
true
julia> unreduced(result)
Dict{String,Int64} with 2 entries:
"1" => 2
"2" => 4
The termination can happen inside a group or outside a group. That is to say, it’s possible to construct a transducer to terminate the computation, say, three groups are found.
julia> transduce(
GroupBy(identity, Map(identity)) |> ReduceIf(groups -> length(groups) >= 3),
right,
nothing,
1:9,
) |> unreduced
Dict{Int64,Pair{Int64,Int64}} with 3 entries:
2 => 2=>2
3 => 3=>3
1 => 1=>1
Note that the aforementioned group-by functions in other packages are much more optimized than Transducers.GroupBy
. If you need speed and don’t need termination, using Transducers.GroupBy
probably is not a good idea at this point.
See more in the manual: https://tkf.github.io/Transducers.jl/v0.3/manual/#Transducers.GroupBy
Iterator comprehension support
Iterator comprehension can be translated directory to transducers. You don’t need to know how to use transducers to benefit from some speedups:
julia> itr = (y for x in 1:1000 for y in 1:x);
julia> @btime foldl(+, eduction(itr))
21.127 μs (1497 allocations: 39.13 KiB)
167167000
julia> @btime sum(itr)
174.544 μs (1 allocation: 16 bytes)
167167000
See also: https://tkf.github.io/Transducers.jl/v0.3/manual/#Transducers.eduction
OnlineStats support
OnlineStats are the transducers in disguise. Now you can do, e.g., Transducer(OnlineStats.Mean())
, to use them with transducers.
“GPU support”
Stateless and non-terminating transducers can be used without Transducers’ custom foldl
/reduce
. This means that Transducers.jl now “supports” CuArrays’ reduce
.
using Transducers, CuArrays, CUDAnative
A = CuArray(randn(3, 3))
rf = reducingfunction(Map(CUDAnative.sin) |> Filter(x -> x > 0), +)
reduce(rf, A, init=0.0f0, dims=1)
Taking “zeros” seriously (internal)
To fully embrace mutate-or-widen approach and get rid of the use of inference API, I had to “take zeros seriously.” This is because unlike Base.foldl
, Transducers.jl’s foldl
needs to handle the case that the elements in the container are filtered out (i.e., it must handle “dynamic emptiness”). This excludes the strategy used by Base.foldl
where the first element from the container is used as the initial value (note: this approach is also not appropriate for “asymmetric reducing functions” like push!
). For this, I created a small package called InitialValues.jl. It implements a generic “left identity” singleton value Init(op)
for operator op
; i.e., op(Init(op), x) === x
for any x
. This handles the initialization of the state of the reduction.
I also created BangBang.jl which provides functions such as push!!
, implementing the mutate-or-widen variants of a subset of Base
functions.
Combination of these two approaches turned out to be a very powerful way to implement transducers in Julia. At first, I was worried if I can keep the performance advantage of the transducers because using the singleton value Init(op)
introduces union types. Impressively, Julia compiler can handle many cases, although some transducers are still hard to infer; e.g., collect(Take(1), 1:1)
cannot be inferred. Fortunately, specifying the initial value explicitly (e.g., foldl(push!!, Take(3), 1:1; init=Int[])
) fixes the inference most of the times.
Now that Transducers.jl’s main entry points can be inferred by the compiler, if the transducers used are not very complex, it makes it natural to use transducers in performance-sensitive code. (For anyone who read the old manual, it means that the awkward workaround based on eduction
for the function barrier is now gone.)