On Friday, October 31, 2014 10:27:45 PM UTC-7, Franklin Fuller wrote:
>
> As a part of a project I'm working on I found myself wanting to solve a 
> problem, which in English would be phrased:
>
> "Given a vector of parameterized univariate functions [f1(x1, P1), f2(x2, 
> P2), f3(x3, P3), ...] with argument xj and parameters Pj, along with a 
> vector of parameters [P1, P2, P3...], return a multivariate function 
> g(x1,x2,x3, ...) which is the product of the parameterized input functions: 
> f1(x1, P1)*f2(x2, P2)*f3(x3, P3)*..."
>
> I decided, after some investigation, that this might be best solved by 
> some metaprogramming.  I am not at all familiar with metaprogramming, so I 
> ended up spending a full day trying to make this function... (don't judge 
> me).  But it works!  So I'll post it as a slightly more than trivial 
> example.
>
> function prodFun(P::Vector, f::Vector{Function})
>   ex = Expr(:call,Expr(:ref,:($f),1),:(X[1]),:($(P[1])))
>   for k=2:length(f)
>     exnew = Expr(:call,Expr(:ref,:($f),:($k)),:(X[$k]),:($(P[k])))
>     ex = Expr(:call,:*,ex,exnew)
>   end
>   return eval(Expr(:(=),Expr(:call,:g,:x),ex))
> end
>

Note that prodFun as written assigns to the symbol g. You probably want it 
to just return a function instead of assigning to g.
 

> Here's an example of it in action:
>
> >P = {[1; 2]; [3; 4]; [5; 6]; [7; 8]}
> >f(x,P) = P[1]*exp(x)/sin(P[2])
> >g = prodFun(P, [f; f; f])
>
> >g([1; 1; 1])
> >>1567.0
> >f(1,P[1])*f(1,P[2])*f(1,P[3])
> >>1567.0
>

In this case, it's easier to use function currying than metaprogramming. 
E.g. you could first define a function that takes x, P, and f:

    function prodFun(x, P, f)
        out = f[1](x[1], P[1])
        for k in 2:length(f)
            out *= f[k](x[k], P[k])
        end
        return out
    end

And then define a function that takes P and f, and returns a function of x.

    curriedProdFun(P, f) = (x) -> prodFun(x, P, f)

You can now do

    g = curriedProdFun(P, f)

and use g like you did before.

Note that your prodFun and this new one both throw BoundsErrors if they are 
passed empty arrays, because they unroll the loop one step (which is, 
however, helpful for making sure the return type is always consistent).

I also noticed that you're using column matrices [1; 1; 1] instead of 
vectors [1, 1, 1] in several places. I think vectors would work fine here.

 

> So, it works in this (and some other) test cases.
>
> Getting to this point required a lot of fiddling around, mainly with the 
> dump() function.  Along the journey, because I didn't really know what  I 
> was doing, I thought maybe a macro was what I wanted. I could never get it 
> to work like I wanted though.  I think the reason was because it required 
> the inputs to be expressions, but I'm not super confident.  If that's the 
> case, though, what's the point of macros?  You can make a function that 
> accepts expression arguments and can return an expression too, but of 
> course you don't have to be limited like that.  Is there an 
> under-the-hood difference between a macro and a function that operates on 
> expressions?
>

Re: macros, they are essentially functions that take one or more 
expressions and return an expression, but they run before normal code is 
executed, and paste their result into the code as if you had written it 
literally. They are for doing mechanical transformations on code, so 
they're mostly useful for readability, saving typing, or to allow 
transforming the behavior of existing code without editing it directly.

@elapsed is a good example:

https://github.com/JuliaLang/julia/blob/68a09e0f571320f96bcfb8affa2d74864ca514c9/base/util.jl#L63

    t = @elapsed [sin(x) for x in 1:1_000_000]

Has exactly the same behavior as writing

     t = let
         local t0 = time_ns()           local val = [sin(x) for x in 
1:1_000_000]           (time_ns()-t0)/1e9
    end

@elapsed has to be a macro instead of a function because if it was a 
function,

     elapsedfn([sin(x) for x in 1:1_000_000])

its argument would be evaluated before it was called, and so elapsedfn 
wouldn't have an opportunity to measure how long its argument took to run.

Macros and metaprogramming are certainly worth learning about, if nothing 
else because it's fun, but they're only useful in a relatively small number 
of cases. Plain old functions are an excellent, flexible abstraction, and 
you can generally get practically anything done by composing plain old 
functions together.

Reply via email to