3

When trying to turn a class with mutable state into an immutable one I am regularly having a hard time choosing between two alternatives: I can 1) either extract the state into another (immutable) state object and pass this state to the class with every method call (and return the new state) or 2) make the state part of the immutable class and return a new class instance with potentially modified state with every method call.

Here is an example in Scala:

This is where I'm coming from

class SomeApiWrapper(val credentials: ApiCredentials) {

  var nonce = 1L // mutable field

  def apiCall(someParam: String): Unit = {
    // make api call
    // increase nonce
  }

}

Option 1 - Separate state

class SomeApiWrapper(val credentials: ApiCredentials) {

  def apiCall(someParam: String, s: ApiWrapperState) = {
    // get nonce from state
    // api call
    // return new state with increased nonce
  }

}

Option 2 - New instance with every method call

class SomeApiWrapper(val credentials: ApiCredentials, val nonce: Long) {

  def apiCall(someParam: String): SomeApiWrapper = {
    // api call with nonce
    SomeApiWrapper(credentials, nonce + 1)
  }

}

I like the clear separation of configuration parameters (credentials) from the state in the constructor for option 1, however, it is a bit annoying that for every class I now have two (one extra for the state) and I have to pass around the state all the time.

Are there other good arguments for chosing the first or second option? Am I missing another option here? Should I do it in a completely different way? Is there some standard literature I shoudl read on this?

1
  • 1
    either extract the state into another (immutable) state object and pass this state to the class with every method call (and return the new state) Really, how is this any different from having an object's members represent the state, as implicit inputs/returns to all instance methods? It just adds complexity.
    – Alexander
    Sep 18, 2017 at 20:35

1 Answer 1

7

The point of abstractions is to simplify your code. If the design starts getting in your way, something is wrong.

Non-local mutability usually makes code more complicated. There are two general strategies to deal with this:

  • get rid of implicit state: you write pure functions that transform the input state but do not modify it. State is only modified outside of your pure system.

  • encapsulate the state: tuck it into simple objects where you can control access and modifications to the state via the public interface. Keeping a small piece of encapsulated state consistent is much easier than keeping a large amount of global state consistent.

Scala intentionally supports both of these strategies. Which strategy is better depends entirely on the context.

In this context, using an object with internally mutable fields is likely to be the preferred approach. The problem is that with each call, the old state becomes invalid and should not be used again. If you make the states explicit, there is no way to prevent the new state from being discarded and the old state from being reused. This explicit state management is most applicable when all states can be used freely, and no side effects are affected by the state. Here, making an API call would be a side effect. A strong indicator is that the API call does not return any values, and is therefore only used for a side effect.

As a secondary problem, your explicit states break encapsulation of your wrapper and require the user to keep track of the necessary internal state. Unless there is a pressing need for this design, that complicates the code unnecessarily.

It is sometimes possible to partially reconcile immutability with sequences of states by using lazy computations/futures. The future is constructed with all information necessary to perform the API call up front. When the future is accessed, the call is performed and the result stored (this mutability is externally invisible). When the value is accessed subsequently, the stored result is returned again. The return value may include the next future. That way, you can never reuse the same state because you never get direct access to the state. However, this also means that in order to get the results of a call you have to provide the parameters for the next call, which is probably unsuitable in your scenario.

Your two explict-state solutions are fundamentally equivalent, which becomes clearer if we swap the argument order around and view your object as a curried function. Your separate state solution looks like this:

wrapper = Wrapper(...)
initialState = ...
nextState = wrapper(initialState, args)

By currying the wrapper with the initial state, and having the wrapper return itself curried with the next state, we get:

wrapper = Wrapper(..., initialState)
nextWrapper = wrapper(args)
1
  • Thanks, I decided to keep the API wrapper mutable even though it is not really functional. Your argument with the side effects convinced me.
    – lex82
    Sep 26, 2017 at 12:31

Your Answer

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

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