Sorry for yet another FP + side effects question, but I couldn't find an existing one which quite answered this for me.
My (limited) understanding of functional programming is that state/side effects should be minimised and kept separate from stateless logic.
I also gather Haskell’s approach to this, the IO monad, achieves this by wrapping stateful actions in a container, for later execution, considered outside the scope of the program itself.
I’m trying to understand this pattern, but actually to determine whether to use it in a Python project, so want to avoid Haskell specifics if poss.
Crude example incoming.
If my program converts an XML file to a JSON file:
def main():
xml_data = read_file('input.xml') # impure
json_data = convert(xml_data) # pure
write_file('output.json', json_data) # impure
Isn’t the IO monad’s approach effectively to do this:
steps = list(
read_file,
convert,
write_file,
)
then absolve itself of responsibility by not actually calling those steps, but letting the interpreter do it?
Or put another way, it’s like writing:
def main(): # pure
def inner(): # impure
xml_data = read_file('input.xml')
json_data = convert(xml_data)
write_file('output.json', json_data)
return inner
then expecting someone else to call inner()
and saying your job is done because main()
is pure.
The whole program is going to end up contained in the IO monad, basically.
When the code is actually executed, everything after reading the file depends on that file’s state so will still suffer from the same state-related bugs as the imperative implementation, so have you actually gained anything, as a programmer who will maintain this?
I totally appreciate the benefit of reducing and isolating stateful behaviour, which is in fact why I structured the imperative version like that: gather inputs, do pure stuff, spit out outputs. Hopefully convert()
can be completely pure and reap the benefits of cachability, threadsafety, etc.
I also appreciate that monadic types can be useful, especially in pipelines operating on comparable types, but don’t see why IO should use monads unless already in such a pipeline.
Is there some additional benefit to dealing with side effects the IO monad pattern brings, which I’m missing?