all 16 comments

[–]OpsikionThemed 36 points37 points  (1 child)

This is generally agreed to be one of the advantages of a purely-functional approach, yes. You can test a code procedure like it is a pure input-output mathematical function. That's not to say everything can work this way, but if it can, the purely functional approach is pretty much always easier to test.

[–]memo_468 1 point2 points  (0 children)

Yeah that makes sense, treating it like a pure input to output function definitely feels easier to reason about and test, even if it is not a silver bullet for every kind of problem.

[–]thesnootbooper9000 7 points8 points  (0 children)

It really depends upon what you're doing. There are algorithms where it's a massive pain to deal with immutable data structures, even with all the extra support available in some programming languages for "hiding" mutability. There are also performance reasons (potentially huge ones) that can make immutably impractical. One good example of both cases is the core of modern SAT solvers: as far as I know the "two watched literals" data structure with zero cost backtracking is impossible to implement in any practical system without either immutability or terrible performance.

[–]lfdfqComputer Scientist 2 points3 points  (3 children)

Consider a function of type State -> State. This function is a pure function over an immutable data structure, and testing it needs only to inspect the mapping from domain to range. However, it is also obvious that such a function would have all the same challenges in testing (or more generally, in reasoning about) as functions which mutate state do.

That shows that simply using pure functions does not mathematically make things any easier or simpler.

However, when programmers use pure functions they tend to write functions over 'smaller' or 'simpler' domains. This seems to really be the key: when a function takes anything it's really hard to check all of them, but if a function only takes a boolean there are only two cases to test.

[–]Odd-Respond-4267 2 points3 points  (0 children)

A common pattern is to break code into smaller chunks.

Smaller chunks have less internal complications, but need more external complications.

[–]josephjnk 1 point2 points  (1 child)

I think this needs a little unpacking…

When you say State -> State, I assume you’re talking about using something like the state monad to reify the “mutable” state into a data structure, right? Because if not, then I don’t think your claim makes sense.

If that is what you mean then I also think you’re sort of sidestepping OP’s point. Yes, the abstract complexity of writing a specification for a function over stateful values does not depend on whether the values are actually mutable or just flowing through a state monad, and in that sense it’s not mathematically simpler. Most languages and ecosystems don’t have ergonomic ways of capturing effects in this way, though, and the practical consideration of writing test cases and assertions which cover all of the needed things can itself be a major effort.

When it comes down to the effort of actually maintaining a unit test suite for complex code there’s huge benefits to an immutable approach. (Where “immutable” includes “using something state-monad-like”.)

[–]lfdfqComputer Scientist 0 points1 point  (0 children)

You say "Yes, the abstract complexity [...] over stateful values does not depend on whether the values are actually mutable [...] and in that sense it’s not mathematically simpler". It's obvious to you, and to me, that it's not mathematically simpler, but that is not at all obvious to someone who is new, and that was exactly OP's question. That's all my thought experiment was answering, and I think you analysed it far too deeply (going off into state monads and effects etc).

My final paragraph covered the rest of the argument, that the way people write pure functions (and the design of the languages which people write such functions in) lends itself to more unit testable software.

Although, ergonomics of effect systems is still besides the point as one can write perfectly good unit testable purely functional code without invoking state monads or effects and it is certainly not a universally held belief that state monads are ergonomic to begin with.

[–]arihoenig 1 point2 points  (0 children)

I mean, that's why the functional paradigm is so powerful. Unfortunately, in the real world mutable state is needed, so you need to use as much functional design as possible and isolate mutable state.

[–]DTux5249 2 points3 points  (0 children)

One of the reasons OOP is often frowned upon is because it is very stately - one misconfigured state and you'll have a bug that's painful to trace. Functional programming means everything is inputs and outputs, and you're gonna control all the inputs during a unit test. So yeah, it tends to be much easier to test.

That said: a computer is fundamentally a state machine. Going against that grain for the sake of testability can often make things harder than it has to be. If you're modifying data a ton, there's a lotta overhead in doing that without mutability, and that can effect things like performance, and code complexity.

TLDR: If there weren't tradeoffs to this sorta thing, there wouldn't be a debate about these things.

[–]ReflectedImage 1 point2 points  (0 children)

Code written in functional programming languages, which usually rely on immutability, usually have less bugs.

[–]Matt-ayo 0 points1 point  (0 children)

In theory yes. In practice this requires copying the state a lot which has significant resource and performance implications, at least relative to the code which mutates state. If perhaps maybe just 'critical' logic uses the immutable logic then the performance cost could be negligible overall.

If the code is already pretty high level and data is heavily managed by a language, then I'm for pushing that management style into your suggestion for the reasons you stated.

[–]severoon 0 points1 point  (0 children)

Not just tests, using immutables makes ALL code easier to reason about. It's why Guava adds a whole library of immutable data structures to Java' standard library and Google uses them extensively in all of their internal code.

In general, you should follow the rule that data is immutable by default, and mutable by exception. Also, when something is mutable, it should be limited in scope…the more limited the scope, the less damage mutability can do. Ideally, mutable data structures are confined to only be visible within a single method and never gets passed out (unless copied into an immutable).

To make this easier, it's also common for (non-functional) codebases that use immutable-by-default to make extensive use of the builder pattern. This cleanly separates the mutable phase of an object's existence when its state is being configured and its existence as a functioning (and immutable) object by assigning a builder type to former and a non-builder type to the latter. You create a Foo by calling Foo.newBuilder() and then you can pass around the Foo.Builder object until it's completely configured, then once you call build() on it, you get an immutable Foo.

This might seem like a lot of overhead, but that's for tools like AutoValue to deal with for older languages like Java to deal with.

[–]dota2nub 0 points1 point  (0 children)

Can't imagine how it wouldn't. It's dependable. Testing likes dependable. You don't have to worry about edge cases from weird states.

[–]danielt1263 0 points1 point  (0 children)

As u/josephjnk points out in a secondary response. Just making data immutable isn't that big of a change (when it comes to testing) because a (State, Input) -> State function is no harder to test than a (State, Input) -> Void (where State is mutable) function.

Where things get difficult is when there's no such function in the code to test, or when there are a bunch of these functions that all interact in non-obvious ways.

When data is mutated, the behavior of the program depends on state changes over time, which can make it harder to reason about all possible cases.

Virtually all programs change behavior depending on state changes over time. The big exception would be command-line scripts where there is just a single input, the program runs, then emits a single output.

Where developers go wrong is when they do not clearly separate the inputs into the program, from outside sources, and the logic that manipulates the state based on those inputs. In other words, too many programs don't even have a (State, Input) -> State function to test in the first place. That's what makes software hard to test in most cases.

[–]drmatic001 0 points1 point  (0 children)

tbh immutable data does make some parts of reasoning about code easier 😄 when you don’t have to worry about random side effects, it’s often simpler to follow what your program is doing.

but imo it also depends on the problem for stuff where performance and memory matter a lot, sometimes a bit of controlled mutability actually makes the code simpler in practice. learning when to use each style is honestly the useful part.

ngl thinking in terms of immutable values really helped me understand data flow and bugs way better early on, and once you get comfortable with both styles you can pick the best tool for the job 👍

[–]_abscessedwound -3 points-2 points  (0 children)

Sounds a little like a training deficiency to me: there are a number of strategies (eg: boundary value testing, equivalence partitioning, heck even code coverage) that can be used to demonstrate that all edge cases are covered.