Julia: function composition: how to correctly split arguments?

How to define functions/functors and split arguments?

I have a struct of fixed parameters, P, a mutable struct of variables, V, and three constructors for methods F, G, and H, where H is the composition of F and G. The constructor for F has three parts: (1) The struct that holds parameters F; (2) a function f(q,x) that unpacks its arguments and returns some calculation; (3) a functor-type method F(q)(x) defined on struct F. The argument tuple (q,x) is split between a typed q and some vector x. The construction of G and H is similar.

The main purpose of this design is to be able to change the functional forms and the parameters while keeping the overall architecture. I’ve managed to make this work in simple cases, but am having difficulty when P and V hold more than one parameter. So let me review what appears to perform as intended and what doesn’t.

Simple case: F is typed with a single fixed parameter a and an instance can be created with F(a0)(x0), for some concrete a0 and x0. Works.

Mixed types: G is typed with both a fixed parameter and a variable, b and β, and an instance can be created with G(b0, β0)(x0), for some concrete b0, β0 and x0.

Composition: H is typed with both P and V and the method must properly unpack and access the fields. That is, the argument tuple q holds an instance p of P and vof V, like so q = p, v. I can get a composition function h(q,x) to work, but I can’t get a method defined directly on the type H to work (i.e. H(q)(x) does not work). I have tried several variations of H(q,x) without success. Trying to pick up a hint from the error message, I tried to type with ::Tuple{P,V}. I also tried to define H(q)(x) as an inner method, but that didn’t work at all.

Thanks for your suggestions!

"""
`P`
struct to hold all fixed parameters
"""
struct P
    a :: Float64  # not used in this example
    b :: Vector{Float64}
end
function P(; a = 1.0, b = [2.0, 3.0])
    P(a, b)
end 


"""
`V`
mutable struct to hold all variable parameters
"""
mutable struct V
    α :: Float64
    β :: Vector{Float64}
end
function V(; α = 0.0, β = [0.0, 0.0])
    V(α, β)
end 


"""
`F(x)`
functor method defined on struct holding fixed parameter a
"""
struct F
    a :: Float64
end
(dummy::F)(x) = f(dummy.a, x)  # one-line functor definition
function f(a, x)
    return x.^(a)  # note the broadcasting dot
end


"""
`G(b, β, x)`
   b :: Vector{Float64}
   β :: Vector{Float64}
"""
struct G
    b :: Vector{Float64}
    β :: Vector{Float64}
end
function (q::G)(x)
    return g(q, x)
end
function g(q, x)
    b = q.b;  β = q.β
    return sqrt.(b .* β) * x
end


"""
`H(q, x)`
functor method defined as the composition of F and G defined on a struct typed with `p::P` (fixed parameters) and `v::V` (variables)
"""
struct H{FG}
    h :: FG
end
function (q::H)(x)
    return h(q, x)
end 
function h(q, x)
    p, v = q                 # split pars/vars
    a, b, β = p.a, p.b, v.β  # unpack
    return (F(a) ∘ G(b,β))(x)
end 

A few checks to see that F and G work as expected, while H does not (but h does):

# Check F:
((a,x) -> x.^(a))(2.0,2.0)
## 4.0

F(2.0)(2.0)
## 4.0 

# Check G:
((b,β,x) -> sqrt.(b.*β)*x)([2.0, 3.0], [4.0, 5.0], 2.0)
## 2-element Vector{Float64}:
##  5.656854249492381
##  7.745966692414834

G([2.0, 3.0], [4.0, 5.0])(2.0)
## 2-element Vector{Float64}:
##  5.656854249492381
##  7.745966692414834

# Create instances of P() and V()
p0 = P(a = 2.0)
## P(1.0, [2.0, 3.0])

v0 = V(β = [4.0, 5.0])
## V(0.0, [4.0, 5.0])

# Check H:
(F(2.0) ∘ G([2.0, 3.0], [4.0, 5.0]))(2.0)
## 2-element Vector{Float64}:
##  32.00000000000001
##  60.00000000000001

(F(p0.a) ∘ G(p0.b, v0.β))(2.0)
## 2-element Vector{Float64}:
##  32.00000000000001
##  60.00000000000001

h((p0, v0), 2.0)
## 2-element Vector{Float64}:
##  32.00000000000001
##  60.00000000000001

H((p0,v0))(2.0)
## ERROR: LoadError: MethodError: no method matching iterate(::H{Tuple{P, V}})

H(p0,v0)(2.0)
## ERROR: LoadError: MethodError: no method matching H(::P, ::V)

Instead of:

It should be:

function (q::H)(x)
    return h(q.h, x)
end 
1 Like

Sweet! Thanks heliosdrm!

and you call it with:

H((p0,v0))(2.0)

P.S. q.h that’s voodoo?

the argument q of your function (q::H) is of type H, and your type H has a field h, thus the q.h.

2 Likes

Makes perfect sense, thanks Leandro!

1 Like