From what I gathered from learning Haskell, functional programming limits the amount of side effects, but in what ways? Hope someone can enlighten me on this one.
There are two different definitions of the term "functional programming". But neither of the two limits side-effects.
The original definition was "programming with first-class and higher-order subroutines". This was the definition applied to LISP, Scheme, ML, and many others. Nowadays, almost every language (with the exception of C) has first-class and higher-order subroutines, the last major holdouts were Java and C++. So, this definition is less useful than it used to be, since almost every language fulfills it.
So, later a new definition became popular: "programming without side-effects". This is the definition which is used in Haskell, for example.
However, neither of the two definitions limits side-effects. The first definition doesn't say anything about side-effects at all (and early LISP as well as many of its descendants, e.g. CommonLisp are very much imperative). And the second definition doesn't limit side-effects, it simply forbids them entirely.
Haskell is a lazy language. A piece of code is only executed if its value is used. This means that if we want to cause side effects in a specific sequence, we must model the order of effects as a value where the later effects require the value of previous effects.
In an imperative language like C, we can simply use semicolons to sequence statements:
In Haskell, to sequence the effects of printing a value, we must express this through the data flow. Something like:
forceEvaluationInSequence [firstEffect, secondEffect] where firstEffect = putStr "foo" secondEffect = putStr "bar"
These values symbolizing external effects are represented in Haskell as the IO monad. The
main function of a program returns an IO value. The
>> operator can be used for sequencing between monads:
main = putStr "foo" >> putStr "bar"
Because monads are central to Haskell they have special syntax: the do-notation. This code is equivalent to the above:
main = do putStr "foo" putStr "bar"
(Because of the similarity in notation to imperative programming, monads are sometimes called “programmable semicolons”.)
So in Haskell, there are no longer side effects. An effect is modeled explicitly as a value. This aligns well with the idea of substitutability/equational reasoning: in a pure functional language, I can replace the expression with the value of that expression.
Haskell code, by default, does not produce or rely on side effects. If you want to write code that uses side effects, it has to be written in a different imperative style and its type has to explicitly declare that side effects are being performed within it.
This is all done through the type system. All library functions that produce side effects and a result of type
a have a result type of
IO a. Pure code has no way to take the
IO wrapper off of this value produced by the library function