5 ms·
You might not need a deep understanding of category theory to write useful programs in Haskell, but the downsides of relying on those explicit monads to control
by Chris_Newton 13y ago
You might not need a deep understanding of category theory to write useful programs in Haskell, but the downsides of relying on those explicit monads to control state and interactions still show through in practical code.
Here’s an early example from the article:
import Control.Monad.State
type Interact a = StateT Int IO a
repl :: Interact ()
repl = forever $ do
val <- liftIO readLn
state <- get
liftIO $ putStr "Total: "
liftIO $ print (val + state)
put val
main = runStateT repl 0
Here’s something with similar behaviour written in Python, an imperative language with a reputation for being easy to read:
total = 0
while True:
total += input()
print "Total:", total
The difference is striking. The latter is obviously much more concise. Ironically, it also seems clearer about how the state is used, having the initialization, reading and writing of that state all kept together. It’s worth considering that the stateful logic is relatively kind here, too, because there’s only a single value that needs to be maintained via StateT in the Haskell version.
The Python code has the added advantage of actually being correct, assuming that the intended behaviour really is to add up the total of all entered numbers over time as the output suggests.
I think this example is instructive in terms of advancing industrial practice rather than the theoretical state of the art. Haskell demonstrates a lot of nice features that mainstream industrial languages lack today, but as soon as any sort of state or effects enter the picture, it becomes absurdly complicated to describe even simple systems. I would love to have future languages incorporate some of the safety that Haskell offers, but I think to catch on with working programmers they will have to hide away a lot of the state/effect details where they aren’t needed, just as tools like type inference have hidden away their own form of boilerplate. It’s not just about not needing to be an expert on category theory, it’s about not having to be aware of it at all.
- sseveran 13y agoIn real life (not contrived examples) haskell greatly simplifies dealing with state. Isolating IO makes it much simpler to reason about where exceptions can come from.
- Chris_Newton 13y agoIn real life (not contrived examples) haskell greatly simplifies dealing with state. That is the assumption I’m questioning. For example, let’s consider quicksort. Hopefully we can agree that this is a substantial and practically useful algorithm, and one that fundamentally relies on mutable state for its performance and scalability. I recently posted a link here to a discussion on haskell.org[1] about implementing a real quicksort. This was contrasted with the usual elegant but not-really-quicksort version that has propped up a million functional programming advocacy posts, and with the original C version[2]. Neither Haskell version seems particularly simple to me when compared to the C, and I see little extra safety in return for all the Haskell syntactic overhead that couldn’t have been had just as well with a couple of "const" annotations in numerous imperative languages. Obviously not every case is as self-contained as this algorithm and in other contexts Haskell’s approach would fare better. But a lot of real world code is stateful, and a lot of it uses this kind of local state, and so I contend that making everything explicit, Haskell-style, does not greatly simplify dealing with state in general. [1] http://www.haskell.org/haskellwiki/Introduction/Direct_Translation http://www.haskell.org/haskellwiki/Introduction/Direct_Trans... [2] http://www.haskell.org/haskellwiki/Introduction#Quicksort_in_C http://www.haskell.org/haskellwiki/Introduction#Quicksort_in...
- dllthomas 13y agoI don't think it was an assumption; I think Steve was speaking from experience. As someone working extensively in C and doing a fair bit of Haskell, I'm finding my C to be more verbose and less safe. The C runs faster, in my application; sometimes I write it faster and other times slower, depending on a bunch of factors.
- Chris_Newton 13y agoThat’s fine. I’m just saying that my experience differs, and trying to demonstrate why.
- dllthomas 13y ago
- dpratt 13y agoI've been coding in Scala for the past couple years, and it is basically exactly what you describe in your last paragraph.
- Chris_Newton 13y agoScala is one of those frustrating languages for me. A lot of the basic syntax and concepts are very much as I’d wish to do them myself. For example, I wrote a toy compiler years ago that not only used the var/val distinction but even used exactly the same terminology. I’ve seen a few videos of Martin Odersky, and he’s one of those presenters where I find myself nodding along with almost everything. Sadly, I think Scala suffers from a similar heritage problem to C++. The good ideas underlying it always seem slightly held back by the historical baggage and compatibility issues, and somehow it never feels quite as clean and tidy as it should. I understand the benefits of building on such well-established foundations, but as with effects in Haskell, sometimes silly things just seem to take far more effort than they should, even though much of the language appeals to me.
- hiker 13y agoThe syntax in Haskell for dealing with side effects is tedious/verbose by design. The intention is to push one in writing pure programs since pure functions and compositions are easier to test, verify and reason about. Good Haskell programs are pure; side effects are usually isolated in a "shell" wrapping the purely functional core of your program that interacts with the world (IO, networking, graphics, etc).
- Chris_Newton 13y agoThe syntax in Haskell for dealing with side effects is tedious/verbose by design. In the entire history of computer programming, I’m not sure language designers taking that kind of attitude has ever worked. Good Haskell programs are pure; side effects are usually isolated in a "shell" The real world is stateful, and many useful programming techniques rely on mutable state. Indeed, programs with no externally observable effect would be rather uninteresting, since by definition they can’t do anything useful! Please don’t misunderstand my position here. I’m strongly in favour of programming languages providing tools to control that state and avoid unexpected effects (or combinations of effects, or ordering of effects). This would prevent many common classes of programmer error, surely improving the quality of production software significantly. However, I don’t accept that trying to completely separate the two worlds is necessarily the best way to achieve that benefit. I think adopting the hard line you describe is one of the reasons that functional programming languages haven’t taken off in the industry as a whole, yet several useful tools found in those languages have been widely adopted in recent years.
- TuringTest 13y ago"functional programming languages haven’t taken off in the industry as a whole" You mean, except for Erlang?
- davidw 13y agoIf Erlang is an example of "taken off"... I am currently using Erlang on a project because it was a good choice, but I would not exactly consider it popular. Popular is stuff like Javascript, Python, Ruby - even Go. I think it's indicative that in an example of how simple Haskell is supposed to be, you find this several paragraphs in: > This is an example of a monad transformer, namely the composition of StateT ( State Transformer ) with the IO Monad. The values from the IO monad are lifted into the State monad with the liftM function. That makes me curious - I'd like to figure out what all that means, exactly, what the concepts are, what they're useful for, and so on. But I don't know if I'm a "typical web developer" either.
- runT1ME 13y ago>The Python code has the added advantage of actually being correct, assuming that the intended behaviour really is to add up the total of all entered numbers over time as the output suggests. Only in a single threaded environment. However, concurrently, the Haskell code stays correct in a concurrent environment while your python code has bugs. Extrapolate a bit, and think about how many python functions may be slightly less trivial than yours and yet still have the same concurrency issues...
- dllthomas 13y agoYou mean the Haskell code stays equally incorrect in a concurrent environment...
- asdasf 13y agoWhat is incorrect about it?
- tel 13y agoThe thread op assumed that the code was supposed to accumulate the amounts ever entered. The Haskell code doesn't accumulate, it adds you input to the prior state, prints that sum, then stores your input, not the sum as the next state. Of course, that's just a (likely true) assumption of what the behavior was supposed to be and it extrapolates far from the point. The example didn't really intend to show off a particular behavior but instead the interweaving of effects.
- Chris_Newton 13y agoThe example didn't really intend to show off a particular behavior but instead the interweaving of effects. Indeed. My point was not to nitpick an interesting article. I was trying to illustrate how making so many details explicit in Haskell has a real, practical downside: what seems to be an obvious logic error had slipped through, where it surely would have been spotted easily in the simpler Python code. Moreover, no-one had even mentioned this possible bug after a couple of hours and dozens of posts here, though of course that could just be because no-one else felt this point was important enough to make rather than because they hadn’t noticed.
- asdasf 13y ago>The Python code has the added advantage of actually being correct, assuming that the intended behaviour really is to add up the total of all entered numbers over time as the output suggests. What output? I don't understand why you wrote python code that does something different than the haskell example does, and then decided that makes the haskell code wrong.
- Chris_Newton 13y agoI think if you’ve got an example of an open-ended read loop, and it collects numbers, and you’re printing something that says “Total:”, it’s reasonable to ask whether the intent was to print the total of the numbers collected by the loop so far. That this side discussion has happened almost makes my point for me. If the Haskell code had described what the mutable state actually was (the total to date or the previous entered value) rather than getting hung up on the fact that it was mutable state at all (even as written, the term “state” appears five times in this trivial algorithm) then we wouldn’t be having this conversation.
- asdasf 13y ago>it’s reasonable to ask whether the intent was to print the total of the numbers collected by the loop so far. Yes, it is reasonable to ask that. But you just said it was incorrect, you didn't ask that. >That this side discussion has happened almost makes my point for me I don't see how. Your point appears to be "I can write shorter python". You can write shorter haskell to do that too, the point was to demonstrate the State monad, not to be concise.
- dllthomas 13y agoIf the code isn't incorrect, the label printed is. It is not computing the total.
- olenhad 13y agoI've been working with clojure, and atleast for me, it offers the freedom to work with both pure and impure code with relative ease, whilst still being conservative on state manipulation.
- atondwal 13y agoHis example is intentionally contrived to show a minimal example of StateT. This is who you'd really do it in Haskell: for strings: main = repl "" repl :: String -> IO () repl a = getLine >>= \c -> print (a++c) >> repl (a++c) and it's not that much harder for Ints main = repl 0 repl :: Int -> IO () repl a = fmap read getLine >>= \b -> print (a+b) >> repl (a+b) or using do notation repl a = do $ b <- getLine print sum repl sum where sum = a + read b which is still concise and easy to read.
- dllthomas 13y agoOne could also do something like getContents >>= mapM_ print . drop 1 . scanl (+) 0 . map read . lines
- stonemetal 13y agoFrankly the two aren't equivalent in behavior. So of course the python version is simpler, it does less. The Haskell code equivalent to your python version is almost as trivial. repl :: Int -> IO() repl state = do val <- readLn print ("Total: " ++ (show (val + state))) repl (val + state) main = repl 0
- Chris_Newton 13y agoSo of course the python version is simpler, it does less. What does the Haskell version do that the Python doesn’t? In terms of externally observable behaviour they both prompt for inputs and then add them up. In terms of design they’re both infinite loops that maintain one item of state. I understand that the example was written to show the State monad, and obviously the same externally observable behaviour could be achieved in other ways in this simple case. My point is that in imperative languages you get that State behaviour for free, and unlike the simplified alternatives to the Haskell that rely on recursion instead, such as the one you posted, the imperative state-for-free model scales up when you start to get lots of state and mutation going on. It doesn’t start to suffer in performance metrics like speed or memory usage, nor does it start to have unpredictable performance characteristics in complex systems. As I’ve stated elsewhere in this thread, this argument is not meant to belittle Haskell or dismiss the clear advantages its model offers. I’m arguing that there is a price to making those things as explicit as Haskell does, and that price is probably too high for mainstream industrial acceptance, so if we’re going to reap the benefits of the sound foundations on an industry-wide scale, they need to be available more transparently.
- nightski 13y agoThe imperative one scales alright, into an un-maintainable nightmare of bugs. The problem is that the imperative model places the entire burden in the programmers head (even when it may not even be code you wrote!) whereas the Haskell version makes everything explicit.
- stonemetal 13y agoAs you say the Haskell version explicitly controls state, your python version doesn't. If the python version had access to the variables in question under locks with as much security as the Haskell version then they would be doing the same amount of work. Sure they don't do anything observably different but it is a daft example with no driving motivation for why someone would write code that way.
- mietek 13y agoThe `StateT` example in the article is unfortunate, as it reinforces the misguided notion that handling state in Haskell requires complicated machinery, such as monad transformers. This is simply false. You can use a mutable reference, like this: import Control.Monad import Data.IORef main = do ref <- newIORef 0 forever $ do val <- readLn modifyIORef ref (+ val) total <- readIORef ref putStrLn ("Total: " ++ show total) Haskell's libraries haven't been designed to make this use case as concise as possible, but nothing prevents you from introducing more succinct notation. To illustrate this point: In Haskell, the `forever` control flow operator is not a language keyword, but a function you could write yourself. In Python, could you write `print` yourself?