Search code examples
functional-programmingjuliaargumentsparameter-passingfunction-composition

function composition for multiple arguments and nested functions


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?


Solution

  • 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.