Hacker Newsnew | past | comments | ask | show | jobs | submitlogin

My complaint with FP: Sometimes I just want to do something silly, like adding a log somewhere. If I choose to add said side effect, now all my functions are marked with an io signature (so there might be _other_, nastier side effects hiding there as well - mainly an issue if you have multiple people contributing to the same project). If I don't add the side effect, and choose to refactor multiple layers of code, I will need to make all my functions return multiple values and later fold over all the accumulated strings and... life is too short for that. The principles really resonate with me, but maybe we are limited by the current tooling, because the development experience is quite clunky in its current stage.


This is Haskell-specific, it sounds like. I agree, the IO monad is really quite inconvenient sometimes.

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.


yeah, I've been trying to get this mindset as well - even though sometimes I feel that I'm "cheating" :)


In Haskell you have a lot of options to type your functions in a more granular way. Consider the type class MonadIO, which lets you specify that your function works on any monad that can do side effects, not just IO specifically:

    -- 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 :)


Just let all your functions live in IO then. You'll still come out ahead.

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


yeah, biting the bullet seems to be the way to go. As you mention the lots of "man-hours that have been spent on trying to perfect logging", when doing research the usage of that time might be ok, but when building a product you might need to make concessions.


> Sometimes I just want to do something silly, like adding a log somewhere.

In Haskell, you can use Debug.Trace for just that purpose, when you don't want to change the type of your function.


I've been playing with purescript and found out that they have a similar library, thanks!


You might benefit from a more pragmatic functional language. Erlang is broadly functional, but you can output from anywhere if you want to. It's probably one of the least pure functional languages out there, but it's super handy.


> If I choose to add said side effect, now all my functions are marked with an io signature

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.


Others have mentioned having the same problem with this issue. One post I particularly like about the subject is this one on function colouring (how lagnuages with async/await syntax have a similar "infection"; this is a response to the original post on function colouring and not the original post). https://www.tedinski.com/2018/11/13/function-coloring.html


From my limited knowledge of FP languages it is expected that pure code in fact doesn't evaluate anything until a monad forces it to evaluate.

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?


It's not about monads, it's about effectful code, which is represented by special types (e.g. IO in Haskell, Eff in PureScript). Effectful code can call pure code, but not vice-versa. Since a program will have to do something, the main function is always effectful, i.e. it returns an effectful special type. So you're right that pure code isn't evaluated until some effectful code is ultimately returned by the main function and executed (by a runtime or equivalent). However, in purely functional languages most code is pure, even though it's ultimately called by effectful code.

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.


To me the issue is even simpler, not even about effectful code or monads, it's about the "issues" of lazy evaluation.

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.




Guidelines | FAQ | Lists | API | Security | Legal | Apply to YC | Contact

Search: