Here is another approach with a bit of type piracy.
julia> islinear(::Function) = false
islinear (generic function with 1 method)
julia> function Base.:*(n::Number, f::Function)
if islinear(f)
h = (x...) -> 2 * f(x...)
@eval islinear(::typeof($h)) = true
return h
else
throw(MethodError(*, (n,f)))
end
end
julia> function Base.:+(a::Function, b::Function)
if islinear(a) && islinear(b)
h = (x...) -> a(x...) + b(x...)
@eval islinear(::typeof($h)) = true
return h
else
throw(MethodError(+, (a,b)))
end
end
julia> f(x) = 2x
f (generic function with 1 method)
julia> g(x) = 3x
g (generic function with 1 method)
julia> f + g
ERROR: MethodError: no method matching +(::typeof(f), ::typeof(g))
Closest candidates are:
+(::Function, ::Function) at REPL[3]:1
+(::Any, ::Any, ::Any, ::Any...) at operators.jl:591
Stacktrace:
[1] +(a::Function, b::Function)
@ Main ./REPL[3]:7
[2] top-level scope
@ REPL[6]:1
julia> islinear(::typeof(f)) = true
islinear (generic function with 2 methods)
julia> islinear(::typeof(g)) = true
islinear (generic function with 3 methods)
julia> f + g
#3 (generic function with 1 method)
julia> h = f + g
#3 (generic function with 1 method)
julia> h(1)
5
julia> h(2)
10
julia> f + h
#3 (generic function with 1 method)
julia> islinear(ans)
true
The question then becomes how to avoid the type piracy. You could make your own +
and *
in a module:
julia> module LinearFunctions
islinear(::Function) = false
function +(a::Function, b::Function)
if islinear(a) && islinear(b)
h = (x...) -> Base.:+(a(x...),b(x...))
@eval islinear(::typeof($h)) = true
return h
else
throw(MethodError(+, (a,b)))
end
end
function *(n::Number, f::Function)
if islinear(f)
h = (x...) -> Base.:*(2,f(x...))
@eval islinear(::typeof($h)) = true
return h
else
throw(MethodError(*, (n,f)))
end
end
end
Main.LinearFunctions
julia> f(x) = 2x
f (generic function with 1 method)
julia> g(x) = 3x
g (generic function with 1 method)
julia> f(1)
2
julia> g(2)
6
julia> import .LinearFunctions: islinear, +, *
julia> h = f + g
ERROR: MethodError: no method matching +(::typeof(f), ::typeof(g))
Closest candidates are:
+(::Function, ::Function) at REPL[1]:3
Stacktrace:
[1] +(a::Function, b::Function)
@ Main.LinearFunctions ./REPL[1]:9
[2] top-level scope
@ REPL[5]:1
julia> islinear(::typeof(f)) = true
islinear (generic function with 2 methods)
julia> islinear(::typeof(g)) = true
islinear (generic function with 3 methods)
julia> h = f + g
#1 (generic function with 1 method)
julia> islinear(h)
true
julia> 1 + 2
ERROR: MethodError: no method matching +(::Int64, ::Int64)
You may have intended to import Base.:+
Stacktrace:
[1] top-level scope
@ REPL[10]:1
julia> h(2)
10
One way around this would be to use a macro. Hereās a quick version.
julia> module LinearFunctions
islinear(::Function) = false
function +(a::Function, b::Function)
if islinear(a) && islinear(b)
h = (x...) -> Base.:+(a(x...),b(x...))
@eval islinear(::typeof($h)) = true
return h
else
throw(MethodError(+, (a,b)))
end
end
function *(n::Number, f::Function)
if islinear(f)
h = (x...) -> Base.:*(n,f(x...))
@eval islinear(::typeof($h)) = true
return h
else
throw(MethodError(*, (n,f)))
end
end
macro linear(e)
if e.head == :call && e.args[1] ā (:+, :*)
e.args[1] = :(LinearFunctions.$(e.args[1]))
return esc(e)
elseif e.head == :(=) && e.args[1].head == :call
func = esc(e.args[1].args[1])
e = esc(e)
quote
$e
LinearFunctions.islinear(::typeof($func)) = true
$func
end
else
return e
end
end
end
Main.LinearFunctions
julia> import .LinearFunctions: islinear, @linear
julia> @linear f(x) = 2x
f (generic function with 1 method)
julia> @linear g(x) = 3x
g (generic function with 1 method)
julia> islinear(f)
true
julia> islinear(g)
true
julia> foo = @linear f + g
#1 (generic function with 1 method)
julia> islinear(foo)
true
julia> foo(1)
5
julia> foo(2)
10
julia> h = @linear (@linear 2f) + g
#1 (generic function with 1 method)
julia> h(1)
7
julia> 1 + 2
3