4

When trying to follow a functional programming paradigm, I often find myself in a situation where I have a chain of functions that I would want to combine/compose somehow, but they all also take in a similar set of parameters. I've been trying to research best practices surrounding this kind of set up, because it's not immediately obvious how to combine them, and I don't want to end up with some esoteric solution. I'm also open to the possibility that there may be ways to avoid this sort of problem entirely.

To be more concrete, I noticed the trend working on two unrelated projects: interacting with a peculiar API and an agent based simulation.

  • For the API, I needed to make multiple calls to get the information I needed. Each call needing some information from the last, and each call needed the same general information (like the url, various object ids, and some option parameters).
  • For the agent based simulation, in each iteration I needed to make a series of updates to the population of agents as they interacted and evolved through various stages. Each stage needed the state of the previous stage's update, and each stage needed the same general information (like parameters dictating how the agents interact and change, and how to create new agents, etc)

These aren't the only two examples, just the most recent, and usually in the past I end up giving up and going back to OOP. It sort of makes sense, because if I have a chain of functions all with the same first parameter, that's sort of like how all object methods in Python take self as the first argument. But I'm sure the functional paradigm has another way of tackling this.

Hopefully that makes the problem clear. In sum, what are standard ways to handle these "ambient" parameters in a functional context?


Some paths I've tried that may further clarify the issue

One potential solution I thought about was a sort of composition "lift". For example, if I have two function f :: X -> (A -> B) and g :: X -> (B -> C), I could compose them into a function of type X -> (A -> C) with an implementation like lift_compose f g x = (f x) . (g x). However, I don't have the aesthetic knowledge to know if this is considered "clean", or the vocabulary to know if this is already called something standard.

It also becomes a problem for longer chains of functions functions, like if h :: X -> (C -> D) actually also needs a little information from what type A was in f. In that case the composition isn't as straightforward. Something I tried was having a series of data types that inherited fields from each other, and each function essentially added a field to the data and passed it on. For example:

type A:
    ambient_info : str
    more_info : str

type B (inherits from A)
   new_info : str

type C (inherits from B)
   newer_info : str

Then I could make the functions just f :: A -> B and g :: B -> C and so on for however many steps, and compose normally, keeping all the information throughout. However, this started to feel strange and non standard. Again, this is pretty hard to search because I just don't have the right words. Thanks for any help.


Also, I wrote these examples in a weird pseudo Haskell, but I'm looking for something as language agnostic as practical, since I don't actually get to work in Haskell very much. Python is my main fluency, but I see this as generally affecting me whatever language I'm writing in.

2 Answers 2

4

One alternative is, in case these "ambient" parameters are some sort of context, is to use Monads.

It really depends on your use-case whether that makes sense or not, so let's use an example of Transaction. Instead of passing in some database connection to each function, you could instead return something that needs a database connection. For example:

f :: a -> Transaction b

If you're specifically need to modify some things in each function, a State monad may be the thing you're looking for. Same concept, function will return something that will still need an initial state to "really" run.

Whether that is language agnostic and compose? Well, Haskell has the do notation, Scala has for-comprehensions. If all else fails, you can just use flatMap directly. I've been doing the latter in Java. Works up to a point, but admittedly gets pretty unreadable real fast.

5
  • I'm sure you're right, but I'm having issues mentally closing the gap between the problem and this solution. Could I trouble you to translate the weird "lift compose" / "type chaining" solutions into monadic language? I'll keep reading for sure, but I feel like I keep learning about monads and not seeing how they apply
    – Cordello
    Commented Jul 1, 2022 at 17:59
  • Ok, so provided that f :: a -> m b and g :: b -> m c, where we switch the X "ambient" parameter to some monad that we return, either State or Reader or some custom one, then your "lift_compose" is just f >=> g. Where f >=> g :: a -> m c, i.e. the composed function. Note, that the monad still needs to "run", but that only needs to happen once, it does not matter for composition. Does that help? Commented Jul 1, 2022 at 20:49
  • I might get it for Reader. The Reader itself is like a function X -> whatever, and my f and g could be f :: A -> (X -> B) and g :: B -> (X -> C). These could compose via >=> to A -> (X -> C) and then once it has an input A, to "run it", you just need to give it an environment X. Am I on the right track?
    – Cordello
    Commented Jul 3, 2022 at 4:12
  • I've convinced myself this makes sense. In the same way, the State monad is like a function s -> (a, s). The only thing I'm hung up on is what to do when the type of information in the context is changing, not just the value, like in my second solution attempt. But I guess there are more ways to combine functions that just composition
    – Cordello
    Commented Jul 7, 2022 at 21:34
  • I think you're getting it. If the "additional" types change, that is still composable. So for a State, your function can just have an s (in State s a) of whatever it specifically needs. Functions that call this function will have to potentially project, create or "zoom in/out" the state. In any case, there are tool for that. What you can't do however, is to compose monads or monad stacks that are different. Once you have Reader, for example, pretty much all your functions will have to work with Reader. Or State, or whatever. Commented Jul 9, 2022 at 21:09
1

Objects can be used in a pure functional way. In a language like Scala with good object support, often the cleanest way is to just use an object.

You can also take advantage of scoping inside functions or let blocks. If you define a function within the scope of another function, you don't have to pass the outer function's parameters around.

A reader monad is useful to be able to inject a value after your steps are defined instead of passing it through the definition code. It doesn't really help simplify function signatures, though, because it basically moves the dependency from an input to an output of your function, but it does simplify the composition code.

State monads can be really nice for your agent simulation example. People new to FP sometimes try to overuse state monads, thinking of them of a way to simulate mutability, when they really should just be using a fold or something. However, for appropriate situations, state monads almost feel like magic, because they resemble imperative code but are fully referentially transparent.

2
  • Could you elaborate on how the reader monad moves the dependency from an input to an output?
    – Cordello
    Commented Jul 1, 2022 at 18:02
  • It changes from something like f :: r -> x -> a to f :: x -> Reader r a. The environment r moves. This argument only needs to be supplied once, instead of being passed all over, so it simplifies the composition but not the function signatures. Commented Jul 2, 2022 at 8:31

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.