I work in OCaml, which is also a functional language, but prints can be added in single lines. I address this point in Lecture 19 (Imperative Programming), actually, but my perspective is -- we invented immutability and purity to serve us, but we need not be fanatically beholden to it. In my opinion, I think Haskell goes in that direction, when every usage of IO now needs the IO monad to get involved.
A little mutability is OK. Functional programming is about the avoidance of side effects, more than simply forbidding it.
-- Before
captureAudioDuration :: DeviceID -> DiffTime -> IO WaveData
-- After
captureAudioDuration' :: MonadIO m => DeviceID -> DiffTime -> m WaveData
You can build the same thing, but for logging! class Monad m => MonadLog m where
log :: String -> m ()
-- In IO, just log to stdout.
-- Other implementations might be a state/writer monad
-- or a library/application-specific monad for business logic.
instance MonadLog IO where
log msg = putStrLn ("log: " ++ msg)
-- Before: Bad, doesn't actually do any IO but logging
findShortestPath :: Node -> Node -> Graph -> IO [Node]
-- After: Better, type signature gives us more details on what's happening.
-- We can still use this in an IO context because IO has a MonadLog instance.
-- However, trying to capture audio in this function using either
-- of the functions above will lead to a type error.
findShortestPath' :: MonadLog m => Node -> Node -> Graph -> m [Node]
As you can imagine this can get quite verbose and there's other patterns one can use. Feel free to ask any follow-up questions :)Or do an unsafePerformIO.
Or use trace (where someone else has done the unsafePerformIO for you).
Or use a Writer.
Or introduce some logging capability (Logger m =>) onto your code.
Or take a look at all the man-hours that have been spent on trying to perfect logging: https://hackage.haskell.org/packages/tag/logging
In Haskell, you can use Debug.Trace for just that purpose, when you don't want to change the type of your function.
You got the wrong idea. You're supposed to write FP in a way where the side effect is highly layered and segregated away from pure code. IO are singularities within your chains of pure function compositions. As soon as you hit a singularity you have to break out of it as soon as possible.
The main idea is the meat. Keep your bread tiny and keep it very very separate from the meat.
The pattern is called Imperative Shell, functional core. Think of your IO as two pieces of bread in a sandwich and your pure code is the meat that connects the read to the write.
The game you're playing with haskell is to avoid letting the IO monad pollute any of your logic as much as possible.
Anyway that being said in applications where IO is all over the place... this pattern becomes largely ineffective. You basically have more bread than meat.
You would then need a monad to evaluate the things you're attempting to log. And at that point you have a monad, so you can log as usual?
Monads and side-effects aren't intrinsically related. Simplifying, a monad is something with flatMap() - in JavaScript, Array and Promise are monads (kinda). What flatMap() gives you is the ability to chain things, which is useful to sequence side-effects so that they can be performed by a machine in a given order. That's why IO and Eff are monads.
It doesn't really matter if you made a horrible uncomputable mistake deep inside a pure function, if you either discard or never use that mistake you would never notice it happened.
For example, nixpkgs (in nixlang) for sure isn't "strict pure FP" - you can do effects whenever you want - but will often have uncomputable evaluations in it. And you will only notice they're uncomputable evaluations when you try to use them. There's even a warning left for future wanderers for that:
### Evaluating the entire Nixpkgs naively will fail, make failure fast
AAAAAASomeThingsFailToEvaluate = throw ''
Please be informed that this pseudo-package is not the only part of
Nixpkgs that fails to evaluate. You should not evaluate entire Nixpkgs
without some special measures to handle failing packages, like those taken
by Hydra.
'';
I see that I formulated my thesis badly, conflating strict evaluation with monads, when as you pointed out they're not strictly related.