function composition for multiple arguments and nested functions

986 Views Asked by At

I have a pure function that takes 18 arguments process them and returns an answer. Inside this function I call many other pure functions and those functions call other pure functions within them as deep as 6 levels.

This way of composition is cumbersome to test as the top level functions,in addition to their logic,have to gather parameters for inner functions.

# Minimal conceptual example
main_function(a, b, c, d, e) = begin
    x = pure_function_1(a, b, d)
    y = pure_function_2(a, c, e, x)
    z = pure_function_3(b, c, y, x)
    answer = pure_function_4(x,y,z)
    return answer
end
# real example
calculate_time_dependant_losses(
    Ap,
    u,
    Ac,
    e,
    Ic,
    Ep,
    Ecm_t,
    fck,
    RH,
    T,
    cementClass::Char,
    ρ_1000,
    σ_p_start,
    f_pk,
    t0,
    ts,
    t_start,
    t_end,
) = begin
    μ = σ_p_start / f_pk
    fcm = fck + 8
    Fr = σ_p_start * Ap
    _σ_pb = σ_pb(Fr, Ac, e, Ic)
    _ϵ_cs_t_start_t_end = ϵ_cs_ti_tj(ts, t_start, t_end, Ac, u, fck, RH, cementClass)
    _ϕ_t0_t_start_t_end = ϕ_t0_ti_tj(RH, fcm, Ac, u, T, cementClass, t0, t_start, t_end)
    _Δσ_pr_t_start_t_end =
        Δσ_pr(σ_p_start, ρ_1000, t_end, μ) - Δσ_pr(σ_p_start, ρ_1000, t_start, μ)

    denominator =
        1 +
        (1 + 0.8 * _ϕ_t0_t_start_t_end) * (1 + (Ac * e^2) / Ic) * ((Ep * Ap) / (Ecm_t * Ac))
    shrinkageLoss = (_ϵ_cs_t_start_t_end * Ep) / denominator
    relaxationLoss = (0.8 * _Δσ_pr_t_start_t_end) / denominator
    creepLoss = (Ep * _ϕ_t0_t_start_t_end * _σ_pb) / Ecm_t / denominator
    return shrinkageLoss + relaxationLoss + creepLoss
end

I see examples of functional composition (dot chaining,pipe operator etc) with single argument functions.

Is it practical to compose the above function using functional programming?If yes, how?

3

There are 3 best solutions below

3
Will Ness On BEST ANSWER

The standard and simple way is to recast your example so that it can be written as

# Minimal conceptual example, re-cast 
main_function(a, b, c, d, e) = begin
    x = pure_function_1'(a, b, d)()
    y = pure_function_2'(a, c, e)(x)
    z = pure_function_3'(b, c)(y)     // I presume you meant `y` here
    answer = pure_function_4(z)      // and here, z
    return answer
end

Meaning, we use functions that return functions of one argument. Now these functions can be easily composed, using e.g. a forward-composition operator (f >>> g)(x) = g(f(x)) :

# Minimal conceptual example, re-cast, composed
main_function(a, b, c, d, e) = begin
    composed_calculation = 
        pure_function_1'(a, b, d) >>>
        pure_function_2'(a, c, e) >>>
        pure_function_3'(b, c)    >>>
        pure_function_4

    answer = composed_calculation()
    return answer
end

If you really need the various x y and z at differing points in time during the composed computation, you can pass them around in a compound, record-like data structure. We can avoid the coupling of this argument handling if we have extensible records:

# Minimal conceptual example, re-cast, composed, args packaged
main_function(a, b, c, d, e) = begin
    composed_calculation = 
                   pure_function_1'(a, b, d) >>> put('x') >>>
      get('x') >>> pure_function_2'(a, c, e) >>> put('y') >>>
      get('x') >>> pure_function_3'(b, c)    >>> put('z') >>>
      get()    >>> pure_function_4

    answer = composed_calculation(empty_initial_state)
    return value(answer)
end

The passed around "state" would be comprised of two fields: a value and an extensible record. The functions would accept this state, use the value as their additional input, and leave the record unchanged. get would take the specified field out of the record and put it in the "value" field in the state. put would mutate the extensible record in the state:

put(field_name) = ( {value:v ; record:r} =>
  {v ; put_record_field( r, field_name, v)} )

get(field_name) = ( {value:v ; record:r} =>
  {get_record_field( r, field_name) ; r} )

get() = ( {value:v ; record:r} =>
  {r ; r} )

pure_function_2'(a, c, e) = ( {value:v ; record:r} =>
  {pure_function_2(a, c, e, v); r} )

value(r) = get_record_field( r, value)

empty_initial_state = { novalue ; empty_record }

All in pseudocode.

Augmented function application, and hence composition, is one way of thinking about "what monads are". Passing around the pairing of a produced/expected argument and a state is known as State Monad. The coder focuses on dealing with the values while treating the state as if "hidden" "under wraps", as we do here through the get/put etc. facilities. Under this illusion/abstraction, we do get to "simply" compose our functions.

0
Noughtmare On

I can make a small start at the end:

sum $ map (/ denominator)
  [ _ϵ_cs_t_start_t_end * Ep
  , 0.8 * _Δσ_pr_t_start_t_end
  , (Ep * _ϕ_t0_t_start_t_end * _σ_pb) / Ecm_t
  ]
6
DNF On

As mentioned in the comments (repeatedly), the function composition operator does indeed accept multiple argument functions. Cite: https://docs.julialang.org/en/v1/base/base/#Base.:%E2%88%98

help?> ∘
"∘" can be typed by \circ<tab>

search: ∘

  f ∘ g


  Compose functions: i.e. (f ∘ g)(args...; kwargs...) means f(g(args...; kwargs...)). The ∘ symbol
  can be entered in the Julia REPL (and most editors, appropriately configured) by typing
  \circ<tab>.

  Function composition also works in prefix form: ∘(f, g) is the same as f ∘ g. The prefix form
  supports composition of multiple functions: ∘(f, g, h) = f ∘ g ∘ h and splatting ∘(fs...) for
  composing an iterable collection of functions.

The challenge is chaining the operations together, because any function can only pass on a tuple to the next function in the composed chain. The solution could be making sure your chained functions 'splat' the input tuples into the next function.

Example:

# splat to turn max into a tuple-accepting function
julia> f = (x->max(x...)) ∘ minmax;

julia> f(3,5)
    5

Using this will in no way help make your function cleaner, though, in fact it will probably make a horrible mess.

Your problems do not at all seem to me to be related to how you call, chain or compose your functions, but are entirely due to not organizing the inputs in reasonable types with clean interfaces.

Edit: Here's a custom composition operator that splats arguments, to avoid the tuple output issue, though I don't see how it can help picking the right arguments, it just passes everything on:

⊕(f, g) = (args...) -> f(g(args...)...)
⊕(f, g, h...) = ⊕(f, ⊕(g, h...))

Example:

julia> myrev(x...) = reverse(x);

julia> (myrev ⊕ minmax)(5,7)
(7, 5)

julia> (minmax ⊕ myrev ⊕ minmax)(5,7)
(5, 7)