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

> The most successful languages let you be pure-functional where it makes sense and then stateful where it makes sense. Haskell doesn't, really (from my cursory reading about it).

This is absolutely wrong. Sorry to be so direct, but I want to make sure other people not familiar with Haskell don’t get the wrong impression.

Haskell is explicitly designed to separate purely functional and stateful code with a clear interface between them: monads. It does exactly what you are asking for.



Whenever I've used Haskell, the monads infect things like a virus, similar to the way async/await metastasizes across a C# codebase.

Note: I am not very good at Haskell.


That's what happens when you're working in a pervasive-mutability-by-default (or pervasive-IO-by-default) language as well - your whole codebase is full of hidden state mutations and hidden interactions with the outside world. You just don't have any idea where they're happening. You can write Haskell in the same style with monads everywhere and you're in essentially the same situation (just with more visibility into it) - but then you can actually start isolating the parts where mutation or outside interaction happen, and separating them from your core business logic. Which is the same thing you'd do in a high-quality codebase in any other language, but in Haskell you can do it in a way that's actually enforced and visible rather than just convention.


Probably because async/await IS a monad. Avoiding that infection just takes some design experience.


Sorry for the additional pedantry, but I think this important to be precise about given the target audience of your comment.

Monads aren't the separation between purely functional and stateful code. The Haskell type system maintains that separation. Anything that's doesn't return IO a for some a appears to be a pure function from the perspective of the programmer. Once a function returns IO a, there aren't any* functions provided by the compiler that can make a function that uses those results not also return IO b for some b. For example, the type of getLine is IO String (because it impurely produces a String) and the type of putStr is String -> IO () (because it takes a String and mutates the world without returning anything).

If the compiler provided a function for computing on the a in the IO a, for instance, bindIO :: IO a -> (a -> IO b) -> IO b and a function to wrap the results of non-IO functions, such as returnIO :: a -> IO a, you could do arbitrary computation with these IO-wrapped data types, but know at a glance if your functions were impure.

This approach doesn't require the Monad typeclass at all, just a magic type called IO that tags impure computations that are implemented with compiler and runtime magic. It happens to be the case that this is exactly how GHC implements the IO type. bindIO is implemented here[0] and returnIO is implemented here[1] and the compiler magic used to implement them isn't* exported, so all IO operations have to go through those functions. It is not a coincidence to that these functions have the right types to form a Monad instance for IO and indeed, that is also present[2], but the IO type and the type system that ensures it can't be sneakily hidden are doing the heavy lifting, and the Monad instance (and accompanying syntactic sugar), are just there to make it nicer to work with and easier to abstract over.

If you have a passing familiarity with Haskell, the phrase "state monad" is the obvious place where my claims stop making sense. In fact, the State type only supports computations that are entirely pure. If you want to simulate global variables in a language that didn't have them, you could always pass all of your global variables to every function and get updated ones back from the function along with the nominal results of the computation. The State type is just a regular data type that wraps stateful functions constructed by such state passing. A type of the form State Int String is just a function that takes an Int and returns and String and an Int, no compiler or runtime magic needed.

You can play the same trick as in the IO case and provide functions bindState :: State s a -> (a -> State s b) -> State s b and returnState :: a -> State s a in order to compute on these "stateful" values while making sure the result state got passed to the next function in the chain correctly. Like IO these two functions can be used to create a Monad instance for State. Unlike IO, State is just a data type holding a regular Haskell function, so it's extremely reasonable to write a function of type State s a -> s -> a which runs the State s a computation with an initial value of type s. This is written by unwrapping the State type and then passing the initial state value to the function inside and return the result while ignoring the returned new state. More details on how State is implemented are available here[3].

A complication to this is that if you want stateful mutation for performance reasons, the ST type[4] also exists, which looks identical to the State type from the programmer's perspective, but plays similar tricks to IO in order to actually mutate under the hood while not exposing the implementation details to the user, so it can be reasoned about exactly as if it was pure and using the same implementation as State.

These Monad instances for IO, State, and ST start to pull their weight when you write functions that only use features provided by the Monad typeclass and they work seamlessly with any implementation of stateful computation despite their very different internals. Monad is quite general, so if all you care about is abstracting over stateful computations, you can also use the methods from MonadState[5] which allow you to interact with the state along with the results of the computation independent of the implementation of stateful computation.

* In the name of not getting bogged down in details, there are a few parts of this discussion that are not entirely accurate, particularly around functions like unsafePerformIO[6].

[0] http://hackage.haskell.org/package/base-4.12.0.0/docs/src/GH...

[1] http://hackage.haskell.org/package/base-4.12.0.0/docs/src/GH...

[2] http://hackage.haskell.org/package/base-4.12.0.0/docs/src/GH...

[3] https://acm.wustl.edu/functional/state-monad.php

[4] http://hackage.haskell.org/package/base-4.12.0.0/docs/Contro...

[5] http://hackage.haskell.org/package/mtl-2.2.2/docs/Control-Mo...

[6] http://hackage.haskell.org/package/base-4.12.0.0/docs/System...


Note: The approach of structuring the interactions with the IO type with the functions (bindIO :: IO a -> (a -> IO b) -> IO b) and (returnIO :: a -> IO a) is still using the abstract idea of monads to organize the impure code and make it ergonomic to work with, so "monadic I/O" or "monadic state" aren't entirely misnomers. The thing I wanted to emphasize is that you don't need to know the word "monad" or understand anything in particular about the design process for the Monad typeclass in order to use these libraries.

I think focusing on the "monad" part over the "IO" part of "monadic IO" is particularly confusing to new users because the abstract idea of a monad is very general, so if you assume all places where it shows up are basically like the case of IO, you will be very confused. Further, it makes the idea of a monad seem like a Haskell-specific hack, rather than a general abstraction that can be used in any programming language you want to.

This is particularly important to emphasize because the abstract idea of monads only makes the IO approach to impurity nice to use, it doesn't make it possible. Haskell had I/O (and other impure capabilities) before the monadic way of organizing impure code was introduced. The heavy lifting for IO is done by having a type system strong enough to prevent a function of type IO a -> a from being written by an end-user. If you have written a monad abstraction in a language without such a type system[0], it can still be a nice abstraction, but it doesn't guarantee that pure and impure computations can be distinguished on the type level.

[0] https://www.nurkiewicz.com/2016/06/functor-and-monad-example...




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

Search: