Skip to content
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

Merged
merged 1 commit into from
Jan 4, 2017

Conversation

StefanKarpinski
Copy link
Sponsor Member

No description provided.

@tkelman tkelman added needs tests Unit tests are required for this change needs docs Documentation for this change is required labels Jun 27, 2016
@TotalVerb
Copy link
Contributor

I hope auto-composition doesn't become the new auto-vectorization. There should be a limited number of auto-composed functions, maybe even only !.

@eschnett
Copy link
Contributor

You could define !f as !∘f, which would be more concise. That also raises the question whether !f needs to be defined -- it doesn't save much typing.

@andyferris
Copy link
Member

@TotalVerb I think the plan is only ! for "auto" composition, otherwise the is required.

@eschnett I was skeptical at first but it just looks so damn natural - its a thing you might try. I like the look of map(!isless, a). And the is a little harder to access than !.

@andyferris
Copy link
Member

andyferris commented Jun 28, 2016

FWIW, I have tests and docstrings for this, but I gotta sleep now.

@TotalVerb
Copy link
Contributor

I wonder if & and | could also be auto-"composed". There's no good multi-argument compose, and these look natural too. filter(iseven & isprime, 1:100) seems pretty natural to me, and a little more concise than filter(x -> iseven(x) && isprime(x), 1:100).

But the question becomes whether to short-circuit this composition. That's a little tough. Probably best to leave these out.

@StefanKarpinski
Copy link
Sponsor Member Author

@TotalVerb: this is the opposite of the vectorization issue since this allows terse explicit function fusion.

Example for !f:

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 !isprime it seems to make sense to allow the other boolean operations: f & g, f | g and f ^ g.

The notation f ∘ g is standard for function composition and is also considerably more convenient for functional programming than lambda notation:

map(x->f(g(x)), v)

# versus

map(f  g, v)

@StefanKarpinski
Copy link
Sponsor Member Author

StefanKarpinski commented Jun 28, 2016

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. The short-circuit issue doesn't bother me much since predicates tend to be cheap and if you really need short-circuiting, then you can use a lambda.

Edit: I decided that the lazy version is actually the more useful one and implemented that everywhere.

@StefanKarpinski StefanKarpinski changed the title function negation and composition: !f, f∘g RFC: boolean ops on function, composition (f∘g) Jun 28, 2016
@StefanKarpinski
Copy link
Sponsor Member Author

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...)
Copy link
Contributor

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.

Copy link
Sponsor Member Author

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.

Copy link
Contributor

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.

Copy link
Contributor

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 |.

Copy link
Sponsor Member Author

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.

Copy link
Sponsor Member Author

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.

Copy link
Member

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.

Copy link
Member

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.

@toivoh
Copy link
Contributor

toivoh commented Jun 28, 2016

Btw, are we fine with that the boolean operations work only with Function objects and not general callables which could be almost anything)?

I guess that adding support for constructors doesn't make much sense, not many constructors return Bool values :) (except for functors).

@StefanKarpinski
Copy link
Sponsor Member Author

That was exactly my thinking, @toivoh – there's no real application for using this with constructors.

@andyferris
Copy link
Member

andyferris commented Jun 29, 2016

OK, now &, | and $ are quite interesting. On one hand, for functions that return Bools, what else would they be?

On the other hand, they are a different form of composition to . They broadcast inputs to more than one function. We haven't really thought about how to design for that - general composition in Julia is probably something like a directed acyclic graph (with splatting and mutability added for fun). What would be amazing (and probably impossible) is a way of writing expressing such graphs, in such a way that it is easier than writing an anonymous function. (E.g. even ! ∘ f is better than (x...) -> !f(x...)).

For fun, imagine broadcast over the same input was , so that f ⊙ g == (x...) -> (f(x...), g(x...)). Then maybe thef & g thing would be equivalent to (& ∘ (f ⊙ g)...). Hmm... could get really bad to read, really fast! But still fun to think about.

