-
-
Notifications
You must be signed in to change notification settings - Fork 5.5k
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
base: master
Are you sure you want to change the base?
Conversation
The traditional issue with this has been that |
Yes, but the iterated-function notation is far more common and consistent — there are only a handful of functions like In consequence, settling on the iterated-function meaning doesn't bother me here; it is more general, more consistent, and far more useful (since it is vastly more concise and clear than any existing alternative) than mere exponentiation of the result. |
I am not a big fan of using |
""" | ||
struct IteratedFunction{F} <: Function | ||
f::F | ||
n::Int |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.
I can't think of anything offhand that wouldn't be a bit ugly and confusing… ApproxFun already overloads |
I think superscript o would make sense for this:
I am mainly concerned that |
@@ -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...) |
There was a problem hiding this comment.
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.
Possible Unicode symbol option is |
Isn't composition enough of a multiplication-like operator? After all, it has the same precedence: julia> let prec=Base.operator_precedence; prec(:⋅) ≡ prec(:×) ≡ prec(:∘) ≡ prec(:*) ≡ prec(:⋆) end
true The natural question should be: which type of "multiplication" should I've seen iterated functions written I'll add that FWIW, here's what ChatGPT thinks:
|
I think in programming we need to be a bit more selective about our notation than in math, because while mathematicians have the benefit of context, culture, and critical thinking, computers take the notation literally. For programming, I prefer to specify relationships between symbols, such as |
While I empathize with the sentiment, in the case of I think of the |
Btw, array languages have function iteration syntax, but they also allow Here I'm using https://github.com/JuliaMath/InverseFunctions.jl. using InverseFunctions, Test
struct IteratedFunction{F}
f::F
n::Int
end
((;f,n)::IteratedFunction)(x) =
if n == 0
x
elseif n > 0
foldl((y,f)->f(y), [f for _ in 1:n]; init=x);
else # if n < 0
(inverse(f) ∘̂ (-n))(x)
end
f ∘̂ n = IteratedFunction(f,n)
@test (exp∘̂1)(1) ≈ 2.718281828459045
@test (exp∘̂2)(1) ≈ 15.154262241479262
@test (exp∘̂3)(1) ≈ 3.814279104760214e6
@test (exp∘̂(-1))(ℯ) ≈ 1.0
@test (exp∘̂(-2))(ℯ) ≈ 0 |
Yes, I'm enthusiastic about the possibility of In #24990 I show an example where I write I forgot to mention before, but here's another reason that iterated function composition is a coherent use of Where a linear transformation |
Regarding A while ago I created a package FunctionChains that might be very suitable to host something like this (if it doesn't go into the language itself). FunctionChains supports julia> using FunctionChains, InverseFunctions, FillArrays
julia> f = setinverse(x -> x * 2, x -> x / 2)
julia> g = fchain(Fill(f, 3))
julia> g(2)
16
julia> inverse(g)(16)
2.0
So in principle, this ∘̂(f, n::Integer) = n >= 0 ? fchain(Fill(f, n)) : fchain(Fill(inverse(f), -n)) would already do the trick: julia> (f∘̂ 3)(2)
16
julia> (f∘̂ -3)(16)
2.0
julia> inverse(f∘̂ 3)(16)
2.0 FunctionChains has a few extra tricks, so this works too then julia> with_intermediate_results(f∘̂ 3, 2)
3-element Vector{Int64}:
4
8
16 and also julia> using ChangesOfVariables
julia> f = setladj(x -> x * 2, _ -> log(2))
julia> with_logabsdet_jacobian(f∘̂3, 2)
(16, 2.0794415416798357) It would certainly be better to use a dedicated |
Point of note, julia> let prec=Base.operator_precedence; prec(:∘̂), prec(:^) end
(12, 15) |
Ah, yes - also, it's hard to type. Since |
I like the |
I don't believe |
Well, I'll be happy with whatever the language experts on this thread would prefer to go with. If we do it in a package though, anything like Question, if we do this in the language, though, not in a package, how do get |
I can imagine three possible paths at the moment:
|
Using Using an alternative like |
Not if it's implemented in
My suspicion is that a cost/benefit analysis will find the inconveniency of always needing to type Ok I'll step back now. |
I'll add this to FunctionChains (oschulz/FunctionChains.jl#13) as |
FunctionChains v0.1.6 supports this now: julia> using FunctionChains
julia> (sqrt ∘̂ 2)(16)
2.0
julia> (sqrt ∘̂ -2)(2)
16 Note that when chained this does not behave like exponentiation, and
|
As suggested in #34251 (comment) by @StefanKarpinski and motivated more recently by #39031 for
dirname
, this PR definesfunction^n
as an iterated function.For example,
(dirname^2)("/foo/bar/baz") == dirname(dirname("/foo/bar/baz")) == "/foo"
, and((x -> (x + 2/x)/2)^10)(1.234)
performs 10 iterations of Newton's method forsqrt(2)
, giving1.414213562373095 ≈ sqrt(2)
.(Note that you must call
f^2
as(f^2)(x)
, notf^2(x)
, because the latter is parsed asf^(2(x))
.)Using
literal_pow
, I madef^0 == identity
,f^1 == f
, andf^2 == f ∘ f
, up tof^4
, for improved type inferability and inlining. (Note thatf^-1
already callsinv(f)
, generally giving aMethodError
, fromliteral_pow
, and the interpretation as an iterated function is consistent withf^-1
denoting an inverse function.)Keyword arguments of
f^n
are passed through tof
. I also changed theidentity
function to ignore keyword arguments, and changedf ∘ g
to pass any keywords through to both functions — previously, keywords were not allowed in either case, while the documentation forf ∘ g
incorrectly stated that keywords were passed tog
, so the new behavior is backwards compatible and more useful.