Can I dispatch on (mathematical) functions?

Hi,

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

Well, I disagree:

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 ?

that’s the same as saying

(x->x^2)(3.0) === (3.0)^2

you want this to happen in type system no? without actually running the code. The language compiler does not work symbolically

No it is not the same thing as your version only checks for the value 3.0 while mine checks for the expression itself…

I understood that this will not happend in the type system, fair enough. But I still need it to happend ^^

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"

1 Like

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.

OP wants this to work

julia> h(x->x^2)
ERROR: MethodError: no method matching h(::var"#1#2")

Yes this is what I am currently trying to do

Ok this works:

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.

library is returning a tuple of pairs, you should use a Dict instead.

1 Like

I may be missing the point, but why not just use ordinary named functions for the known transforms as follows:

square_example(x) = x^2
exponential_neg_example(x) = exp(-x)

do_transform(f::F) where {F <: Function} = "I've done a generic transform"
do_transform(f::typeof(square_example)) = "I've transformed x^2!"
do_transform(f::typeof(exponential_neg_example)) = "I've transformed exp(-x)!"



do_transform(square_example) |> println
do_transform(exponential_neg_example) |> println
do_transform(exp) |> println

which produces the output

I've transformed x^2!
I've transformed exp(-x)!
I've done a generic transform

Because I want the user to be able to pass the function without knowing its name

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.

1 Like

I Agree with you. The current sketch

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:

using StaticNumbers
op(@optic _^static(2))

But think first whether your really need this?..

1 Like