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
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions NEWS.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ New library functions

New library features
--------------------
* Function composition now works also on zero or one arguments `∘()=identity; ∘(f) = f` (#34251)


Standard library changes
Expand Down
5 changes: 5 additions & 0 deletions base/operators.jl
Original file line number Diff line number Diff line change
Expand Up @@ -837,6 +837,9 @@ and splatting `∘(fs...)` for composing an iterable collection of functions.
!!! compat "Julia 1.4"
Multiple function composition requires at least Julia 1.4.

!!! compat "Julia 1.5"
Composition of zero or one functions requires at least Julia 1.5.

# Examples
```jldoctest
julia> map(uppercase∘first, ["apple", "banana", "carrot"])
Expand All @@ -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
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
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
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.

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

Expand Down
18 changes: 18 additions & 0 deletions test/operators.jl
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,25 @@ Base.promote_rule(::Type{T19714}, ::Type{Int}) = T19714
@test ∘(x -> x-2, x -> x-3, x -> x+5)(7) == 7
fs = [x -> x[1:2], uppercase, lowercase]
@test ∘(fs...)("ABC") == "AB"

@test ∘()(0) === 0
@test ∘(x -> (x, 1))(0) === (0, 1)
@test ∘(x -> (x, 2), x -> (x, 1))(0) === ((0, 1), 2)
@test ∘(x -> (x, 3), x -> (x, 2), x->(x,1))(0) === (((0, 1), 2), 3)
@test ∘(x -> (x, 4), x -> (x, 3), x->(x,2), x-> (x, 1))(0) === ((((0, 1), 2), 3), 4)

# test that user defined functors only need to overload the two arg version
struct FreeMagma
word
end
Base.:(∘)(a::FreeMagma, b::FreeMagma) = FreeMagma((a.word, b.word))

@test ∘(FreeMagma(1)) === FreeMagma(1)
@test ∘(FreeMagma(1), FreeMagma(2)) === FreeMagma((1,2))
@test ∘(FreeMagma(1), FreeMagma(2), FreeMagma(3)) === FreeMagma(((1,2), 3))
@test ∘(FreeMagma(1), FreeMagma(2), FreeMagma(3), FreeMagma(4)) === FreeMagma((((1,2), 3), 4))
end

@testset "function negation" begin
str = randstring(20)
@test filter(!isuppercase, str) == replace(str, r"[A-Z]" => "")
Expand Down