3

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?

Will Ness
  • 70,110
  • 9
  • 98
  • 181
Joseph S
  • 180
  • 1
  • 10
  • 3
    That is a lot of arguments. Have you considered making a type (or multiple types) to organize some (sub)collection of those parameters? – David Young Oct 30 '22 at 19:09
  • yes I plan to wrap the tuples to data types later. I'm curious how does functional programmers compose this big function from smaller functions. – Joseph S Oct 30 '22 at 19:16
  • Do you not want to use a `let` expression (which is what you should be using in your question; at least write valid Haskell instead of this pseudocode)? – chepner Oct 30 '22 at 20:12
  • code is written in julia. I was curious if haskell has a way to compose functions better. AFIK `let` expression is similar to calculating intermediate values in julia. – Joseph S Oct 30 '22 at 20:21
  • I'm a bit confused. What does this have to do with function composition? You just have a function that calls other functions, but this is nothing to do with composition, as far as I understand the term. What I _do_ see is a strong need to introduce types to clean up the input arguments. – DNF Oct 30 '22 at 20:33
  • 2
    "You just have a function that calls other functions" this is essentially function composition. If all the functions has only one argument its easier to compose them with a pipe operator in julia. I was wondering if there's any similar methods for multiple arguments in functional programming. right now all the top level functions has two duties 1- do the calculations 2- collect arguments for inner functions. If there was a way to compose just like those single argument functions, then all functions only need to worry about doing the calculations which makes them easier to test. – Joseph S Oct 30 '22 at 20:54
  • No, that's just a normal function. Function composition is a _higher order function_ that takes two (or more) functions as input arguments and returns a new function. It's basically `compose(f, g) = x->f(g(x))`. In Julia the composition function, `∘`, is available in Base (https://docs.julialang.org/en/v1/base/base/#Base.:%E2%88%98), and does exactly that, and it also composes functions that take arbitrary numbers of input arguments. I don't see the relevance to your question, though, as your function just returns values, not functions. – DNF Oct 30 '22 at 22:21
  • 1
    @DNF *A* function composition is distinct from *the* function composition *operator*. This is like the difference between a sum and the addition operator. The latter has two parameters. The former is a number (with no parameters), and you are just emphasizing the fact that you arrived at that number by using the addition operator when you call it "a sum". Likewise for a function composition (or a "composition of functions"). A "function composition" is a ("just") function, but you're just emphasizing that you happened to get it by using the function composition operator – David Young Oct 31 '22 at 01:58
  • @DavidYoung Whether it's the composition operator or the result of the operation, it still involves higher order functions, which the OP does not. I'm just trying to figure out what the OP is looking for, and unclear terminology does not help. Referring to "a function calling a function" as an instance of 'function composition' is meaningless, since _all_ functions call other functions. It is _truly_ confusing what the OP is looking for, I'm not just quarreling. – DNF Oct 31 '22 at 06:06
  • @DNF compose function, composition operator(nouns) etc are just a mechanism to perform functional composition on single variable functions. when you write `f(g(x)` you are essentially applying the transformations described for each function one after the other on the data x. I can do the same thing `compose(f, g)` in an imperative style like `a = g(x)`, and then `f(a)`. This code also performing function composition(verb) with additional ceremony. When call the function f(g(x)) the result of g(x) is passed to `f` without any intermediate variable. All of them doing the same thing. – Joseph S Oct 31 '22 at 06:36
  • @DNF compose function just abstract out the function application with higher order functions which is fine and dandy for single variable functions but does not work for polyvariadic functions. I was hoping for some monad magic to compose the my main function with lesser ceremony as I have hundreds of such functions to deal with. Or is it even practical – Joseph S Oct 31 '22 at 06:46
  • @JosephS Firstly, function composition _is_ that abstraction. If you are not somehow using higher order functions, it is not function composition. It specifically has to do with the creating functions from from functions, not with the computational result of applying the composed function. That distinction is crucial. The 'abstraction' is the point. Secondly, you _can_ indeed do this on functions with multiple input arguments, and this is supported by the Julia composition operator, which I linked to further up. – DNF Oct 31 '22 at 07:43
  • @DNF Composing function calls. For example, suppose we have two functions f and g, as in z = f(y) and y = g(x). Composing them means we first compute y = g(x), and then use y to compute z = f(y). Here is the example in the C language: float x, y, z; // ... y = g(x); z = f(y); The steps can be combined if we don't give a name to the intermediate result: z = f(g(x)); source: https://en.wikipedia.org/wiki/Function_composition_(computer_science)#Composing_function_calls – Joseph S Oct 31 '22 at 09:07
  • I think I've explained this as much as I can in this space, so I won't continue. But I suggest that you focus your question a bit more, with a more minimal MWE, and emphasize exactly the one main thing you are looking to accomplish. – DNF Oct 31 '22 at 11:12

3 Answers3

2

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
  ]
