Implementing a pointwise arithmetic on functions

Hey,

I just came across a problem that I am sure can be solved elegantly in Julia but I am lacking a good starting angle. Assume that I have a collection of functors (callable types) of the form

struct Functor
  f::function
end

(f::Functor)(x)::Real = f.f(x)

This makes sure that all objects of type Functor can be called with a single argument x (which would have to be checked in a proper constructor…)

What would be the best way to implement point-wise arithmetic on these functors? I.e., I’d like to be able to create a new functor via arbitrary Julia expressions treating functor objects as reals. Something like

a = Functor(sin)
b = Functor(cos)
c = @compose a + exp(b) # c should also be of type Functor
c(1) # should return sin(1) + exp(cos(1))

should work. Not quite sure but I suspect this is a macro problem since I essentially need to treat the argument to @compose as Julia expression, replace all references to functors by their pointwise evaluation and then evaluate the expression.

Are macros the way to go here or are there better options?

Macros work on syntax but here you are talking about the type of variables which are not known until the function runs. If you want to create a macro you need to think about the input and output syntax of the macro. A good start is to write down a few examples by hand, and then write the macro to do it. If you can’t write down the syntactic transformation by hand, then you can’t use a macro.

An example could be, input syntax: a + exp(b), output syntax Functor(x -> a(x) + exp(b(x))).

1 Like

I see, so I will essentially have to figure our which symbols in the input syntax point to objects of type Functor in the enclosing environment, and replace these with calls to <>(x) in the input expression. Should be doable, thx.

Generally speaking though, metaprogramming is the way to go here, right? Sub-typing Real won’t get me anywhere since I cannot pass arguments, I guess.

If you just want a syntactic transform you could do something like

apply(f, y) = f
apply(f::Functor, y) = f(y)

macro compose(expr)
    s = gensym()
    _compose!(expr,s)
    return :(Functor($s -> $expr))
end

function _compose!(expr, s)
    isa(expr, Symbol) && return :(apply($expr, $s)
    isa(expr, Expr) || return expr
    for i in 1:length(expr.args)
        expr.args[i] = _compose!(expr.args[i], s)
    end
    return expr
end
julia> a = Functor(sin)
Functor(sin)

julia> b = Functor(cos)
Functor(cos)

julia> c = @compose a + exp(b) # c should also be of type Functor
Functor(getfield(Main, Symbol("##5#6"))())

julia> c(1)
2.5579966843568

but it is kinda strange just to replace symbols. You might have Functors being returned from other functions etc for which a macro wouldn’t work.

x) cool stuff, thx! Still struggling with a few things here.

First, super dummy question, how does

isa(expr, Expr) || return expr

work? What does the ‘or’ do with ‘return’? Is that something like a super concise error handling? I just can’t wrap my head around it and googling didn’t help either x)

Your solution might actually work for what I had in mind, will play around with it a bit longer, thx a lot.

Note that in principle you don’t need a macro for this. For example, if you define:

struct Functor{F<:Function} <: Function
  f::F
end
(f::Functor)(x)= f.f(x)

for op in (:+, :-, :*, :/, :\)
   @eval begin
        Base.$op(a::Functor, b) = Functor(x -> $op(a(x), b))
        Base.$op(a, b::Functor) = Functor(x -> $op(a, b(x)))
        Base.$op(a::Functor, b::Functor) = Functor(x -> $op(a(x), b(x)))
    end
end
for f in (:sin, :cos, :exp, :log, :+, :-)
    @eval Base.$f(a::Functor) = Functor(x -> $f(a(x)))
end

Then if you do

a = Functor(sin)
b = Functor(cos)
c = a + exp(b - 2)
c(1)

it gives 1.073777476539272, which is equal to sin(1) + exp(cos(1) - 2).

The downside of this approach, of course, is that you need to predetermine the set of functions where you want Functor to automatically compose. The advantage is that, for this set, it works reliably — Julia knows exactly which things are of type Functor, so it won’t get confused if you have a symbol of another type, e.g.:

y = 17
d = a + y
d(1)

will correctly give 17.841470984807895 == sin(1) + 17. (In contrast, a macro executes right after parsing and doesn’t know the type of anything — it only knows how things are spelled.)

For a different function-composition approach, see the https://github.com/JuliaApproximation/ApproxFun.jl package. In ApproxFun, each time you compose its functions (a Fun type), it forms a new polynomial approximation. No matter how many functions you compose, you get a single polynomial — not only does this result in fast evaluation, but it also let’s you do things like find roots and integrate or solve PDEs.

4 Likes

See https://docs.julialang.org/en/v1/manual/control-flow/index.html#Short-Circuit-Evaluation-1.

1 Like

Also nice, less scoping issues and straight forward support for things like

c = a
for i in 1:3
  global c = c + a
end

Probably would need to implement a register function to allow post hoc addition of new compositional functions, e.g.

function register(f)
    @eval $f(a::Score) = Score(x -> $f(a(x)))
end

register(:(Base.sin))