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.