Noughtmare
  • 9,410
  • 1
  • 12
  • 38
2

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.

Will Ness
  • 70,110
  • 9
  • 98
  • 181
  • 1
    You might already be aware of it yourself, but maybe it's worth mentioning in your answer that your "recasting of functions ... such that they return functions with one argument" is similar in spirit to [currying](https://en.wikipedia.org/wiki/Currying). I would say that in your answer you're somehow doing an incomplete currying, as it is enough for the needs in this particular case. – Sebastian Nov 06 '22 at 17:37
  • 1
    @Sebastian yes. :) perhaps I could also mention State monad -- augmented function composition, functions from (val,state) to (val,state)... – Will Ness Nov 06 '22 at 20:48
  • I wouldn't mind : ) – Sebastian Nov 06 '22 at 21:15
0

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)
DNF
  • 11,584
  • 1
  • 26
  • 40
  • Yes, composition operator can take multiple arguments. Its not helpful in this context. We cannot align the output tuples to the next function coz its only makes things worse than my imperative example. I saw some examples where functional programmers use `lift` combinators but its not that flexible. Another way is to chain monads together. I just wanted to know how the functional programmers handle this kind of problems? Is it practical to use monads or still stick to imperative style? This is also why I tagged `haskell` unfortunately its removed by a moderator. – Joseph S Nov 03 '22 at 10:07
  • Also what do you mean by reasonable types? I can wrap all that params to some object type so I can get some help from the IDE? Its not a big problem that needs solving as of now. As the production code will be written in statically typed language where I can do easy destructuring. – Joseph S Nov 03 '22 at 10:25
  • The reason your code is hard to read is that you are juggling a large number of arguments. IMO, if your function has more than 4 inputs, you should start organizing them in types. 18 inputs is far beyond anything reasonable. For example, can't `t0, ts, t_start, t_end` be collected in a `Timing` type? And the others in a `Material` type? What problem are you trying to solve, if it isn't making the code cleaner? That's what I mean by reasonable types: put logically connected values together. Organize your code the same way as you intend in your production code. – DNF Nov 03 '22 at 10:35
  • But focusing on your specific question: Can you type out a pseudo-code version of you 'minimal, conceptual example', showing what you are looking for. Based on mine being basically the only feedback you've received, it seems highly likely that you are not phrasing your question clearly enough. And finally, you are much more likely to get productive feedback and helpful discussion on https://discourse.julialang.org/ , which is a forum dedicated to back-and-forth discussion. – DNF Nov 03 '22 at 10:37
  • I do not what to go into whole static vs dynamic typed debate lol. Just know that sometimes its perfectly reasonable to not care about types. – Joseph S Nov 03 '22 at 10:43
  • 1
    I'm not debating static vs dynamic types in any way. I'm just talking about code organization. As I said, what is the purpose of your question, if it isn't cleaning up your code, making it more readable, simple, testable and debuggable? – DNF Nov 03 '22 at 10:45