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!