diff --git a/NEWS.md b/NEWS.md index 139c9238de7f1..a28b48b0e35be 100644 --- a/NEWS.md +++ b/NEWS.md @@ -23,6 +23,7 @@ New library functions New library features -------------------- +* Function composition now works also on one argument `∘(f) = f` (#34251) Standard library changes diff --git a/base/operators.jl b/base/operators.jl index 049fdf3410273..cd673f791e527 100644 --- a/base/operators.jl +++ b/base/operators.jl @@ -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 one function ∘(f) requires at least Julia 1.5. + # Examples ```jldoctest julia> map(uppercase∘first, ["apple", "banana", "carrot"]) @@ -856,6 +859,8 @@ julia> ∘(fs...)(3) 3.0 ``` """ +function ∘ end +∘(f) = f ∘(f, g) = (x...)->f(g(x...)) ∘(f, g, h...) = ∘(f ∘ g, h...) diff --git a/test/operators.jl b/test/operators.jl index 58102d7d65a32..64c0ebab5ab37 100644 --- a/test/operators.jl +++ b/test/operators.jl @@ -111,7 +111,30 @@ 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" + + # Like +() and *() we leave ∘() undefined. + # While `∘() = identity` is a reasonable definition for functions, this + # would cause headaches for composition of user defined morphisms. + # See also #34251 + @test_throws(MethodError, ∘()) + + @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]" => "")