Functional Programming – Why Some Languages Need Software Transactional Memory

clojurefunctional programminghaskellstm

Functional languages, by definition, should not maintain state variables. Why, then, do Haskell, Clojure, and others provide software transactional memory (STM) implementations? Is there a conflict between two approaches?

Best Answer

There is nothing wrong with a functional language maintaining mutable state. Even "pure" functional languages such as Haskell need to maintain state in order to interact with the real world. "Impure" functional languages like Clojure allow side effects which can include mutating state.

The main point is that functional languages discourage mutable state unless you really need it. The general style is to program using pure functions and immutable data, and only interact with "impure" mutable state in the specific parts of your code that require it. That way, you can keep the rest of your code base "pure".

I think there are several reasons why STM is more common in functional languages:

  • Research: STM is a hot research topic, and programming language researchers frequently prefer to work with functional langauges (a reasearch topic in themselves, plus it is easier to create "proofs" about program behaviour)
  • Lock don't compose: STM can be seen as an alternative to lock-based approaches to concurrency, which starts to run into problems when you scale up to complex systems by composing different components. This is arguably the main "pragmatic" reason for STM
  • STM fits well with immutability: If you have a large immutable structure, you want to make sure it stays immutable, so you don't want some other thread coming in and mutating some sub-element. Likewise, if you can guarantee immutability of said data structure, you can reliably treat is as a stable "value" in your STM system.

I personally like Clojure's approach of allowing mutability, but only in the context of strictly controlled "managed references" that may participate in STM transactions. Everything else in the language is "purely functional".

  ;; define two accounts as managed references
  (def account-a (ref 100))
  (def account-b (ref 100))

  ;; define a transactional "transfer" function
  (defn transfer [ref-1 ref-2 amount]
    (dosync
      (if (>= @ref-1 amount)
        (do 
          (alter ref-1 - amount)
          (alter ref-2 + amount))
        (throw (Error. "Insufficient balance!")))))

  ;; make a stranfer
  (transfer account-a account-b 75)

  ;; inspect the accounts
  @account-a
  => 25

  @account-b
  => 175

Note the above code is fully transactional and atomic - an external observer reading the two balances within another transaction will always see a consistent atomic state, i.e. the two balances will always sum to 200. With lock-based concurrency, this is a surprisingly hard problem to solve in a large complex system with many transactional entities.

For some extra enlightenment, Rich Hickey does an excellent job of explaining Clojure's STM in this video