At least, being able to splat a function like that before composition could be useful?

@andyferris
Copy link
Member

I guess that adding support for constructors doesn't make much sense, not many constructors return Bool values :) (except for functors).

I don't see generalizing this as difficult. In the future, such functors might take e.g. the BooleanFunctor trait:

!(f::Any::BooleanFunctor, g::Any::BooleanFunctor) = (x...) -> !(f(x...), g(x...))
# also &, !, $

@andyferris
Copy link
Member

andyferris commented Jun 29, 2016

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 &, possibly fixed by a new splatting composition operator (maybe that could be (\odot), (\bullet) or ∘... (\circ ...) or something). But it seems interesting.

@andyferris
Copy link
Member

andyferris commented Jun 29, 2016

Just to clarify my motivation behind the last posts: users might see something like map(f & g, a) and want to generalize it to their own functions (replacing the &). I'm not comfortable with "magic" in a computer language, but having the general way of doing it and providing shortcuts for boolean functions seems much more reasonable.

EDIT: Of course, the "general way" could just be defined as using an anonymous function.

@andyferris
Copy link
Member

OK, I've got the basic version (no &, | or $) with test/docs on #17184.

@stevengj
Copy link
Member

stevengj commented Jun 29, 2016

It feels odd and ad-hoc to limit this to boolean operations. Why should !isprime work but not sin + cos? Why shouldn't sin(cos) then return x -> sin(cos(x)), for that matter? Of course, that is probably a can of worms, but that also seems like an argument for not doing this at all.

@andyferris
Copy link
Member

@StevenGL I sympathize... this is why I felt having something like + ∘... (sin, cos) would be a better direction. The disadvantage is that you end up with a whole new language describing compositions!

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 , which is pretty unambiguous, perhaps this other stuff could be left for post-v0.5 with some exploration via packages.

@andyferris
Copy link
Member

Why shouldn't sin(cos) then return x -> sin(cos(x)), for that matter?

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?

map(@compose sin + cos, array_of_angles)
new_function = @compose sin(cos)

Or perhaps you could indicate the input with _, but then it's probably less typing to use an anonymous function!

@stevengj
Copy link
Member

stevengj commented Jun 29, 2016

@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 sin(cos) defining the composed function. My point was that if implicit composition works for boolean functions, it seems like it should work for all functions. And since doing this for all functions seems a bit too crazy, maybe we shouldn't do it for any functions. Singling out the boolean case is strange to me.

@andyferris
Copy link
Member

And since doing this for all functions seems a bit too crazy, maybe we shouldn't do it for any functions. Singling out the boolean case is strange to me.

I could support that.

any generic composition like this could be overridden for specific user-defined callables

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

@ararslan
Copy link
Member

I'd still love to see arbitrary composition happen with even if boolean functions aren't given separate treatment. filter(! ∘ f, x) would be fine with me.

@andyferris
Copy link
Member

Just an interesting observation: in #17184 we discovered we sometimes need (!) ∘ f rather than ! ∘ f. Interaction between consecutive operators might confuse the parser. Possibly is a point in favour for overloading more of these methods, if only for the convenience of dropping the brackets (or train the parser to be smarter, if that is at all possible?).

@andyferris
Copy link
Member

OK great! I'll finalize #19670 then (it's close).

@StefanKarpinski StefanKarpinski merged commit 887815c into master Jan 4, 2017
@StefanKarpinski StefanKarpinski deleted the sk/funcnegcomp branch January 4, 2017 01:47
@yurivish
Copy link
Contributor

yurivish commented Jan 9, 2017

Should these have been added to NEWS.md?

@StefanKarpinski
Copy link
Sponsor Member Author

Yes. I'll take a pass through everything with the new label before we release and make sure it has a NEWS entry.

@giordano
Copy link
Contributor

giordano commented Jan 17, 2017

