This is what you must understand about using this package:
The only way that it âknowsâ how to invert a function, is through a lookup table.
For example, it has been programmed that the inverse of the function x\mapsto\sqrt x is y\mapsto y^2. But how does it know that a function is x\mapsto \sqrt x to begin with? By reference. The only way it âknowsâ that the inverse of sqrt
is a squaring function, is because it has been programmed that the inverse of the function we reference with the identifier sqrt
is a squaring function. And for the most part, this is how humans invert functions too.
This is best explained with an example:
julia> inverse(sqrt) # magic!
square (generic function with 2 methods)
julia> f(x) = sqrt(x) # same thing, right?
f (generic function with 1 method)
julia> inverse(f) # oh no! it has no idea!
NoInverse{typeof(f)}(f)
The moment we wrap sqrt
in a new function, the package doesnât know what the inverse is anymore. By inspection I can say that f(x)=\sqrt x for all x, but I can say this because I wrote it and I can see it; the inverse
function has no clue whatâs inside.
The same thing would happen if I blindfold you and give you a function f without telling what its definition is, and ask you to tell me the inverse; you wouldnât be able to invert the function, even if it was really simple, because you donât know whatâs inside.
For contrast, letâs see what happens if we just create a new reference to the same function:
julia> w = sqrt # notice: this is just an assignment
sqrt (generic function with 19 methods)
julia> inverse(w) # it works!
square (generic function with 2 methods)
When I set w = sqrt
, w
is a new reference pointing to the same location in memory as sqrt
, so inverse
is able to identify that itâs the same thing.
As weâve previously mentioned, when we create a more complicated function, we run into the same problem: inverse
canât tell whatâs inside, unless we define the function as a composition of other functions.
This works because when I compose two functions using the â
operator, I create an object which has references to both functions. For example:
julia> exp â sqrt
exp â sqrt
julia> (exp â sqrt).inner
sqrt (generic function with 19 methods)
julia> (exp â sqrt).outer
exp (generic function with 14 methods)
This gives InverseFunctions.jl visibility into what functions have been composed to make the greater function, and as long as it has inverses defined for the inner
and outer
functions, it can invert the composition:
julia> inverse((exp â sqrt).inner)
square (generic function with 2 methods)
julia> inverse((exp â sqrt).outer)
log (generic function with 26 methods)
julia> inverse(exp â sqrt)
InverseFunctions.square â log
Notice that this works:
julia> s = exp
exp (generic function with 14 methods)
julia> t = sqrt
sqrt (generic function with 19 methods)
julia> inverse(s â t)
InverseFunctions.square â log
but this does not:
julia> u(x) = exp(x)
u (generic function with 1 method)
julia> v(x) = sqrt(x)
v (generic function with 1 method)
julia> inverse(u â v)
NoInverse{ComposedFunction{typeof(u), typeof(v)}}(u â v)
we can force it to work though by manually adding entries to the lookup table (by overloading the inverse
function):
julia> InverseFunctions.inverse(::typeof(u)) = log
julia> InverseFunctions.inverse(::typeof(v)) = InverseFunctions.square
julia> inverse(u â v)
InverseFunctions.square â log
this isnât useful in this case, because u
and v
are so simple they could have just been made to be references, but it could become useful later if you define more complicated functions.
So in your case, we want to invert a function x\mapsto \sqrt{4x}. We can write it as a composition of functions, (x\mapsto\sqrt x)\circ(x\mapsto 4x), but if we want to leverage InverseFunctions.jlâs power we need identifiers for these functions that it recognizes.
For the first function, we have the sqrt
identifier.
And for the second? The function being called is multiplication, which is a binary operator. In order for multiplication to be invertible, we need to declare a single-argument function in terms of it: we need to fix one of its arguments. And thatâs where Base.Fix1(*, 4)
comes in: itâs a way to partially-apply the multiplication operatorâresulting in a single-argument functionâand because itâs built-in to the language, InverseFunctions.jl has been programmed to recognize it. (namely, for any two-argument function f, we can represent the function x\mapsto f(c, x) with Base.Fix1(f, c)
and x\mapsto f(x, c) we can represent with Base.Fix2(f, c)
).
And so we write:
julia> inverse(sqrt â Base.Fix1(*, 4))
Base.Fix1{typeof(\), Int64}(\, 4) â InverseFunctions.square
Now, something you have to be careful with, is that if you have previously declared h
as a function (using named function syntax), you will get this error:
julia> h = inverse(sqrt â Base.Fix1(*, 4))
ERROR: invalid redefinition of constant h
so we will need to either a) restart Julia so that we can give h
a new meaning, or b) pick a different identifier such as inv
:
julia> inv = inverse(sqrt â Base.Fix1(*, 4))
Base.Fix1{typeof(\), Int64}(\, 4) â InverseFunctions.square
julia> [inv(y) for y = 1:8]
8-element Vector{Float64}:
0.25
1.0
2.25
4.0
6.25
9.0
12.25
16.0
Now, if you think that learning about internals like Base.Fix1
for this purpose is unsavory, I agree. Getting PR#24990 approved, along with a generalized partial function applicator and a function composition fallback as laid out in points 1, 2, and 3 at the bottom of this post would solve this. But for now, if you just want something that works today and you donât mind it being constrained to a macro call, @optic
works quite well:
julia> using Accessors
julia> inv = inverse(@optic sqrt(4_))
Base.Fix1{typeof(\), Int64}(\, 4) â InverseFunctions.square
Itâs also possible to use a numerical solver, for example:
julia> using Roots
julia> f(x) = â(4x)
f (generic function with 1 method)
julia> [fzero(x->f(x)-y, 0) for y=1:8]
8-element Vector{Float64}:
0.25
1.0000000000000002
2.25
4.0
6.25
9.0
12.25
16.0
Before jumping to the conclusion that numerical solvers are magic and solve all our problems, you need to be aware that some people spend their entire lives making them work and they still donât solve all our problems. They work sometimes though, and itâs nice when they do.