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

PR: Function composition and negation (a.k.a. "It's like a category, man!") #17119

Closed
wants to merge 0 commits into from

Conversation

andyferris
Copy link
Member

This PR allows the ability to compose functions with compose() or the operator, so that (f ∘ g)(x) = f(g(x)). Furthermore, ! applied to functions is converted to a composition, so !f == ! ∘ f. This enables the syntax sought by @JeffBezanson, where we can write e.g. map(!isless, a, b) instead of explicitly introducing an anonymous function, map((i,j) -> !isless(i,j), a, b).

The composed functions or functors are stored by default in a functor of type ComposedFunctor instead of an anonymous function, and I'm also defining the interface (in the docstrings) to make it acceptable to intercept this. The reason for this is that we can (optionally) then introspect the structure of the composition to apply intelligent optimizations and transformations. As a simple example, we can define that (! ∘ !)(x::Bool) = x, which should already be done by LLVM. A more complex example would be taking a series of dataframes/database/SQL commands and transforming them to the most efficient form before extracting data and computing the result.

Finally, we also define inv(f) as an interface for defining the inverse of functions/functors. So far, I have only implemented inv(identity) = identity, but this is useful interface to use in packages (discussions about domain issues in things like inv(sin) == asin can/should be done separately to this PR). In particular, our CoordinateTransformations.jl package already uses this extensively and it is extremely useful (and well-defined).

PS - this is my first PR to Julia, and I don't know how to do squashing on GitHub (I read it is available when the PR is merged). Please let me know if I've done something else wrong.

PPS - thanks for JuliaCon 2016!!

@andyferris
Copy link
Member Author

andyferris commented Jun 25, 2016

Oh, and the other "interesting" feature is an ability to perform splatting, using the splat functor (singleton instance of SplattingFunctor). This allows (f ∘ splat ∘ g)(x...) = f(g(x...)...). Unfortunately ... is not an object or symbol I can hook onto here.

The alternative was to implement a separate, splatting composition. Although this is a great idea, I tried it and I found it to be more confusing to use and much less clean than the current implementation. To make it easily usable, yet another unicode symbol would be needed, or alternatively take over some syntax like:

(f ∘ g...)

@tkelman tkelman added the needs tests Unit tests are required for this change label Jun 25, 2016
@StefanKarpinski
Copy link
Member

I think I'd be ok with this without the splatting and the functor parts. Of course, that boils down to about two definitions:

(f, g) = (x...)->f(g(x...))

!(f::Function) = (x...)->!f(x...)

@andyferris
Copy link
Member Author

Hmm, yes, I see your point! And thanks for the review. Indeed, the ugliest part was splat, and it probably deserves some more thought (e.g. functions can take multiple inputs so you really have an entire diagram of possible generic compositions).

Regarding functors - is this a "less is more" approach to Base, or a "this needs more thought and 0.5 is due really soon" or a "I just prefer anonymous functions" thing?

I'm still planning on overloading for my own purposes because I want to do things along the lines of:

inv(f ∘ g) = inv(g) ∘ inv(f)

which I thought was particularly beautiful for a programming language. (You obviously loose this, and similar abilities, with anonymous functions). But there is no reason packages outside of Base can't implement different-but-semantically-similar methods of composition.

Final question, would you include the ASCII alternative compose() function, or just skip it entirely?

@oxinabox
Copy link
Contributor

oxinabox commented Jun 26, 2016

e.g. functions can take multiple inputs so you really have an entire diagram of possible generic compositions

See the discussion of #5571 (Which is the currently 14th most commented julia issue of all time).
on MIMO function composition, and syntax there of.

Also, for SISO functions, this composition syntax, is arguebaly plain better than the pipelining syntax.
Though I'm not sure how it would read for long chains of composition.
It is certainly more general.

Consider the equivalents, when actually evaluated at a point

  • g(f(x)
  • x |> f |>g
  • (g ∘ f)(x)
  • x |> g ∘ f (Is that correct bracketting?)
  • f(x) |> g

@andyferris
Copy link
Member Author

Thanks @oxinabox for reminding me of #5571. I just put in my two cents there #5571 (comment).

I guess what I like about more than |> is that I'm left with an object I can reuse and call multiple times. I haven't looked at relative order of and |> ( is multiply-level, and typically needs brackets).

@bramtayl
Copy link
Contributor

bramtayl commented Jun 26, 2016

I have a macro to do this in ChainMap.jl, @l. For example, @l f g or @l f !, or even @l -(_, 1) +(2)

@ararslan
Copy link
Member

ararslan commented Jun 27, 2016

I don't know how to do squashing on GitHub (I read it is available when the PR is merged)

Yes indeed! Whoever merges this can choose "merge and squash." I would have recommended making a PR from a feature branch rather than master though. Next time 😄

Nice ideas here by the way, I've been wanting map(!f, x) for quite a while.

@StefanKarpinski
Copy link
Member

StefanKarpinski commented Jun 27, 2016

Here's the minimal alternative: #17155. Maybe not desirable since it may make using f∘g the way you want to with functors harder.

@andyferris
Copy link
Member Author

Cool, Stefan. I actually have those lines plus docstrings and tests on my laptop, but unfortunately I wasn't able to post them before the long flight to Australia.

And now I'm swimming in children and dead tired, so I might get around to adding them later.

Maybe not desirable since it may make using f∘g the way you want to with functors harder.

In my docstrings, I described f ∘ g as doing anything functionally equivalent to (x...) -> f(g(x...)). I think that pretty much defines the interface and lets people do functors with introspection and optimizations when they need to, and do the obvious thing otherwise.

I don't know - would you want more complex behavior in Base? I sort of see its role as providing semantic consistency among the language's users, and IMO "functionally equivalent" is good enough for that.

@andyferris
Copy link
Member Author

Moved to #17184

@tkelman tkelman removed the needs tests Unit tests are required for this change label Jun 29, 2016
@Sacha0 Sacha0 mentioned this pull request Jan 12, 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.

6 participants