Can be abused to provide fast methods for composition of functions or it should be considered a bad practice? For example:

julia> @benchmark (logexp)(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 (logexp)(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 (sinacos)(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 (sinacos)(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).

@TotalVerb
Copy link
Contributor

@giordano Note that such compositions are frequently not semantically correct, as log(exp(x)) may, due to rounding, return a different result than float(x).

I would say that only if it is a perfect relationship would this definition be OK. For example, identity and any other function would be fine, but not - and - because (-)((-)(true)) is 1.

@giordano
Copy link
Contributor

@TotalVerb Yeah, I was unsure about the actual function to choose for log∘exp, thought that float was the one that would match in most cases the same type of the return value of log(exp(x)) (consider for example any x<:Real that isn't x<:AbstractFloat: identity would give a different return type, though the result would be most probably more accurate). Anyway, that was just an example of the possible application ;-)

@andyferris
Copy link
Member

andyferris commented Jan 17, 2017

I have been using for a while now with my own types (specifically, Transformation from CoordinateTransforms.jl) and with your own callables it seems reasonable to make your own judgement about what emerges from the composition. For instance, in geometry the composition of two translations is a single translation, but the result of applying that may differ due to floating-point precision/rounding.

I would guess that in Julia's Base library, it would be best not to make too many assumptions about the behavior of a function unless the semantics are very clear. IMO, identity is certainly a lower bound on "clear enough" for simplified composition but there may be other cases worth exploring.

My preference, and the way I originally planned a PR on this topic, was for f ∘ g to create an introspectable callable object, say Composition{typeof(f),typeof(g)}(f,g). That way one can perform a variety of powerful higher-order logic which only gets used when the user knows it's safe.

E.g. One might define (probably in a package) a simplify function, say, so that simplify(f ∘ g) applies certain mathematically precise (but not necessarily numerically precise) rules such as simplify(log ∘ exp) = identity. (Though this is fraught with complications like exp ∘ log only working for positive-definite inputs, etc... anyone who has used Mathematica will be familiar with how complex this can become).

@oscardssmith
Copy link
Member

Are you sure simplify(log ∘ exp) = abs isn't right? when working only in the reals, this is more correct.

@andyferris
Copy link
Member

Are you sure simplify(log ∘ exp) = abs isn't right?

Umm... I think log(exp(-1)) == -1, no?

@johnmyleswhite
Copy link
Member

Won't work for positive numbers either:

julia> log(exp(1000))
Inf

julia> abs(1000)
1000

@TotalVerb
Copy link
Contributor

I don't think it should be the role of to provide more efficiency or numerical accuracy to chained operations. Given more constraints and allowable operations, this may be more appropriate with a fastcompose function, perhaps turned on with @fastmath.

@andyferris
Copy link
Member

@TotalVerb that sounds right

Though without an introspectable composition, we are limiting "fast' compositions with the nice operator/syntax to macros only, rather than also permitting functional approaches.

@TotalVerb
Copy link
Contributor

TotalVerb commented Jan 17, 2017

@andyferris That was not at all my intent. I mean that could perhaps be rewritten with @fastmath to fastcompose, which is allowed to make additional assumptions. Admittedly this is not as general as your proposal.

tkelman pushed a commit that referenced this pull request May 14, 2017
@Sacha0 Sacha0 mentioned this pull request Jan 12, 2018
StefanKarpinski pushed a commit that referenced this pull request Feb 8, 2018
@Jutho
Copy link
Contributor

Jutho commented Oct 29, 2018

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 !==: is it !(==) or the built in equivalent of !(===)? My apologies for reviving an old closed thread. I don't expect anything to happen, except for maybe a note in the documentation?

@StefanKarpinski
Copy link
Sponsor Member Author

Yes, adding a note to the documentation would be a great addition :)

@KristofferC KristofferC removed the needs news A NEWS entry is required for this change label Nov 13, 2018
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet