-
Notifications
You must be signed in to change notification settings - Fork 7
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
making things cleaner, more transparent, more consistent: road to 1.0 #22
Comments
Is the overload of xf'
xf'(rf₁) is a shortcut for calling reducingfunction(xf, rf₁).
More precisely, adjoint xf′ of a transducer xf is a reducing function transform rf₁ -> rf₂. That is
to say, xf' a function that maps a reducing function rf₁ to another reducing function rf₂.
Examples
≡≡≡≡≡≡≡≡≡≡
julia> using Transducers
julia> y = Map(inv)'(+)(10, 2)
10.5
julia> y == +(10, inv(2))
true |
@ExpandingMan I'm sorry for the over a month late reply, life has been kinda hectic lately. Yeah, I think you're raising some great points here and I'd like if we could start moving forward with these ideas. Regarding the return type stuff, a lot of this was designed by Takafumi as an exploration into how one can design efficient and flexible algorithms like this without relying on compiler internals. I think he did an impressive job at that, but ultimately I think we should probably start taking advantage of the compiler's capabilities, since as you say it could simplify a lot of code, and make it more robust. |
@jariji yes, there is some (category theory) math going on there with The idea is that a transducer is a function which takes in an iterable sequence and spits out a transformed iterator sequence. An adjoint-transducer is a a function which takes in a reducing function and spits out a transformed reducing function. These follow an adjoint relationship in the sense that fold(rf, xf(itr)) == fold(xf'(rf), itr) which should be thought of in the same sense as how for vectors u' * v == v' * u |
I haven't been playing with this lately, as I was hoping to hear some feedback before I put a lot of work into a big PR, however I remain heavily invested in this package and have continued to gradually use it more and more. Transducers + BangBang + (other important dependencies) has become quite big, so I'm thinking of trying out my ideas in an experimental "MiniTransducers" package where I can write stuff from scratch, evaluate whether I have any good ideas, and later port them to Transducers which is potentially a big job on its own.
I didn't respond to this earlier because I didn't immediately want to get this thread way off track, but yes, this is related to the adjoint in a fairly straightforward way, category theory not necessarily required. The usual adjoint in linear algebra takes an operator on a vector space and makes it an operator on the dual space, which is a space of "reducing" functions (in that it maps |
One further thought I've had about this recently is that we should have better ways of handling / preserving the shape and type of input data. This is currently pretty ad-hoc and random in Transducers. For instance: julia> collect([1 2 ; 3 4] |> Map(sqrt))
2×2 Matrix{Float64}:
1.0 1.41421
1.73205 2.0 versus julia> collect(StructArray([1+im 2 ; 3 4-im]) |> Map(sqrt))
4-element Vector{ComplexF64}:
1.09868411346781 + 0.45508986056222733im
1.7320508075688772 + 0.0im
1.4142135623730951 + 0.0im
2.015329455153383 - 0.24809839340235612im and also julia> collect((1,2,3,4) |> Map(sqrt))
4-element Vector{Float64}:
1.0
1.4142135623730951
1.7320508075688772
2.0 We'll probably want some "data style" traits or something for informing Transducers about how it can handle input data. |
What behavior do you expect here? |
That's true. Maybe a new function would be appropriate to replace the way currently use Something that'd be kinda weird but almost makes sense would be map(Map(f) ⨟ Filter(cond), itr) could use the Transducers system. But that looks really weird, so maybe it's best to make our own name, e.g. each(itr |> Map(f) |> Filter(cond)) (And also have keyword arguments for things like which backend, I.e. serial, multithreaded, distributed, etc) |
Update/correction: JuliaLang/julia#50051 (comment)
|
In regard to shape, I don't feel I know what the right answer is, but I do find it super annoying that there's basically no way to do a shape-preserving operation with transducers. Frankly I don't think Julia's |
See also JuliaLang/julia#36537 |
The other thing I want to focus on if we make a push for a 1.0 is trimming down the library of transducers a lot, or implementing some major fixes. I think if we do a major refactor we should basically just start out of |
I've started a repo: https://github.com/JuliaFolds2/TransducersNext.jl where I'm playing with ideas for what's next. This repo is much less featureful than the current transducers.jl but what is here I think works pretty well. Lets discuss individual design questions there |
I have been doing some work on the package lately and have some thoughts on what needs
improving.
Problems
Here are, as I see it, some problems with the package in its current state.
API Entropy
There are many different ways of doing the same thing which are in some cases distinct in arbitrary
and confusing ways, while in other cases are subtly but curcially different. For example, the
package exports
transduce
as well asfoldxl
and adds methods tofoldl
.transduce
andfoldxl
differ primarily in thattransduce
may return aReduced
, whilefoldxl
andfoldl
arelargely equivalent (I don't think they have exactly the same methods but it's hard to even tell).
Opacity of Folds
Functions such as
copy
andcollect
arguably don't look like one would reasonably expect based onthe fundamentals such as
foldxl
. For example, it seems reasonable to expect something very muchlike
The truth is much more complicated than this, for good reason. I am not suggesting that we
compromise the convenience of functions such as
collect
, however I consider it a problem whenthese functions rely on complicated opaque voodoo.
The catch here is that, for obvious performance reasons, one should not simply
append!!
separatearrays, instead Transducers figures out that it can allocate an
undef
Vector
and insert objectsinto it. This situation is greatly complicated by the need to infer a container type prior to
executing the fold, which Julia itself doesn't provide an API for handling (more on this in a
moment).
In my view, this issue is of more than mere aesthetic interest. It leads to a lot of behavior which
reasonable users might not expect, for example, the following 3 lines all error on latest
main
:Too Much Dependence on Complicated Type Inference Voodoo
I am worried code using Transducers.jl could easily become too difficult to optimize, and due to use
of undocumented Julia internals might degrade over time. The crux of this is a method of
transduce
used in practically every invocation of Transducers.jl which one immediately runs intowhen analyzing code with JET.jl, found
here.
In particular, there can be type instability arising from
convert(realtype, ur_result)
. This isan extremely difficult problem to address because it is more of a fundamental problem with Julia
than Transducers.jl in particular. The issue is of course that Julia does not expose its type
inference machinery, so calls to
Core.Compiler.return_type
may be unavoidable for the crucial usecase of inferring an appropriate container type before folding is executed.
Threaded Functions are Inflexible
Threaded functions, i.e. those based on
foldxt
, rely on being provided an associative reductionstep. This is reasonable as a default, but unfortunately there is no way to check whether a step is
associative a priori, which can lead to undefined behavior. From a user perspective, this can lead
to some rather confusing results when exchanging
foldxl
forfoldxt
. The associative requirementcomes from the need for
foldxt
to know how to combine results from each thread, but it currentlydoesn't allow for any way of specifying non-associative operations in the thread-local case in
addition to a way to combine those results, meaning that users essentially need to write the
threading code "from scratch" if attempting this. Note that non-associative operations such as
push!!
are quite common.Possible Solutions
I propose that
transduce
be made private (this was suggested by TKF in the code) and that packageinternals should be rewritten where appropriate, and users should be instructed to rely on
(or something very similar) as a "fundamental" method. I'm not proposing that frequently used
convenience functions such as
collect
,copy
and their threaded equivalents be discarded, butthey should be related to
foldxl
in a more transparent and understandable way.One of the prerequisites for achieving this is a more accessible, transparent way of dealing with
cases where, instead of e.g. combining containers with an associative step such as
append!!
, oneinstead needs to insert results into a pre-allocated container. To that end, I propose that the API
be expanded to allow for something like this
Granted, I'm not yet entirely confident that such an interface is feasible, but if it is it seems
greatly preferrable to the current state of affairs in which the only equivalent to such a call
involves lots of rather inscrutible internals; surely highly undesirable for such a simple and
common use case. Achieving this would likely require a bit of reworking of BangBang.jl. Note that
that the concept of default initialization already exists to some extent, for example there is
Init(op)
, but it is not applied uniformly in Transducers.jl internals.As for
foldxt
, I believe it should be based on a method something likeor perhaps something even more fundamental. We could then create additional methods, more similar
to the current methods with reasonable defaults.
Lastly, while I don't know if it's possible to clean up the type inference problems in a consistent
way, I think we should at least try. For one, I believe the notorious
transduce
method withCore.Compiler.return_type
was written quite a few minor versions of Julia ago. We might also beable to branch the behavior at compile time so that it does "normal stuff" by default and falls back
to
Core.Compiler.return_type
when there is no better option. It might also be worth describingthe issue to some of the core Julia people in detail in the hope of getting useful advice.
The text was updated successfully, but these errors were encountered: