Functional programming includes many different techniques. Some techniques are fine with side effects. But one important aspect is equational reasoning: If I call a function on the same value, I always get the same result. So I can substitute a function call with the return value, and get equivalent behaviour. This makes it easier to reason about the program, especially when debugging.
Should the function have side effects, this doesn't quite hold. The return value is not equivalent to the function call, because the return value doesn't contain the side effects.
The solution is to stop using side effects and encoding these effects in the return value. Different languages have different effect systems. E.g. Haskell uses monads to encode certain effects such as IO or State mutation. The C/C++/Rust languages have a type system that can disallow mutation of some values.
In an imperative language, a print("foo")
function will print something and return nothing. In a pure functional language like Haskell, a print
function also takes an object representing the state of the outside world, and returns a new object representing the state after having performed this output. Something similar to newState = print "foo" oldState
. I can create as many new states from the old state as I like. However, only one will ever be used by the main function. So I need to sequence the states from multiple actions by chaining the functions. To print foo bar
, I might say something like print "bar" (print "foo" originalState)
.
If an output state is not used, Haskell doesn't perform the actions leading up to that state, because it is a lazy language. Conversely, this laziness is only possible because all effects are explicitly encoded as return values.
Note that Haskell is the only commonly used functional language that uses this route. Other functional languages incl. the Lisp family, ML family, and newer functional languages like Scala discourage but allow still side effects – they could be called imperative–functional languages.
Using side effects for I/O is probably fine. Often, I/O (other than logging) is only done at the outer boundary of your system. No external communication happens within your business logic. It is then possible to write the core of your software in a pure style, while still performing impure I/O in an outer shell. This also means that the core can be stateless.
Statelessness has a number of practical advantages, such as increased reasonability and scalability. This is very popular for web application backends. Any state is kept outside, in a shared database. This makes load balancing easy: I don't have to stick sessions to a specific server. What if I need more servers? Just add another, because it's using the same database. What if one server crashes? I can redo any pending requests on another server. Of course, there still is state – in the database. But I've made it explicit and extracted it, and could use a pure functional approach internally if I want to.