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

add one arg function composition #34251

Merged
merged 7 commits into from
Jan 13, 2020
Merged

Conversation

jw3126
Copy link
Contributor

@jw3126 jw3126 commented Jan 3, 2020

@ararslan ararslan added needs compat annotation Add !!! compat "Julia x.y" to the docstring needs news A NEWS entry is required for this change labels Jan 3, 2020
@@ -856,6 +859,8 @@ julia> ∘(fs...)(3)
3.0
```
"""
∘() = identity
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wonder if this is the best way. For example, ∘(f, g, ∘()) may not work out-of-the-box if f and g are generic morphisms like lenses or transducers. It's the same reason why we don't have +() or *(). OTOH, it's nice that we have a canonical identity for . I guess I'd be less worried if

(f, ::typeof(identity)) = f
(::typeof(identity), f) = f
(::typeof(identity), ::typeof(identity)) = identity

are implemented as well.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That is a very good observation. My favored solution would be dropping ∘() and adding your remark as a comment I think.

Copy link
Contributor Author

@jw3126 jw3126 Jan 8, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Or perhaps ∘() = error(your remark).

Copy link
Sponsor Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's based on the desire to have for other "morphisms" than functions, which has been requested before, e.g. in the discussion where compose was requested as an ASCII name for .

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Adding only ∘(f) sounds good. Adding things is much easier than removing things.

Copy link
Sponsor Member

@StefanKarpinski StefanKarpinski Jan 9, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would also be fine with just adding the definitions you proposed:

∘(f, ::typeof(identity)) = f
∘(::typeof(identity), f) = f
∘(::typeof(identity), ::typeof(identity)) = identity

So I guess we have two options: less or more.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree. I'm also fine with both choices.

I actually like adding that three-line definition with ::typeof(identity). It makes writing morphisms slightly easier. But this can happen independently of ∘(). If we need some excuse for adding that three-line definition, and if ∘() is enough for that, then I'd vote for adding ∘(). Yeah, that's a bit 😈

Copy link
Contributor Author

@jw3126 jw3126 Jan 9, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

While I can live with both choices, here are some arguments why less is more:

I take lens composition as an example, but this applies to transducers and probably most kinds of morphisms.

  • When I write lens = ∘(lenses...) I expect that I can use the lens api on the result. Like get(obj, lens). This is impossible to achieve for the empty case. Now defining ∘() = identity (along with @tkf clever extra methods say) means that some part of the lens will work e.g ∘(lens, ∘()) and others will not.
    I would favor if everything fails at composition time with a sane error and not having some stuff work and other stuff throw a latent error about using lens api on identity.
  • Morphisms often ship their own identity (since you want an identity of type ::Lens). In order to make things work, one must also define e.g.
(::IdenityLens, typeof(()))
(typeof(()), ::IdenityLens)

Not the end of the world, but having two identities is an extra stumbling block package authors need to keep in mind.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These are very good points. I'm now against ∘().

Copy link
Sponsor Member

@StefanKarpinski StefanKarpinski Jan 9, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok, so let's go with the less option then: remove the ∘() method and fix up the docs to correspond to that and this should be good to go.

# While `∘() = identity` is a reasonable definition for functions, this
# would cause headaches for composition of user defined morphisms.
# See also #34251
msg = """Empty composition ∘() is undefined."""
Copy link
Sponsor Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why define it at all then? Why not just let it be a method error?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So that we don't get a stream of PRs that want to define it 😄

Copy link
Sponsor Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it's better to leave it undefined.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see the benefit of making it very clear that not defining it is intentional with this approach. But I think leaving a comment is enough to prevent future PRs.

@StefanKarpinski
Copy link
Sponsor Member

There seems to be trailing whitespace.

@StefanKarpinski StefanKarpinski merged commit 09d617a into JuliaLang:master Jan 13, 2020
@StefanKarpinski
Copy link
Sponsor Member

Thanks for sticking this process out! Who knew this would require so much discussion?

KristofferC pushed a commit that referenced this pull request Apr 11, 2020
@azev77
Copy link
Sponsor

azev77 commented May 28, 2020

Is it possible to have (∘^n)(f)(x)?
(∘^0)(f)(x) = identity(x) = x
(∘^1)(f)(x) = f(x)
(∘^n)(f)(x) = (f∘ f∘...∘f)(x)

Right now it can be done recursively:

h(x) = x^2 - x + 1
hh(x, n) = (n == 0) ? x : (hhh)(x, n-1)
h(2), h(h(2)), h(h(h(2)))
(h)(2), (hh)(2), (hhh)(2)
hh(2,1), hh(2,2), hh(2,3)
hh(2,4)

While we're here can we return to multivariate function composition?
@oxinabox does multivar pipe: @pipe a |> b(x,_) # == b(x,a)
Why not: (f(a,_) ∘ g)(x) = f(a,g(x)) ?

@jw3126
Copy link
Contributor Author

jw3126 commented May 29, 2020

@azev77 thats a nice idea. (f∘ f∘...∘f)(x) is an operation that comes up every now and then and in any language I tried so far I never found a name for it that I really liked. That is until I read your suggestion.

There are some good arguments against defining (∘^0) see above, but the rest seems fine.

@oxinabox
Copy link
Contributor

can we correcrt the name of this PR?

@jw3126
Copy link
Contributor Author

jw3126 commented May 29, 2020

@azev77 thats a nice idea. (f∘ f∘...∘f)(x) is an operation that comes up every now and then and in any language I tried so far I never found a name for it that I really liked. That is until I read your suggestion.

There are some good arguments against defining (∘^0) see above, but the rest seems fine.

Thinking about it some more, I am not sure I like the name anymore.

(∘^2) = ()()
(∘^3) = ()()()
...

would be another plausible meaning for ∘^n. While (∘)∘(∘)∘(∘) is crazy and useless in Julia, it is actually a useful pattern in e.g. Haskell.

@jw3126 jw3126 changed the title add zero and one arg function composition ~add zero and~ one arg function composition May 29, 2020
@jw3126 jw3126 changed the title ~add zero and~ one arg function composition ~~add zero and~~ one arg function composition May 29, 2020
@jw3126 jw3126 changed the title ~~add zero and~~ one arg function composition add one arg function composition May 29, 2020
@azev77
Copy link
Sponsor

azev77 commented May 29, 2020

@jw3126 there might be other options, but we need something for function iteration.
A dynamical system is defined to be an iterated function...
This would make it easier to code famous examples such as the mandelbrot set: https://discourse.julialang.org/t/a-call-for-community-supplied-julia-code-examples/38490/5?u=albert_zevelev

In Mathematica: Nest[f, x, 3]= f[f[f[x]]] & NestList[f, x, 2] = {x, f[x], f[f[x]]}
btw Mathematica allows iteration of multivariate functions!

Given a function h It might be closer to math to have h^n=(h∘ h∘...∘h)

h(x) = x^2 - x + 1
h^2(x) = h(h(x))
h^n(x)=(h h...h)(x)

@StefanKarpinski
Copy link
Sponsor Member

I’m ok with f^n for f iterated n times. That sort of implies that f*g should be function composition but there’s an ambiguity there since that could also mean x -> f(x)*g(x). There a slight worry that f^n could mean x -> f(x)^n.

@azev77
Copy link
Sponsor

azev77 commented May 29, 2020

I see your point, ^ is generalization of *.
But every dynamical systems book I've ever read defines f^n(x)=(f∘f^n-1)(x)
Even the wiki page on iterated function defines it that way.
Maybe Nest[f,x,n] might also be a good idea?

@StefanKarpinski
Copy link
Sponsor Member

Yes, that's why I'm somewhat inclined to define it for function iteration.

@tkf
Copy link
Member

tkf commented May 29, 2020

I think defining f^n as function iteration is a bad idea because x^n becomes ambiguous when x is a callable and * is defined for it. That is to say, we can't define a reasonable behavior for ^(::Any, Integer). Defining ^(::Function, Integer) is also not great since anything can be a callable in Julia.

@jw3126
Copy link
Contributor Author

jw3126 commented May 30, 2020

One notation I occasionally see and like for iterating operations other then * is:

x^⊗3 = x  x  x
f^∘2 = f  f

I don't think its worth to change the parser for this, but maybe we could have

^(op, x, n) = ...
^(x, n) = ^(*, x, n)
^(, f, 3) = f  f  f # or some variant type stable in n

In theory one could also overload ^(x, (op, n)::Tuple{Any, Int}) to enable x^(⊗, n) notation, but I don't think we should.

@tkf
Copy link
Member

tkf commented May 31, 2020

I started to wonder if f^n actually makes sense if we have (M::AbstractMatrix)(x::AbstractVector) = M * x and (a::Number)(b::Number) = a * b. It also sounds kind of useful to write A ∘ B ∘ C * x to do A * (B * (C * x)) if the matrices A, B, and C are large.

In this scenario, f^n would be defined as a best-effort "eager" computation of f ∘ f ∘ ... ∘ f. For matrices, such eager computation is reduced to the usual multiplication (and extended to complex). For a generic object, the fallback is

f^(n::Integer) = x0 -> foldl((x, _) -> f(x), 1:n; init = x0)

If n is a small literal number, using also makes sense.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
needs compat annotation Add !!! compat "Julia x.y" to the docstring needs news A NEWS entry is required for this change
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

7 participants