Reputation: 161
I am trying to create a function that would allow for changing a formula and coefficients in Julia. I am 80% sure I should be using anonymous functions for this?
This SO post using python is a more discrete example of what I am trying to accomplish (in particular the base python example by chepner, rather than using a library). Pass a formula as a function parameter in python
I also found this SO post using Julia that uses a type to store needed parameters and then pass them to a function. How to pass parameter list to a function in Julia
Using these as a base, this is what I have created so far:
#Create composite type
type Params
formula
b1::Float64
b2::Float64
end
#create instance of type and load
foo=Params((b1,b2,X)-> X^b1+X+b2,0.004,0.005)
#create function
function DoMath(X,p::Params)
p.formula(X,varargs...) #??
end
Am I on the right track as to how to structure this by using composite types and/or lambdas? I don't have any CS training and I am muddling my way through many concepts while trying to learn Julia.
What is the correct syntax for a function that can allow a user to change the formula and any coeffs. for a given X? Ultimately, I am imagining something with functionality like:
DoMath(4) #some default formula with changing X
DoMath(4, X*b1 +X*b2) #change X and change formula
DoMath(4, (X,b1,b2,b3)->X*b1+X*b2+x*b3) # change x, change formula to a 3 parameter function
Thank you
Update: I got it working by following @Chris's syntax. One thing I had to tinker with is using
(p::Params)(x) = p.formula(x,p.b) #p.b, not just b otherwise error
and I had to wrap the 2.0 and 3.0 in an array before calling
p = Params((x,b)->x*b[1]+b[2],[2.0,3.0])
Upvotes: 5
Views: 1834
Reputation: 8566
Here is a similar pattern that I currently use for dealing with these "fixed parameters" vs "changing parameters" problems.
Fixed parameters are those do not often change when running a particular program(e.g. b1
,b2
,b3
). Changing parameters are common variables(e.g. x
) which almost keep changing between every function call. In many cases, using optional arguments or keyword arguments is enough to deal with the problem, but if we would like to change both function and its parameters simultaneously, this solution maybe not an ideal one. As the answer in this post suggested, a better way would be to create a type and use multiple dispatch. However, we also need to manually unpack
the type in the function body. In fact, we can write a more generic function wapper using @generated
macro:
abstract FormulaModel
immutable Foo{F<:Function} <: FormulaModel
formula2fixedparams::F
fixedParam1::Float64
fixedParam2::Float64
end
Foo(f, b1=5., b2=10.) = Foo(f, b1, b2)
Foo() = Foo((x,b1,b2)->x^b1+x+b2) # default formula
immutable Bar{F<:Function} <: FormulaModel
formula3fixedparams::F
fixed1::Float64
fixed2::Float64
fixed3::Float64
end
Bar(b1,b2,b3) = Bar((x,b1,b2,b3)->b1*x+b2*x+b3*x, b1, b2, b3)
Bar() = Bar(1.,2.,3.)
@generated function DoMath(model::FormulaModel, changingParams...)
fixedParams = [:(getfield(model, $i)) for i = 2:nfields(model)]
func = :(getfield(model, 1))
# prepare some stuff
quote
# do something
$func(changingParams..., $(fixedParams...))
# do something else
end
end
julia> DoMath(Foo(), 4)
1038.0
julia> DoMath(Foo((x,y,b1,b2)->(b1*x+b2*y)), 4, 10)
120.0
julia> DoMath(Bar(), 4)
24.0
Upvotes: 4
Reputation: 19132
The idea is to build a callable type. A callable type is any type which has a "call". A function f
is a callable type because you can call on it: f(x)
for example. However, functions are not the only things that can act like functions. Indeed, in Julia, functions are basically callable types which <: Function
.
So let's build one for your example. Make your type contain the data you want:
type Params
b1::Float64
b2::Float64
end
Now let's add a call to a Params
. Let's say we want to do x*b1 + b2
. We can make a call that does this by:
(p::Params)(x) = x*p.b1 + p.b2
Let's see how this works. Make a Params:
p = Params(2.0,3.0)
now we can calculate the formula by using its call:
p(4) # 4*2+3 = 11
Now see that p
acts as a function which uses the internal data. This is it.
The rest is all building up from this same foundation. You need to respect the fact that Julia types are not dynamic. This is for good reasons. However, let's say you don't know how many b's you want. Then you can just have a field be an array of b
's:
type Params
b::Vector{Float64}
end
(p::Params)(x) = x*b[1] + b[2]
Now let's say you wanted to be able to modify the formula. Then you can have a formula field:
type Params
formula
b::Vector{Float64}
end
and make the call throw the values into that:
(p::Params)(x) = p.formula(x,b)
Now if a user did:
p = Params((x,b)->x*b[1]+b[2],2.0,3.0)
then, as before:
p(4) # 4*2+3 = 11
Yay it acts the same, and still uses internal data.
But since p
is just any ol' type, we can modify the fields. So here we can modify:
p.formula = (x,b)-> x*b[1]+x*b[2]+b[3]
push!(p.b,2.0) # p.b == [2.0,3.0,2.0]
and call again, now using the updated fields:
p(4) # 4*2 + 4*3 + 2 = 22
Indeed, as @LyndonWhite pointed out, ParameterizedFunctions.jl implements something like this. The strategy for doing so is callable types.
Some libraries were built (incorrectly) requiring that the user pass in a function. So here we have a p
that "acts like a function", but some libraries won't take it.
However, there is a quick fix. Just make it <:Function
. Example:
type Params <: Function
b1::Float64
b2::Float64
end
Now things which require a function will take your p
as it is a <:Function
. This is just one way to point out that in Julia, Function
is an abstract type, and each function
is just a callable type which subtypes Function
.
Upvotes: 13