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^n for iterated functions #39042

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all 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 base/exports.jl
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ export
IOStream,
LinRange,
Irrational,
IteratedFunction,
Matrix,
MergeSort,
Missing,
Expand Down
62 changes: 55 additions & 7 deletions base/operators.jl
Original file line number Diff line number Diff line change
Expand Up @@ -559,15 +559,15 @@ extrema(f, x::Real) = (y = f(x); (y, y))
"""
identity(x)

The identity function. Returns its argument.
The identity function. Returns its argument. Any keyword arguments are ignored.

# Examples
```jldoctest
julia> identity("Well, what did you expect?")
"Well, what did you expect?"
```
"""
identity(x) = x
identity(x; kws...) = x

+(x::Number) = x
*(x::Number) = x
Expand Down Expand Up @@ -931,7 +931,7 @@ julia> fs = [
julia> ∘(fs...)(3)
3.0
```
See also [`ComposedFunction`](@ref).
See also [`ComposedFunction`](@ref) and [`IteratedFunction`](@ref).
"""
function ∘ end

Expand All @@ -940,9 +940,9 @@ function ∘ end

Represents the composition of two callable objects `outer::Outer` and `inner::Inner`. That is
```julia
ComposedFunction(outer, inner)(args...; kw...) === outer(inner(args...; kw...))
ComposedFunction(outer, inner)(args...; kw...) === outer(inner(args...; kw...); kw...)
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 don't think this is the right definition. Arguments to the composed function should only be passed to inner. If it were possible for inner to return something that caused keyword arguments to be passed to outer, then that would be the right thing to do. Since that isn't possible, the outer function should never be passed keyword arguments.

The situation is similar to that with varargs: the inner function can take any number of arguments, but the outer function can only accept the inner function's returned value.

```
The preferred way to construct instance of `ComposedFunction` is to use the composition operator [`∘`](@ref):
The preferred way to construct an instance of `ComposedFunction` is to use the composition operator [`∘`](@ref):
```jldoctest
julia> sin ∘ cos === ComposedFunction(sin, cos)
true
Expand All @@ -962,7 +962,7 @@ julia> composition.inner === cos
true
```
!!! compat "Julia 1.6"
ComposedFunction requires at least Julia 1.6. In earlier versions `∘` returns an anonymous function instead.
`ComposedFunction` requires at least Julia 1.6. In earlier versions `∘` returns an anonymous function instead.

See also [`∘`](@ref).
"""
Expand All @@ -973,7 +973,7 @@ struct ComposedFunction{O,I} <: Function
ComposedFunction(outer, inner) = new{Core.Typeof(outer),Core.Typeof(inner)}(outer, inner)
end

(c::ComposedFunction)(x...) = c.outer(c.inner(x...))
(c::ComposedFunction)(x...; kws...) = c.outer(c.inner(x...; kws...); kws...)

∘(f) = f
∘(f, g) = ComposedFunction(f, g)
Expand All @@ -985,6 +985,54 @@ function show(io::IO, c::ComposedFunction)
show(io, c.inner)
end

"""
IteratedFunction{F} <: Function

`IteratedFunction(f,n)` represents the function `f` iterated `n ≥ 0` times on
its input, which must be a single argument.

That is, for functions `f(x)`, it represents the function
`x -> f(f(f(f(...(f(x))))))` iterated `n` times`. Any keyword arguments
are passed through to all calls.

If `f isa Function`, you should normally use the construction `f^n`
to form an `IteratedFunction`. If `n` is a literal integer, `f^n`
may construct a more specialized object, e.g. `f^1 == f`, `f^0 == identity`,
and `f^2 == f ∘ f`.

!!! compat "Julia 1.7"
`IteratedFunction` and `f^n` require at least Julia 1.7.

See also [`∘`](@ref) and [`ComposedFunction`](@ref).
"""
struct IteratedFunction{F} <: Function
f::F
n::Int
Copy link
Member

Choose a reason for hiding this comment

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

Maybe this should be a type parameter instead, so application can be type stable?

Copy link
Member Author

Choose a reason for hiding this comment

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

I thought about that — it depends on how this is used. If n is almost always a hard-coded literal value, then a type parameter is definitely the way to go. On the other hand, if people use an n determined dynamically a lot, it would create a lot of dynamic dispatch.

Copy link
Member Author

Choose a reason for hiding this comment

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

Maybe one could get the best of both words? Use n::TN where TN is Val{N} if a literal argument was used, and where TN is Int otherwise?

Copy link
Member

Choose a reason for hiding this comment

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

Is it really that common to construct a lot of these with dynamic n? The uses I had in mind was mostly with higher order functions like map, where the cost of calling the functor is much more important than that of constructing and dynamically passing it. Making this dependent on literal_pow sounds interesting, but I can still imagine use cases, where n is not a literal, but will still pretty much always constant propagate. I am also not 100% sure whether we want to make literal_pow more special, since it's not an uncommon source of confusion already.

IteratedFunction{F}(f, n::Integer) where {F} = new{F}(f, _check_nonnegative(n))
IteratedFunction(f, n::Integer) = new{Core.Typeof(f)}(f, _check_nonnegative(n))
end
_check_nonnegative(n::Integer) = n ≥ 0 ? n : throw(ArgumentError("$n is not ≥ 0"))
^(f::Function, n::Integer) = IteratedFunction(f, n)
^(fn::IteratedFunction, n::Integer) = IteratedFunction(fn.f, fn.n * n)
literal_pow(::typeof(^), f::Function, ::Val{0}) = identity
literal_pow(::typeof(^), f::Function, ::Val{1}) = f
literal_pow(::typeof(^), f::Function, ::Val{2}) = f ∘ f
literal_pow(::typeof(^), f::Function, ::Val{3}) = f ∘ f ∘ f
literal_pow(::typeof(^), f::Function, ::Val{4}) = f ∘ f ∘ f ∘ f
literal_pow(::typeof(^), fn::IteratedFunction, ::Val{2}) = IteratedFunction(fn.f, fn.n * 2)
literal_pow(::typeof(^), fn::IteratedFunction, ::Val{3}) = IteratedFunction(fn.f, fn.n * 3)
literal_pow(::typeof(^), fn::IteratedFunction, ::Val{4}) = IteratedFunction(fn.f, fn.n * 4)
function (fn::IteratedFunction)(x; kws...)
for i in Base.OneTo(fn.n)
x = fn.f(x; kws...)
end
return x
end
function show(io::IO, fn::IteratedFunction)
show(io, fn.f)
print(io, "^", fn.n)
end

"""
!f::Function

Expand Down
1 change: 1 addition & 0 deletions base/show.jl
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ function show(io::IO, ::MIME"text/plain", f::Function)
end

show(io::IO, ::MIME"text/plain", c::ComposedFunction) = show(io, c)
show(io::IO, ::MIME"text/plain", c::IteratedFunction) = show(io, c)

function show(io::IO, ::MIME"text/plain", iter::Union{KeySet,ValueIterator})
isempty(iter) && get(io, :compact, false) && return show(io, iter)
Expand Down
29 changes: 29 additions & 0 deletions test/operators.jl
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,35 @@ Base.promote_rule(::Type{T19714}, ::Type{Int}) = T19714
@test sprint(show, "text/plain", uppercase ∘ first) == "uppercase ∘ first"
end

@testset "function iteration" begin
@test (sin^0)(1.2) == 1.2
@test (sin^1)(1.2) == sin(1.2)
@test (sin^2)(1.2) == sin(sin(1.2))
@test (sin^3)(1.2) == sin(sin(sin(1.2)))
@test (sin^4)(1.2) == sin(sin(sin(sin(1.2))))
@test (sin^5)(1.2) == sin(sin(sin(sin(sin(1.2)))))
@test sin^0 == identity
@test sin^1 == sin
@test sin^2 == sin ∘ sin
@test repr(sin^8) == "sin^8" == repr("text/plain", sin^8)
@test (sin^8)^2 == sin^16
@test (sin^8)^10 == sin^80
let f(x; y) = x + y
@test (f^0)(0; y=3) == 0
@test (f^1)(0; y=3) == 3
@test (f^2)(0; y=3) == 3*2
@test (f^3)(0; y=3) == 3*3
@test (f^4)(0; y=3) == 3*4
for n = 0:10
@test (f^n)(0; y=3) == 3*n
end
end
@test_throws MethodError sin^-1 # calls inv(sin)
let n=-1
@test_throws ArgumentError sin^n
end
end

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