Skip to content

Commit

Permalink
Treat transducers as iterator transforms at surface syntax (#319)
Browse files Browse the repository at this point in the history
This patch implements the idea discussed in #67.  That is to say,

```julia
collect(Map(f) |> Filter(g), xs)
foldl(+, Map(f) |> Filter(g), xs)
```

is now written as

```julia
xs |> Map(f) |> Filter(g) |> collect
foldl(+, xs |> Map(f) |> Filter(g))
```

or

```julia
collect(opcompose(Map(f), Filter(g)), xs)
foldl(+, opcompose(Map(f), Filter(g)), xs)

collect(Map(f) ⨟ Filter(g), xs)
foldl(+, Map(f) ⨟ Filter(g), xs)
```

or even (not recommended)

```julia
collect(Filter(g)((Map(f)(xs))))  # Julia >= 1.3

collect(Filter(g) ∘ Map(f), xs)
foldl(+, Filter(g) ∘ Map(f), xs)

collect((Filter(g)' ⨟ Map(f)')', xs)
foldl(+, (Filter(g)' ⨟ Map(f)')', xs)

collect((Map(f)' ∘ Filter(g)')', xs)
foldl(+, (Map(f)' ∘ Filter(g)')', xs)
```

Above syntax are all compatible with the view that `xf(itr)` is an
iterator transformation.  OTOH, `xf'` (`adjoint(::Transducer)`) is now
the "classic" transducer; i.e., reducing function transformation.
This makes it easy to use transducers with other "reducing function
combinators" like `TeeRF`:

```julia
rf = TeeRF(Filter(iseven)'(min), Filter(isodd)'(max))
reduce(rf, Map(identity), xs)  # => (even minimum, odd maximum)
```

This PR only deprecates `::Transducer |> ::Transducer` to help
migration from the old syntax.
  • Loading branch information
tkf authored Jul 4, 2020
1 parent db547f9 commit f2ab3d0
Show file tree
Hide file tree
Showing 59 changed files with 916 additions and 439 deletions.
2 changes: 2 additions & 0 deletions Project.toml
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ version = "0.4.39-DEV"
[deps]
ArgCheck = "dce04be8-c92d-5529-be00-80e4d2c0e197"
BangBang = "198e06fe-97b7-11e9-32a5-e1d131e6ad66"
CompositionsBase = "a33af91c-f02d-484b-be07-31d278c5ca2b"
Distributed = "8ba89e20-285c-5b6f-9357-94700520ee1b"
InitialValues = "22cec73e-a1b8-11e9-2c92-598750a2cf9c"
Logging = "56ddb016-857b-54e1-b83d-db4d58db5568"
Expand All @@ -18,6 +19,7 @@ Tables = "bd369af6-aec1-5ad0-b16a-f7cc5008161c"
[compat]
ArgCheck = "1, 2.0"
BangBang = "0.3.27"
CompositionsBase = "0.1"
InitialValues = "0.2.6"
Requires = "0.5, 1.0"
Setfield = "0.3, 0.4, 0.5, 0.6"
Expand Down
3 changes: 1 addition & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,7 @@ know similar concepts in iterator libraries:

```julia
using Transducers
xf = Partition(7) |> Filter(x -> prod(x) % 11 == 0) |> Cat() |> Scan(+)
foldl(+, xf, 1:40)
1:40 |> Partition(7) |> Filter(x -> prod(x) % 11 == 0) |> Cat() |> Scan(+) |> sum
```

However, the protocol used for the transducers is quite different from
Expand Down
9 changes: 7 additions & 2 deletions benchmark/Manifest.toml
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,11 @@ git-tree-sha1 = "c437ba8bb82f5ec9a5d8cb881031ffa2dbe1038c"
uuid = "34da2185-b29b-5c13-b0c7-acf172513d20"
version = "3.6.0"

[[CompositionsBase]]
git-tree-sha1 = "f3955eb38944e5dd0fabf8ca1e267d94941d34a5"
uuid = "a33af91c-f02d-484b-be07-31d278c5ca2b"
version = "0.1.0"

[[ConstructionBase]]
git-tree-sha1 = "a2a6a5fea4d6f730ec4c18a76d27ec10e8ec1c50"
uuid = "187b0558-2788-49d3-abe0-74a17ed4e7c9"
Expand Down Expand Up @@ -254,10 +259,10 @@ deps = ["Distributed", "InteractiveUtils", "Logging", "Random"]
uuid = "8dfed614-e22c-5e08-85e1-65c5234f0b40"

[[Transducers]]
deps = ["ArgCheck", "BangBang", "Distributed", "InitialValues", "Logging", "Markdown", "Requires", "Setfield", "SplittablesBase", "Tables"]
deps = ["ArgCheck", "BangBang", "CompositionsBase", "Distributed", "InitialValues", "Logging", "Markdown", "Requires", "Setfield", "SplittablesBase", "Tables"]
path = ".."
uuid = "28d57a85-8fef-5791-bfe6-a80928e7c999"
version = "0.4.22-DEV"
version = "0.4.37-DEV"

[[UUIDs]]
deps = ["Random", "SHA"]
Expand Down
9 changes: 7 additions & 2 deletions docs/Manifest.toml
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,11 @@ git-tree-sha1 = "c437ba8bb82f5ec9a5d8cb881031ffa2dbe1038c"
uuid = "34da2185-b29b-5c13-b0c7-acf172513d20"
version = "3.6.0"

[[CompositionsBase]]
git-tree-sha1 = "f3955eb38944e5dd0fabf8ca1e267d94941d34a5"
uuid = "a33af91c-f02d-484b-be07-31d278c5ca2b"
version = "0.1.0"

[[ConstructionBase]]
git-tree-sha1 = "a2a6a5fea4d6f730ec4c18a76d27ec10e8ec1c50"
uuid = "187b0558-2788-49d3-abe0-74a17ed4e7c9"
Expand Down Expand Up @@ -330,10 +335,10 @@ deps = ["Distributed", "InteractiveUtils", "Logging", "Random"]
uuid = "8dfed614-e22c-5e08-85e1-65c5234f0b40"

[[Transducers]]
deps = ["ArgCheck", "BangBang", "Distributed", "InitialValues", "Logging", "Markdown", "Requires", "Setfield", "SplittablesBase", "Tables"]
deps = ["ArgCheck", "BangBang", "CompositionsBase", "Distributed", "InitialValues", "Logging", "Markdown", "Requires", "Setfield", "SplittablesBase", "Tables"]
path = ".."
uuid = "28d57a85-8fef-5791-bfe6-a80928e7c999"
version = "0.4.22-DEV"
version = "0.4.37-DEV"

[[TypedTables]]
deps = ["SplitApplyCombine", "Tables", "Unicode"]
Expand Down
1 change: 1 addition & 0 deletions docs/Project.toml
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ Documenter = "e30172f5-a6a5-5a46-863b-614d45cd2de4"
InitialValues = "22cec73e-a1b8-11e9-2c92-598750a2cf9c"
JSON = "682c06a0-de6a-54ab-a142-c8b1cf79cde6"
Literate = "98b081ad-f1c9-55d3-8b20-4c87d4299306"
Logging = "56ddb016-857b-54e1-b83d-db4d58db5568"
Markdown = "d6f4376e-aef5-505a-96c1-9c027394607a"
OnlineStats = "a15396b6-48d5-5d58-9928-6d29437db91e"
Random = "9a3f8284-a2c9-5f02-9a11-845980a1fd5c"
Expand Down
8 changes: 4 additions & 4 deletions docs/src/explanation/comparison_to_iterators.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ transducers. Consider a transducer
```jldoctest filter-map
julia> using Transducers
julia> xf = Filter(iseven) |> Map(x -> 2x);
julia> xf = opcompose(Filter(iseven), Map(x -> 2x));
```

which works as
Expand Down Expand Up @@ -42,8 +42,8 @@ foldl(+, imap(f, filter(iseven, input))) # equivalent
Compare it to how transducers are used:

```julia
foldl(+, Filter(iseven) |> Map(f), input, init=0)
# ________________________
foldl(+, opcompose(Filter(iseven), Map(f)), input, init=0)
# _________________________________
# composition occurs at computation part
```

Expand Down Expand Up @@ -121,7 +121,7 @@ sequence can be processed efficiently. Consider the following
example:

```jldoctest map-filter-cat
julia> xf = Map(x -> 1:x) |> Filter(iseven ∘ sum) |> Cat()
julia> xf = opcompose(Map(x -> 1:x), Filter(iseven ∘ sum), Cat())
foldl(*, xf, 1:10)
29262643200
```
Expand Down
97 changes: 84 additions & 13 deletions docs/src/explanation/glossary.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,25 +33,96 @@ sense also includes other interfaces such as [`start(rf, ::X)`](@ref
Transducers.start) and [`complete(rf, ::X)`](@ref
Transducers.complete).

## Transducer
## [Transducer](@id glossary-transducer)

A transducer transforms a reducing function into a new reducing
function. It is sometimes referred to as a `xf` or `xform`. A
transducer can be composed of many sub-transducers; the syntax in
Transducers.jl is `xf = xf₁ |> xf₂ |> ... |> xfₙ`. The composed
transducers are applied to the "bottom" reducing function from right
to left, i.e., schematically, a new reducing function ``\mathrm{rf}``
is obtained from the "bottom" reducing function ``\mathrm{step}`` by
A transducer in Transducers.jl is a transformation `xf` that

* transforms an iterator with [`xf(itr)`](@ref eduction)
(**iterator transformation**)
* transforms a reducing step function with [`xf'(rf)`](@ref adjoint)
(**reducing function transformation**)

Common variable names for transducers are `xf` and `xform`.

The idea of generalizing the transducer as two kinds of transformation
is due to Jan Weidner [`@jw3126`](https://github.com/jw3126). See the
discussion in
[JuliaFolds/Transducers.jl#67](https://github.com/JuliaFolds/Transducers.jl/issues/67).

### [Iterator transformation](@id glossary-ixf)

As of Transducers.jl 0.4.39, the call overload of [`Transducer`](@ref)
is interpreted as an _iterator transformation_. That is to say, the
iterator transformation using `Base.Iterators`

```jldoctest ixf; setup = :(using Transducers)
julia> ixf₁ = itr -> Iterators.filter(isodd, itr);
```

and the iterator transformation in Transducers.jl

```jldoctest ixf
julia> ixf₂ = Filter(isodd);
```

behaves identically:

```jldoctest ixf
julia> collect(ixf₁(1:10)) == collect(ixf₂(1:10))
true
```

`Filter(isodd)(1:10)` is an [`eduction`](@ref).

### [Reducing function transformation](@id glossary-rfxf)

Transducers.jl 0.4.39 also exposes reducing function (RF)
transformation with [`xf'(rf)`](@ref adjoint) (`adjoint`):

```jldoctest ixf
julia> rf = Filter(isodd)'(+); # equivalent to (acc, x) -> isodd(x) ? acc + x : acc
julia> rf(0, 2) # `2` filtered out
0
julia> rf(0, 1) # `1` not filtered out
1
```

### Transducer in the narrow sense (Clojure)

The transducer as originally
[introduced by Rich Hickey](https://clojure.org/reference/transducers)
is a transformation of reducing step function. Thus, what is referred
to as a transducer ``\mathrm{xf}`` in Clojure and many other languages
is the reducing function transformation `xf'` in Transducer.jl.

Since a transducer in the narrow sense maps a reducing function to a
reducing function, it can be composed with simple function composition
````. When a composite transducer ``\mathrm{xf} = \mathrm{xf}_1
\circ \mathrm{xf}_2 \circ ... \circ \mathrm{xf}_n`` to a "bottom"
reducing function ``\mathrm{rf}_0``, it is processed from right to
left just like normal functions:

```math
\mathrm{rf} =
\mathrm{xf}_1(\mathrm{xf}_2(...(\mathrm{xf}_{n}(\mathrm{step}))))
\mathrm{xf}_1(\mathrm{xf}_2(...(\mathrm{xf}_{n}(\mathrm{rf}_0))))
```

Given a composition `xf₁ |> xf₂`, transducer `xf₂` is said to be the
_inner transducer_ of `xf₁ |> xf₂`. Likewise,
``\mathrm{xf}_2(\mathrm{rf}')`` is an _inner reducing function_ of
``\mathrm{xf}_1(\mathrm{xf}_2(\mathrm{rf}'))``.
which is equivalent to the following forms in Transducers.jl

```julia
rf = xf₁'(xf₂'(...(xfₙ'(rf₀))))
rf = (xf₁' xf₂' ... xfₙ')(rf₀)
rf = (xfₙ ... xf₂ xf₁)'(rf₀)
rf = (xf₁ xf₂ ... xfₙ)(rf₀)
```

### Inner transducer

Given a composition `xf₁' ∘ xf₂'`, transducer `xf₂` is said to be the
_inner transducer_ of `xf₁' ∘ xf₂'`. Likewise,
`xf₂'(rf₀)` is an _inner reducing function_ of `xf₁'(xf₂'(rf₀))`.

## Reducible collection

Expand Down
11 changes: 5 additions & 6 deletions docs/src/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -64,19 +64,19 @@ it would look very familiar to you:
```jldoctest
julia> using Transducers
julia> collect(Map(x -> 2x), 1:3) # double each element
julia> 1:3 |> Map(x -> 2x) |> collect # double each element
3-element Array{Int64,1}:
2
4
6
julia> collect(Filter(iseven), 1:6) # collect only evens
julia> 1:6 |> Filter(iseven) |> collect # collect only evens
3-element Array{Int64,1}:
2
4
6
julia> collect(MapCat(x -> 1:x), 1:3) # concatenate mapped results
julia> 1:3 |> MapCat(x -> 1:x) |> collect # concatenate mapped results
6-element Array{Int64,1}:
1
1
Expand All @@ -91,8 +91,7 @@ Transducers can be composed (without, unlike iterators, referring to
the input):

```jldoctest filter-map
julia> xf = Filter(iseven) |> Map(x -> 2x)
collect(xf, 1:6)
julia> 1:6 |> Filter(iseven) |> Map(x -> 2x) |> collect
3-element Array{Int64,1}:
4
8
Expand All @@ -104,7 +103,7 @@ An efficient way to use transducers is combination with
intermediate lazy object and compiles to a single loop:

```jldoctest filter-map
julia> foldl(+, xf, 1:6)
julia> foldl(+, 1:6 |> Filter(iseven) |> Map(x -> 2x))
24
```

Expand Down
1 change: 0 additions & 1 deletion docs/src/reference/interface.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
## Core interface for transducers

```@docs
Transducers.Transducer
Transducers.AbstractFilter
Transducers.R_
Transducers.inner
Expand Down
6 changes: 6 additions & 0 deletions docs/src/reference/manual.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,12 @@ Transducers.append_unordered!

## Transducers

```@docs
Transducers.Transducer
Base.:∘
Base.adjoint
```

```@autodocs
Modules = [Transducers]
Private = false
Expand Down
1 change: 1 addition & 0 deletions docs/utils.jl
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ EXAMPLE_PAGES = [
"Tutorial: Missing values" => "tutorials/tutorial_missings.md",
"Tutorial: Parallelism" => "tutorials/tutorial_parallel.md",
"Parallel word count" => "tutorials/words.md",
"Upgrade to new `|>` of Transducers.jl 0.4.39" => "howto/upgrade-to-ixf.md",
"Empty result handling" => "howto/empty_result_handling.md",
"Writing transducers" => "howto/transducers.md",
"Writing reducibles" => "howto/reducibles.md",
Expand Down
4 changes: 2 additions & 2 deletions examples/primes.jl
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,10 @@ sieve(xf, x) =
if isnothing(foldl(right, xf, (x,), init=nothing))
nothing, xf
else
x, xf |> Filter(n -> n % x != 0)
x, opcompose(xf, Filter(n -> n % x != 0))
end

prime_xf = ScanEmit(sieve, Map(identity)) |> Filter(!isnothing)
prime_xf = opcompose(ScanEmit(sieve, Map(identity)), Filter(!isnothing))

using Test #src
primes = begin #src
Expand Down
4 changes: 2 additions & 2 deletions examples/reducibles.jl
Original file line number Diff line number Diff line change
Expand Up @@ -41,11 +41,11 @@ collect(Map(identity), vov)
# documentation). In practice, using `@next` means that your
# `__foldl__` supports early termination:

collect(Take(3), vov)
vov |> Take(3) |> collect

# More complex example:

collect(PartitionBy(isequal(1)) |> Zip(Map(copy), Map(sum)), vov)
vov |> PartitionBy(isequal(1)) |> Zip(Map(copy), Map(sum)) |> collect

# Notice that writing [`Transducers.__foldl__`](@ref) is very
# straightforward comparing to how to define an iterator:
Expand Down
6 changes: 3 additions & 3 deletions examples/transducers.jl
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,8 @@ using Transducers: Transducer, R_, next, inner, xform

# ## Stateless transducer

# Let's write manually what `Filter(x -> x isa Int) |> Map(x -> x +
# 1)` would do:
# Let's write manually what `opcompose(Filter(x -> x isa Int), Map(x
# -> x + 1))` would do:

struct AddOneIfInt <: Transducer end

Expand Down Expand Up @@ -111,7 +111,7 @@ end # hide
# Indeed, it picks up some random elements from the past elements.
# With slightly more complex transducer:

collect(Filter(isodd) |> RandomRecall() |> Filter(x -> x > 10) |> Take(5), 1:100)
1:100 |> Filter(isodd) |> RandomRecall() |> Filter(x -> x > 10) |> Take(5) |> collect

# Note that [`Transducers.complete`](@ref) can do more than `unwrap`
# and `complete`. It is useful for, e.g., flushing the buffer.
Expand Down
Loading

0 comments on commit f2ab3d0

Please sign in to comment.