2

I've been using a functional approach in my programming of late. I know that mutability and state changes are big no nos within the paradigm. I now find myself working with databases and other data structures where state must be changed. I've been trying to learn about monads and other means of interacting with state, but I'm struggle to grasp the concept.

I was wonder, what's the proper way to deal with state and necessary state changes? What sort of functional constructs can be used and how can I use them. And more importantly, how should I be thinking about the interaction while I'm coding?

Would this be a scenario where it would be a good idea to switch to OOP or some other paradigm? If I do that, how should I think about that interaction and working across paradigms.

2 Answers 2

5

Functional programming encompasses a number of techniques, depending on who you ask:

  • higher-order functions,
  • closures,
  • referential transparency, and
  • making state explicit.

The value of using pure functions is that these functions can be composed, without side effects leading to unexpected behaviour. Their behaviour is characterized solely by their inputs and outputs. As such, they are easier to reason about. Ideally, state should not be an implicit global context, but an explicit parameter/return value of a function.

So far, the theory. In practice, the vast majority of functional languages including the Lisp and ML families is also imperative: State may be implicit in the control flow, and statements may have side effects. It is not necessary to always make the state explicit. Other aspects of functional programming still apply.

The odd one is Haskell. Because Haskell uses lazy evaluation, expressions are not evaluated in any particular order. If expressions have side effects, the order of these effects is unspecified. Therefore, Haskell introduces the IO monad and the State monad to encode dependencies between effects on a type-system level. These monads allow a computation to be forced even when the result is thrown away or when the operation has no clear result.

In languages without lazy evaluation, the need for state or IO monads is greatly reduced. In fact, most languages do not have a sufficiently expressive type system to encode these monads. It is therefore wrong to see Haskell-specific techniques as the only way to be truly functional. Mixing functional and imperative techniques is perfectly fine.

8

I'd like to offer a slightly different perspective that may help with your current problem.

Don't think of state changes as bad. After all, if there were no state changed in your program, you could safely replace it with a no-op.

The problem with state change is that it's notoriously hard for the developer and the system to reason about. This is made worse if the state changes are spread widely through the program and especially true if they're implicit/hidden.

What functional programming promotes with concepts like purity and immutability isn't so much the removal of state change. It's more about making the state change transparent and encapsulated.

So, to take your example, one approach would be to have the logic that generates the DB commands simply accumulate the commands but not execute them. Once you have them all then you can have one component that executes the commands.

Further, if your generation logic accepts the current list of commands and returns a new list, with the changes it made, then you have purity in the generation. The advantage is that it's easier to reason about, and test, your logic functions.

It may appear that all you've done is to move the complexity around and that is, to some extent, true. But note, the code that handles the mutability is pretty specific: it doesn't have any generation logic, it just executes commands. Anything you needed to change wrt command batching etc would be encapsulated there.

Where monads tend to be useful is when you need to maintain order and dependency information i.e. if you work out what you need to do then do it straight away they're not very helpful. But note, in the scheme above, there is a conceptual delay between the generation and execution so they could be useful. Once you factor in error paths then their rigour can be invaluable.

And, in practice, it's the delay between actions that I find is a good indicator that a monad may be useful. Even outside of functional languages. For example, error handling and asynchronous code often benefit from monads. interestingly, these are the areas where monads have had crossover e.g. futures are fairly common in non-functional languages.

As an aside, this is my view as to why monads have been particularly prevalent in Haskell. It's not so much that the type system can express them well, though this helps. It's that, due to Haskell's laziness, almost everything has a delay between code execution and the effect occurring.

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.