TLDR: Is it possible to dispatch on (mathematical) functions ?
Broad context and reasons I am asking this
In this (just registered) package GitHub - lrnv/WilliamsonTransforms.jl I implemented a bijective mapping between the set of non-negative random variables \mathcal X and a set of d-monotone functions \mathcal F. You may take a look at the documentation there : Home · WilliamsonTransforms.jl to see what I am talking about.
Now that i have the general formula, this is great, but I wanted to implement some specific cases where the transforms are known, using dispatch. In one way, I can dispatch on random variables, but on the other way I cannot dispatch on functions…
The generic version is much more expensive than the particular cases, where the result is simply known.
Consider this functional operator:
function op(F)
return "Generic"
end
square(x) = x^2
function op(F <: typeof(square))
return "Specific for the square function"
end
op(x -> x^2) # does not work as intended..
You may assume that all functions that I will use are univariate continuous functions from \mathbb R^+ to \mathbb R^+. I think it might then be possible to check them using e.g. Symbolics.jl, against a “library” of a few implemented special cases. But is that possible to do in the type system ?
no, not like this, in fact, every x -> x^2 is a new function right now… but even if we solve that(we kinda can’t because world age?), there’s no way to know square(x) and x->x^2 are the same function
using Symbolics
square(x) = x^2
contestant_1 = x -> x^2
contestant_2 = x -> x^3
@variables t
isequal(square(t),contestant_1(t)) # true
isequal(square(t),contestant_2(t)) # false
But maybe indeed not at type system level indeed. If not, how can i construct a tool that will “identify” a given function argument and put this identification into the type system ?
This is exactly what ChainRules.jl does with rrules and frules, no?
You don’t get the subtyping that I think you’re looking for, but so long as you’re using actual named functions and not anonymous functions, it works
julia> f(x) = x^2
f (generic function with 1 method)
julia> g(x) = x^3
g (generic function with 1 method)
julia> h(::typeof(f)) = "Squaring"
h (generic function with 1 method)
julia> h(::typeof(g)) = "Cubing"
h (generic function with 2 methods)
julia> h(f)
"Squaring"
julia> h(g)
"Cubing"
that’s impossible, because dispatch is a type system thing. You can make your own dispatch system and always ask user to call that function. And in the function, you run your variable through functions and see if the values (expression in Symbolics.jl) returned are the same.
library(x) = (
x^2 => :SquareExemple,
exp(-x) => :Independant,
(1+x)^(-1) => :Clayton
)
function identify(f, library)
@variables t
# This did not work:
# return library(t)[f(t)]
f_t = f(t)
for (g_t,name) in library(t)
if isequal(f_t,g_t)
return name
end
end
return :NotFound
end
identify(contestant_1,library)
identify(contestant_2,library)
identify(x -> 1/(1+x),library)
But now I probably want something like this to lift up the information in the type system for future uses:
abstract type Transform end
struct ClaytonTransform<:Transform end
struct IndependentTransform<:Transform end
struct SquareExemple<:Transform end
struct GenericTransform{Tf}<:Transform
f::Tf
function Transform(f)
id = identify(f,library)
if id == :NotFound
return new{Tf}(f)
elseif id == :Clayton
return ClaytonTransform()
elseif id == :Independant()
return IndependentTransform()
elseif id == :SquareExemple
return SquareExemple()
end
end
end
If the list grows a bit longer, that would be tedious to manage in the package repository… But I guess I only have about 20 particular cases so I can mange this.
I would think that it would be useful for the user to be aware of which functions have known transforms and are therefore much cheaper. In that case giving them specific names for the user to invoke doesn’t seem too onerous. Just my opinion.
using Symbolics
library(x) = Dict(
x^2 => :SquareExemple,
exp(-x) => :Independant,
(1+x)^(-1) => :Clayton
)
function identify(f, library)
@variables t
return get(library(t),f(t),:NotFound)
end
abstract type Transform end
struct ClaytonTransform<:Transform end
struct IndependentTransform<:Transform end
struct SquareExemple<:Transform end
struct GenericTransform{Tf}<:Transform
f::Tf
end
function Transform(f)
id = identify(f,library)
if id == :NotFound
return GenericTransform{typeof(f)}(f)
elseif id == :Clayton
return ClaytonTransform()
elseif id == :Independant
return IndependentTransform()
elseif id == :SquareExemple
return SquareExemple()
end
end
Transform(square)
Transform(x -> x^2)
Transform(x -> x^3)
Transform(x -> exp(-x))
allows for discoverability (if all structs are exported), but also to pass an unknown function. The user (let’s face it: its me) might not even know its own function, might result from an algorithm or something.
How crucial it is for op(x -> x^2) specifically to dispatch?
There are dispatchable alternatives that don’t involve special square(x) definition:
op(Base.Fix2(^, 2)) # works, but not convenient and hard to read
using Accessors
op(@optic _^2) # fundamentally the same as above, but with nicer syntax
these are for case where dispatch by “any power function” is enough, 2 isn’t a compile-time parameter. For that you would need static integers: