-
-
Notifications
You must be signed in to change notification settings - Fork 5.5k
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
function composition (f∘g) and negation (!f) #17155
Conversation
I hope auto-composition doesn't become the new auto-vectorization. There should be a limited number of auto-composed functions, maybe even only |
You could define |
@TotalVerb I think the plan is only @eschnett I was skeptical at first but it just looks so damn natural - its a thing you might try. I like the look of |
FWIW, I have tests and docstrings for this, but I gotta sleep now. |
I wonder if But the question becomes whether to short-circuit this composition. That's a little tough. Probably best to leave these out. |
@TotalVerb: this is the opposite of the vectorization issue since this allows terse explicit function fusion. Example for using Primes: isprime
julia> filter(isprime, 1:10)
4-element Array{Int64,1}:
2
3
5
7
julia> filter(!isprime, 1:10)
6-element Array{Int64,1}:
1
4
6
8
9
10 Otherwise you have to write this: julia> filter(n->!isprime(n), 1:10)
6-element Array{Int64,1}:
1
4
6
8
9
10 Which is considerably less convenient. I do agree with @TotalVerb that if we allow The notation map(x->f(g(x)), v)
# versus
map(f ∘ g, v) |
The point of where to draw the line on "automatic composition" of functions is a fair one. Why should booleans included but not other kinds of values? An argument for this is that it's very common to want to compose predicates using boolean operations. Edit: I decided that the lazy version is actually the more useful one and implemented that everywhere. |
Just to round this out for the sake of discussion, here are the other boolean ops on functions. |
|
||
!(f::Function) = (x...)->!f(x...) | ||
^(f::Function, g::Function) = (x...)->f(x...) ^ g(x...) | ||
|(f::Function, g::Function) = (x...)->f(x...) || g(x...) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That should be &
and |
for consistency.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is consistently lazy. It's just that in the xor case, lazy and non-lazy are the same.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I guess what @eschnett means is consistency in the sense that f | g
means x -> f(x) | g(x)
etc.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Let me rephrase my request: Since the functions are short-circuiting, their names should be &&
and ||
instead of &
and |
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It is consistent: f | g
means x -> f(x) || g(x)
and f & g
means f(x) && g(x)
and f ^ g
means x -> f(x) ^^ g(x)
except that ^^
is just spelled ^
since you always need to evaluate both arguments to an xor. I decided that the lazy version was likely more useful in general.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@eschnett: that's not possible since those are control flow operators, not functions.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
So maybe better use &
and |
for clarity. As you said, predicates are often quite cheap anyway. The distinction between &
and &&
is already subtle enough for non-professional programmers (i.e. most scientists) that we shouldn't increase confusion.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
f & g
etc. yielding lazy equivalents does seem a bit surprising and magical.
Btw, are we fine with that the boolean operations work only with I guess that adding support for constructors doesn't make much sense, not many constructors return |
That was exactly my thinking, @toivoh – there's no real application for using this with constructors. |
ebe7ab5
to
336dbfd
Compare
OK, now On the other hand, they are a different form of composition to For fun, imagine broadcast over the same input was At least, being able to splat a function like that before composition could be useful? |
I don't see generalizing this as difficult. In the future, such functors might take e.g. the
|
Back to more completely rampant speculation about splatting and broadcasting, could we define call on tuples of functions to be broacasting? Something a bit like: (fs::Tuple{Function,Function})(x...) = (x...) -> (fs[1](x...), fs[2](x...)) # Also any length tuple
# implies:
(& ∘ (f, g))(input) == ((x...) -> f(x...) & g(x...))(input) There's clearly some holes in the above code regarding splatting the outputs into |
Just to clarify my motivation behind the last posts: users might see something like EDIT: Of course, the "general way" could just be defined as using an anonymous function. |
OK, I've got the basic version (no |
It feels odd and ad-hoc to limit this to boolean operations. Why should |
@StevenGL I sympathize... this is why I felt having something like Or we could just overload the hell out of all the mathematical operators for functions. It's a bit sad that won't work for other callables but, like I said earlier, perhaps traits could fix that. Unlike |
This would be bad for user-defined callables, no? Presumably sometimes people would want to apply unary functions to them. OTOH, a simple macro that goes into "function composition mode" might be nice?
Or perhaps you could indicate the input with |
@andyferris, any generic composition like this could be overridden for specific user-defined callables — you could still define higher-order functions. And I don't think this can be done by a macro, since macros don't have access to type information (so they don't know what symbols are functions). In any case, I wasn't seriously advocating for |
I could support that.
Of course. I guess I'm advocating a generic composition interface that "just worked". Then, compositions of callables and actions on callables are distinct: hyperbolic(sin) = sinh
hyperbolic ∘ sin = # something else |
I'd still love to see arbitrary composition happen with |
Just an interesting observation: in #17184 we discovered we sometimes need |
OK great! I'll finalize #19670 then (it's close). |
Should these have been added to NEWS.md? |
Yes. I'll take a pass through everything with the |
Can julia> @benchmark (log∘exp)(5)
BenchmarkTools.Trial:
memory estimate: 0.00 bytes
allocs estimate: 0
--------------
minimum time: 36.720 ns (0.00% GC)
median time: 36.795 ns (0.00% GC)
mean time: 37.107 ns (0.00% GC)
maximum time: 71.426 ns (0.00% GC)
--------------
samples: 10000
evals/sample: 992
time tolerance: 5.00%
memory tolerance: 1.00%
julia> import Base.∘
julia> ∘(::typeof(log), ::typeof(exp)) = float
∘ (generic function with 2 methods)
julia> @benchmark (log∘exp)(5)
BenchmarkTools.Trial:
memory estimate: 0.00 bytes
allocs estimate: 0
--------------
minimum time: 1.533 ns (0.00% GC)
median time: 1.840 ns (0.00% GC)
mean time: 1.825 ns (0.00% GC)
maximum time: 12.163 ns (0.00% GC)
--------------
samples: 10000
evals/sample: 1000
time tolerance: 5.00%
memory tolerance: 1.00%
julia> @benchmark (sin∘acos)(0.5)
BenchmarkTools.Trial:
memory estimate: 0.00 bytes
allocs estimate: 0
--------------
minimum time: 34.137 ns (0.00% GC)
median time: 34.213 ns (0.00% GC)
mean time: 34.562 ns (0.00% GC)
maximum time: 70.001 ns (0.00% GC)
--------------
samples: 10000
evals/sample: 993
time tolerance: 5.00%
memory tolerance: 1.00%
julia> ∘(::typeof(sin), ::typeof(acos)) = x -> sqrt(1 - x*x)
∘ (generic function with 3 methods)
julia> @benchmark (sin∘acos)(0.5)
BenchmarkTools.Trial:
memory estimate: 0.00 bytes
allocs estimate: 0
--------------
minimum time: 1.490 ns (0.00% GC)
median time: 1.784 ns (0.00% GC)
mean time: 1.764 ns (0.00% GC)
maximum time: 12.029 ns (0.00% GC)
--------------
samples: 10000
evals/sample: 1000
time tolerance: 5.00%
memory tolerance: 1.00% Probably this would be even more useful if it were possible to dispatch over methods (thus it would be possible to define composition only for specific methods, without overtaking all methods). |
@giordano Note that such compositions are frequently not semantically correct, as I would say that only if it is a perfect relationship would this definition be OK. For example, |
@TotalVerb Yeah, I was unsure about the actual function to choose for |
I have been using I would guess that in Julia's My preference, and the way I originally planned a PR on this topic, was for E.g. One might define (probably in a package) a |
Are you sure |
Umm... I think |
Won't work for positive numbers either: julia> log(exp(1000))
Inf
julia> abs(1000)
1000 |
I don't think it should be the role of |
@TotalVerb that sounds right Though without an introspectable composition, we are limiting "fast' compositions with the nice |
@andyferris That was not at all my intent. I mean that |
I just learned about this today, and while I like the idea, I find it strange that no-one brought up how the negation part of this PR obfuscates the meaning of |
Yes, adding a note to the documentation would be a great addition :) |
No description provided.