Question Details

No question body available.

Tags

unit-testing functional-programming functional-testing rust cli

Answers (3)

Accepted Answer Available
Accepted Answer
August 5, 2025 Score: 5 Rep: 222,571 Quality: Expert Completeness: 80%

FCIS is a helpful approach when you have a reasonable amount of processing logic which can be implemented in a purely functional manner. This logic can be moved to the "functional core". On the other hand, anything which manipulates state, as well as anything which uses outer state directly needs to stay in the imperative shell part.

Let us look at your example. There is a part in the constructor which takes the sessionname and the root and creates a Command, before command.execute() is called. This part, which covers approximately 50% of the code, can be extracted into a pure function with the synopsis

 command = CreateCommand(sessionname, root)

(I am not familiar with Rust, hence I leave it to you to do the actual refactoring, you can surely spot by yourself which parts of the original function I mean). CreateCommand can now be unit tested on its own (calling command.getargs() or command.getprogram() in the test), without any need for writing mocks, and it can be called from the constructor shown in your question.

On the other hand, the code which deals with existing sessions (like targetexists(sessionname) or the part which runs the command is clearly stateful and should stay in the imperative part of the system. This part can either call CreateCommand directly, or get the function CreateCommand as a parameter (a.k.a. dependendency injection). This will allow things like replacing the original CreateCommand by a signature-identical wrapper which logs the command args to a file.

So what you achieve by the refactoring is a part of your program which becomes somewhat more unit-testable and more "pluggable". Argueably, the unit tests which are now possible will not really tell us whether the program works correctly as a whole, when tmux will be executed - that's where one needs integration tests for (not necessarily automatic ones, maybe you are satisfied with manual tests). But that is something you will always need, with or without a functional core. Maybe you find the unit-testability helpful, or maybe you say "hey, I am not interested what command.get_args() will return exactly - that's an unimportant implementation detail, the only thing which matters is the fact whether tmux behaves correctly, or not".

The need for extra unit tests depends also on the overall quality requirements of your system - will your tmux wrapper be used only by a few other devs in your own company, or is it just a self-learning example, or are you going to sell it to more than 10K other people which may use it in all kind of critical environments like your city's power station over the next decade? This is nothing you find out by just looking at the code - one needs context. Note the pros and cons of unit vs integration tests have been discussed in several older questions on this site, like this one. Still, when you come to the conclusion unit tests are a good idea for your program, having functional core parts separated from the imperative parts of the program is an effcient approach to achieve unit testability.

In the end, you have to decide by yourself whether extracting the functional core parts into functions on its own (and the gained unit testability) is worth the effort in this specific case, or if you think it is overengineered. I don't know how much other logic your wrapper contains, there may be other parts for which a purely functional implementation makes sense, or not. But there are definitely situations (different from tmux wrappers) where the parts which allow a purely functional implementation are larger or more complex, and where additional unit testability and pluggability will clearly pay off.

August 3, 2025 Score: 0 Rep: 120,196 Quality: Medium Completeness: 50%

TLDR: extract ad-hoc pure functions when it improves readability. It’s ok to leave them next to their imperative clients. But make clear what they are in some way so that maintainers will keep them pure.


A particular use case is not a good way to predict if FCIS is useful.

To be able to make use of FCIS you just need logic.

Logic that could be separated from IO, side effects, structural knowledge, whatever. Separate your logic from what it doesn’t need to know about and it becomes easier to look at.

Now if all you’re doing is wiring 3rd party component A to 3rd party component B and not applying any logic of your own then you’ll be hard pressed to find a use for pure functions. Don’t complicate it for no reason.

rather each component that makes up some cohesive functionality would have it's own pure core, and impure shell

Done right this is much better than some monolithic pure core. It gives you a chance to extract pure functions where you find them.

But let’s boil this down even more. What is needed is some way for maintainers to tell when they’re looking at purely functional code. That doesn’t have to be a core component. But it helps if it’s marked with something more than just existing as a function. Those can be impure. It doesn’t have to be structural. Heck, it could be a comment.

Do that and I’m less likely to brainlessly add the side effects you worked so hard to remove. Because you told me not to.

I don't really know if that idea is applicable for my particular use case.

Once you have pure functions in your mind set, finding uses for them is much less about use cases and more about spotting a chance to refactor confusing code into easy to read code.

At least that’s my approach. My hatred for global state and side effects is firmly grounded in how hard they make it to read code. I will not adopt a pure function implementation if it makes the code harder to read. It usually makes it easier but I never assume. I check.

Another good thing about pure functions is that they are deterministic. That’s is, they always do the same thing. Write code that behaves predictably and its tests will behave predictably. It’s nice when you don’t have to rerun a test just because you think the result was a fluke.

I also don’t assume return is the only way to respond. I like output ports as well. They can also be made predictable.

When I have time to improve existing code I constantly scan for a chance to extract a pure function. I find them everywhere. What limits me the most is thinking of a good name.

It is also worth pointing out that the formatted argument that I'm passing to tmux. It tells it to return some data about the created session. I don't really know if that should belong to io to deal with or the pure par

You're creating a wrapper. Consider tmux one of many possible implementations that could be hiding inside your wrapper. I would lean towards thinking of it as IO. You could remove logic that knows about it directly and simply pass the two ids into the constructor (I don’t like constructors that do real work anyway). Why does the wrapper need to know what it's wrapped around? All it needs to know is how to talk to it. If something else built the tmux object/session and handed it to the wrapper then the wrapper will have no job besides being the facade you wanted.

If, after doing that, there is no logic left to speak of, then you don't need any pure functions. Or mocks. Or unit tests.

But if there is some logic, that is, some behavior, that your wrapper is solely responsible for then id advise getting it under unit test. Isolate enough that it will pass or fail reliably (mock only if you must) and, so long as it doesn’t hurt readability, putting the logic in a pure function will make it deterministic and help make your tests reliable.

August 4, 2025 Score: -1 Rep: 5,231 Quality: Low Completeness: 40%

In my limited experience (desktop IDEs, script interpreters), attempts to extract a functional core are usually detrimental:

  • the imperative shell requires much more testing in the first place and has more bugs (because of concurrency and event model which maps poorly to functional core), so the "testability" argument does not hold
  • the functional core has to be exposed to testing code, violating the natural unit bounds (requires testing implementation instead of interface)

Benefits:

  • significantly improved readability
  • lots of fun

are subjective and usually not worth it.

On the other hand functional utilities are fine (but are usually provided by standard library).

Conclusion

The functional core - imperative shell approach requires a matching domain to be effective. Something where "mutation" is not the main objective.