Hello folks, and thanks for your oppinion in advance.
First, I’m pretty new to Julia so you may forgive me the style if you will
While learning the language and creating the first functionalities, I came up with this one:
Suppose I have a chain of functions that all need the same arguments (KnowledgeBase, Parameters, etc) and additionally they need the input from the previous function (some sort of extended chaining if you will).
I basically figured out two ways to do this (without writing the call manually):
Custom composition with expressions or wrapping in lambdas and normal composition.:
EDIT: added leightweight variant from @Tamas_Papp for comparison. Recursive evaluation is slightly slower than composition but much easier to read.
using BenchmarkTools
struct MyChainer1{T, F<:Function} # construct as expression
constantArgument::T
fun::F
function MyChainer1(constArg::Any, funs::AbstractVector{<:Function})
@assert length(funs)>1 #else it would be pointless
ex = :(X)
for f in funs
ex = :($f($ex, $constArg))
end
ex = :(X -> $ex)
fun = eval(ex)
new{typeof(constArg), typeof(fun)}(constArg, fun)
end
end
(m1::MyChainer1)(data) = m1.fun(data)
struct MyChainer2{T, F<:Function} # construct from lambda-wrapped functions
constantArgument::T
fun::F
function MyChainer2(constArg::Any, funs::AbstractVector{<:Function})
@assert length(funs)>1 #else it would be pointless
funs = map( g -> x -> g(x, constArg), funs)
fun = reduce(∘, reverse(funs))
new{typeof(constArg), typeof(fun)}(constArg, fun)
end
end
(m2::MyChainer2)(data) = m2.fun(data)
struct MyChainer3{T, F<:Function} # construct by hand
constantArgument::T
fun::F
function MyChainer3(constArg::T, fun::F) where {T, F<:Function}
new{T, F}(constArg, fun)
end
end
(m3::MyChainer3)(data) = m3.fun(data)
# as suggested by @pbayer and @Christopher_Fisher
function chain_quote(x::S, ca::A, fs::AbstractVector{<:Function}) where {S,A}
if length(fs) == 1
:( $(fs[1])($x, $ca) )
else
chain_quote( :( $(fs[1])($x, $ca) ), ca, fs[2:end])
end
end
struct MyChainer4{T, F<:Function}
constantArgument::T
fun::F
function MyChainer4(constArg::Any, funs::AbstractVector{<:Function})
@assert length(funs)>1 #else it would be pointless
sym = gensym()
fun= eval(:($sym -> $(chain_quote(sym, constArg, funs)) ))
new{typeof(constArg), typeof(fun)}(constArg, fun)
end
end
(m4::MyChainer4)(data) = m4.fun(data)
# suggested by @Tamas_Papp (and @pbayer similarly)
chain_them(b, A) = b # base case
chain_them(b, A, f1, fs...) = chain_them(f1(b, A), A ,fs...)
setup_chain(A, fs...) = x -> chain_them(x, A, fs...) # addition to allow partial function application
A = [1 2 -4 1 1; 2 -5 4 2 3; 3 0 -3 -1 4;2 3 7 -2 -8 ; 0 3 0 -9 2]
chainer1 = MyChainer1(A, [/,*,+,*,+,/,+,*])
chainer2 = MyChainer2(A, [/,*,+,*,+,/,+,*])
chainer3 = MyChainer3(A, x -> ((((((((x / A) * A) + A) * A) + A) / A) + A) * A))
chainer4 = MyChainer4(A, [/,*,+,*,+,/,+,*])
chainer5 = setup_chain(A,/,*,+,*,+,/,+,*)
# all should yield the same results
@assert chainer1(A') == chainer2(A') && chainer2(A') == chainer3(A') && chainer3(A') == chainer4(A') && chainer3(A') == chain_them(A', A ,/,*,+,*,+,/,+,*) && chainer3(A') == chainer5(A')
show(stdout, "text/plain", @benchmark chainer1($A'))
println()
show(stdout, "text/plain", @benchmark chainer2($A'))
println()
show(stdout, "text/plain", @benchmark chainer3($A'))
println()
show(stdout, "text/plain", @benchmark chainer4($A'))
println()
show(stdout, "text/plain", @benchmark chainer5($A'))
println()
show(stdout, "text/plain", @benchmark chain_them($A', $A ,/,*,+,*,+,/,+,*))
To my surprise, wrapping everything in lambdas did not incur a performance penalty (here?).
EDIT: figured out that I was using benchmarktools wrong and the lambdas do seem to come with a penalty. Too bad since I liked that composition operator most.
But I’m still thinking that there must be a more elegant way to do this. Does anyone know of a better way? Am I doing something stupid here or did I miss something in the docs?
Thanks in advance!