13 ms·
Do notation considered harmful
- twblalock 6y agoI like that Haskell exists, but stuff like this article demonstrates why it isn't adopted widely. Imperative programming is the most natural thing for people. "Do this, then that, then that, in that order." Some languages take that model and run with it, and add features that are not "pure" but let people get work done. We all know which ones those are. If you work in industry they are the ones you use. Haskell rejects the imperative model, yet it attempts to appear imperative to be palatable to more people while retaining a pure functional model under the hood. But that causes it to be criticized by the pure-minded. So who is it for?
- danidiaz 6y agoI think the position that do-notation is harmful is nowadays in the minority. Personally, I prefer it to long chains of monadic operators. It proved so convenient that there are extensions like "ApplicativeDo" that let you use do-notation in contexts other than monadic: https://downloads.haskell.org/~ghc/latest/docs/html/users_guide/glasgow_exts.html#extension-ApplicativeDo https://downloads.haskell.org/~ghc/latest/docs/html/users_gu... > Imperative programming is the most natural thing for people. "Do this, then that, then that, in that order." I'm not sure. For example, do users of Excel really care in which order the cells are calculated, beyond the order imposed by the data dependencies they have defined?
- mrkeen 6y agoWhat work could you not get done in Haskell?
- MaxBarraclough 6y agoTechnical answer: Hard real-time systems. Non-technical answer: Handing over your implementation to an average programmer.
- jbotz 6y agoTry handing your rust implementation over to a Java programmer. Or even your C implementation, which might be worse because they might think they can hack it, but are likely to introduce nasty and hard to find bugs. With Haskell, once they've learned enough Haskell to be able to understand some of your code, if they modify it and it compiles it's (more) likely to be correct.
- felixyz 6y agoI think you're right that imperative semantics are natural for people in some contexts, but far from all the time. For someone who has worked hard day after day for years to fit all kinds of problems into an imperative mould, it will of course appear more "natural" than for others. Cooking recipes are good examples of imperative "scripts", and for the most part it would be silly to try to write them in a declarative style. But note that they too often contain declarative formulas, like "if the pancakes stick to the pan, use more butter". It's the most natural thing in the world to add such regulative rules. There is no mention of exactly when this rule should be applied, in the sense of where it fits into a sequence, and trying to formulate it that way would be cumbersome. It's a big drawback for the industry languages you allude to (and for the software industry as a whole) that there is no simple way to integrate such rules into programs. (I suppose aspect oriented programming is/was an attempt at that.) You can also read through the rules for any board game. They typically consist of a mix of imperative, sequential instructions, regulative rules, and constraints. Why? Because that is the most natural thing.
- spindle 6y agoI agree. System configuration is a good example of something that's not naturally imperative - hence the success of NixOS and other declarative configuration tools. ("Success" in the sense that they work really well. I realise they haven't taken over the world.)
- tetha 6y agoMonitoring feels like that as well. You usually end up with a more imperative part to execute a check or to pull data and push it to a monitoring system. "Call this API / read this file. For each line / json key, mangle into monitoring format. Add in missing metadata. Push all resulting lines to monitoring ingress host". Beyond that, our icinga has a very declarative config file. "This is host foo.local, it runs a tomcat with application X and a mysql and backups". The system then goes ahead and figures out what kind of checks might be necessary for an application X, or backups.
- deleted 6y ago[deleted]
- noelwelsh 6y agoYou're misunderstanding Haskell and do-notation. Haskell does not "[attempt] to appear imperative to be palatable to more people". Haskell has to have a way of handling effects because no useful program can be written without effects. Even if that is as simple as printing output on completion, that is still an effect. Haskell furthermore needs to integrate effects into the language in such as way it does not break the substitution model of evaluation because 1) that's the goal of Haskell as defined back in 1992-ish and 2) order of evaluation is hard to reason about in a lazy language. Monads are Haskell's solution to this, and do-notation is syntax that makes monads easier to use. There is nothing here to do with popularity. Furthermore, claims that "Imperative programming is the most natural thing for people" is not true. All programming is an artificial construction. There is no "naturalness" to be found here, only familiarity and prior experience. In my experience non-programmers take easily to functional programming. Experienced imperative programmers may struggle a bit.
- goto11 6y ago> it attempts to appear imperative to be palatable to more people I doubt that was the reasoning, given the Haskell slogan of "Avoid success at all costs". Imperative is most natural when you have mutable state and side effect. Haskell does not deny that. But even imperative languages (anything above assembler) have both statements and expressions. Haskell just tries to move as much as possible to expressions, and reduce and isolate mutable state and side effects.
- raverbashing 6y agoThe way I see it, they try to separate the "pure" from the "imperative" world but the imperative world is like trying to write a completely different language. Then they try to "make it easy" through some constructs like 'do' and >>= but it still feels like controlling an airplane using an Atari joystick. In language and API design it seems it's very easy to shoot yourself in the foot and create problems that wouldn't exist in the first place had a more sensible approach been taken (which I agree it is hard sometimes).
- otabdeveloper4 6y ago> Imperative programming is the most natural thing for people. "Do this, then that, then that, in that order." No way. Have you ever mentored technical people who aren't specifically trained programmers by education? These kinds of people will have no problem writing huge SQL queries with joins nested four levels deep, or pages and pages of declarative Pandas or R code using weird transformation and filter logic, but they'll have humongous problems understanding simple loops or variable assignment. Another data point to consider: Excel is the most popular programming environment in the world, and it's also declarative, not imperative.
- cousin_it 6y agoYes, Excel and SQL are widely used by non-programmers due to some engineering quality. But whatever that quality is, Haskell (very narrowly used even by programmers) doesn't seem to share it.
- otabdeveloper4 6y agoI'm trying to say that whatever makes Haskell unpopular, it's not its declarative nature. Whoever manages to make a purely declarative general-purpose programming language will be a technological superstar, because for normal non-programmer folks it would be the ideal gateway to programming. (That said, I don't think it's possible. 'General purpose' implies some sort of Turing completeness, i.e., recursion or loops, which immediately makes it impossible to reason about in normal declarative terms.)
- pjc50 6y agoYou would have to add some kind of statefulness, which I suspect is the thing that those people are having trouble reasoning about (and which is also an argument for making things immutable even in imperative languages!). "Statecharts"? Some other kind of boxes-and-arrows Moore/Mealy machine visualisation?
- wwright 6y agoI think we can be a little more optimistic than that. I agree that it’s probably a white whale to try to build a language which is fully declarative 100% of the time, but if we can make something that is simple and declarative 80% of the time, but falls back to something as graceful as Python for the other 20%, that may be enough to have a really big impact.
- hnick 6y agoI also like that it exists. It's interesting if nothing else. > Imperative programming is the most natural thing for people. "Do this, then that, then that, in that order." I think back to the sandwich example most of us had in CS class. Step by step, how do you make a sandwich? And then the teacher will always trip us up with a step we forgot. The problem is that with current computer programming, you can't just say "Make me a sandwich" you have to specify it in excruciating detail. Naturally, an imperative form suits this. But if we could in fact get sandwich making right, roll it out via code-reuse, and collapse that into one command, a more functional style might suit. Say what you want to achieve, instead of how to achieve it, and suddenly the order matters a bit less. Let the computer care about that. Maybe that's not possible with current architectures or programming languages, I don't know. It would be nice though.
- felixyz 6y agoLogic programming comes the closest. (Not that the goal is fully achieved yet.)
- benibela 6y agoOr you have a library and then just write new Sandwich
- andrekandre 6y ago> But if we could in fact get sandwich making right, roll it out via code-reuse, and collapse that into one command, a more functional style might suit. Say what you want to achieve, instead of how to achieve it, and suddenly the order matters a bit less. Let the computer care about that. Maybe that's not possible with current architectures or programming languages... the problem is business/marketing imo once you've made your sandwich making declaritive, safe, and predictable, business will always ask for a sandwich with some crazy requirement like "also dispenses ketchup at 2am" or "can also be used as a substitute for morning coffee"... things it was never intended to do i feel the world of programming will never find a "one true way" and will always be a mess because our assumptions are always changing and never static (at least as long as we are chasing the next dollar at least)
- nilkn 6y agoI don’t think I’d judge the language too much based on this article. It’s over a decade old and modern Haskell has progressed quite a bit since then. During the 2010s the language community really shifted towards becoming more practical. It certainly has still maintained an academic element too but that side of things doesn’t dominate nearly as much anymore.
- lmm 6y ago> Haskell rejects the imperative model, yet it attempts to appear imperative to be palatable to more people while retaining a pure functional model under the hood. But that causes it to be criticized by the pure-minded. The point of do notation is to rebuild imperative programming atop a rigorous foundation. Haskell is a deliberately flexible language: if you want to do imperative programming in it, you can (as long as you do it properly). But most users eventually find that the imperative model isn't actually something you want or need, even if you thought you did when you started. > So who is it for? People for whom the defect rate of traditional imperative programming is too high, or for whom achieving an acceptable defect rate in a traditional language takes too much time.
- wwright 6y agoIMO, imperative programming is only intuitive for imperative tasks. Many common programming tasks may be imperative, but many are not. Looking up a product number, placing that product in cart, and checking out is a naturally imperative flow. Humans would think about this somewhat imperatively and iteratively if they were doing it in the real world. However, other tasks like formatting or searching data are usually not intuitively imperative in my experience. As programmers, we build a habit for translating these ideas to imperative sequences, but I don’t think that’s obvious to people who haven’t practiced it. Compare a regular expression to a 40 line manually-written matching algorithm. If you ignore syntax, I think many people would agree that the regular expression which directly describes the goal is more intuitive to think about than the imperative machinery that achieves the same thing.
- exdsq 6y agoActually this is wrong - for example one study [0] shows only 12% of non-programmers approach problems like this while 54% use event-based approaches. Anecdotally, I've found good mathematicians who come to programming after their studies tend to dislike imperative systems too. http://alumni.cs.ucr.edu/~ratana/PaneRatanamahatanaMyers00.pdf http://alumni.cs.ucr.edu/~ratana/PaneRatanamahatanaMyers00.p...
- ashtonkem 6y agoHonestly, I think that the reason why Haskell never got adopted is that they never really tried. A ton of the decisions over the years they’ve made would’ve been insane for an industry language, but appear to have been non-problematic for their goals. What would’ve happened if they’d tried is an interesting counter factual. My biggest problem with Haskell is that is damn near impossible for me to mentally parse. I always end up liberally sprinkling my code with parenthesis and “$” hoping to make the compiler happy. I never managed to develop a mental framework to help me out here.
- ncmncm 6y ago"Get" functions, too. And type names with "state" or "metadata" in.
- mrkeen 6y agoDo-notation papers over map and bind in the same way that for-loops paper over cmp and jmp. I do not want to go back to map and bind.
- andolanra 6y agoThis is a very old opinion, and one I don't think has aged terribly well. How old? Partway down the page there's a link called "new developments" that cites GHC 6.12, a version that came out in 2009! Some thing it cites are still reasonably true, but others are complaints that reflect the Haskell of a different era. For example, it says, "Even more unfortunate, the applicative functors were introduced to Haskell's standard libraries only after monads and arrows, thus many types are instances of Monad and Arrow classes, but not as many are instances of Applicative." This has not been true for about half a decade, when Applicative was made a superclass of Monad. The article follows with: "There is no special syntax for applicative functors because it is hardly necessary." This is also no longer true, since the introduction of ApplicativeDo: even if you don't like it (and I confess I'm not a huge fan of ApplicativeDo myself!) it turns out that many people do find that sugar easier to write. However, I think the key point I'd make is: all the advantages listed here of using monad without do-notation are real in some cases, but there's nothing stopping you from not using do-notation sporadically! Occasionally a snippet that uses do-notation can be made much clearer by using >>= or some other monadic function, and occasionally a snippet that uses >>= can be made much clearer by using do-notation, and I don't think there's anything inherently wrong with Haskell keeping both. To make a Python analogy: sometimes you can express a given construct more clearly with a loop than with a list comprehension, but I don't think that list comprehensions should be considered harmful: just use those tools when you need them, and don't use them when you don't need them!
- lynguist 6y agoYou are right. The title should have (2009) appended.
- jcelerier 6y ago> https://gitlab.haskell.org/ghc/ghc/-/wikis/applicative-do https://gitlab.haskell.org/ghc/ghc/-/wikis/applicative-do can't help but chuckle at the accomplishment of 40 years of research in functional PL, ending up with almost C-like syntax once again.
- saagarjha 6y ago
- sfvisser 6y agoNot a very compelling article. None of the drawbacks stated are really serious issues, or issues at all. Everyone with a bit of experience will be aware of the possible drawbacks and knows how to avoid this. Newcomers probably have other things on their mind. Their app is not going to break, because their code is somewhat clumsy. And more importantly: do-notation is really awesome!
- unhammer 6y ago> These misunderstandings let people write clumsy code like do putStrLn "text" Is it really a problem that newcomers write things like that? It doesn't make the program any slower or more buggy. It's trivial for a code-reviewer to fix. If the new haskellers have hlint installed, they'll get a hint to simplify it and eventually they'll learn the idiomatic way. (And the readFile/writeFile example was just fine before being "fixed".)
- _ikke_ 6y agoFor me, that's similar to writing if(var == true) { } It's more verbose than necessary, but not wrong.
- iso-8859-1 6y agoIn Python and JavaScript it is also asserting the type of the thing being compared, which, I argue is better code style. It is a mess having to think about all those things being truish.
- ghostwriter 6y ago> they'll get a hint to simplify it and eventually they'll learn the idiomatic way. I can confirm that, the recent IntelliJ Haskell (https://plugins.jetbrains.com/plugin/8258-intellij-haskell https://plugins.jetbrains.com/plugin/8258-intellij-haskell) does a pretty good job notifying beginners about redundant "do" blocks and eta-reduction possibilities (with a help from HLint). The redundant blocks are even highlighted in real-time right on the editor.
- dasKrokodil 6y agoThe do-notation is one of the best features of Haskell.
- alephu5 6y agoIn terms of pedagogy I think there's a valid argument that we shouldn't teach beginners about the do notation until they're comfortable with monads. Coming from an OO background they can grok typeclasses as interfaces quite easily, and the bind and return operations as special methods. It's probably better to use pure instead of return but that's another debate. In the same way I never tell python beginners about list comprehensions, we just do pedestrian loops. In both cases, as you get comfortable and learn more you'll naturally stumble across these features and a myriad of others. Once you can happily read and write both variants, it doesn't matter too much, it's really just a matter of taste. I sometimes combine both in a single expression doing stuff like this do x <- a >>= b ... for x in (f(t) for t in ts if g(t)): h(x)
- tome 6y ago> do x <- a >>= b ... Please at least keep everything flowing in the same direction! do x <- b =<< a ...
- themagicalcake 6y agoisn't IO inherently not functional? it's a side effect
- lgas 6y agoIn Haskell your code doesn't "do IO", you write (and compose) IO actions which describe what IO will be done at runtime. Only at runtime do the side effects occur. The code itself is still pure and referentially transparent.
- kryptiskt 6y agoNo, not if you include the state of the world in the type signature. If you make every primitive IO function take a RealWorld value and return one, you can thread the execution correctly using the last one as an argument to the next. But then the question is what happens if you reuse a RealWorld that you already called a function with. That leads you to types with values that can only be used once, uniqueness types (https://en.wikipedia.org/wiki/Uniqueness_type https://en.wikipedia.org/wiki/Uniqueness_type). Which is the solution for IO that Clean, the other lazy functional language, uses.
- saithound 6y agoTo add to lgas' point: The distinction between functions with side effects and descriptions of what IO will be done at run-time has many practical consequences. For example, if you execute the Haskell code greet name = do putStrLn ("Hello, " ++ name) repeat3 action = do putStrLn "Doing it thrice." action action action putStrLn "Done it thrice." main = repeat3 (greet "themagicalcake") you get the following output: Doing it thrice. Hello, themagicalcake Hello, themagicalcake Hello, themagicalcake Done it thrice. But if you execute the similar-looking Python script def greet(name): print ('Hello %s' % name) def repeat3(action): print ('Doing it thrice.') action() action() action() print ('Done it thrice.') if __name__ == "__main__": repeat3(greet('themagicalcake')) you get Hello themagicalcake Doing it thrice. Traceback (most recent call last): File "test.py", line 10, in <module> repeat3(greet('themagicalcake')) File "test.py", line 5, in repeat3 action() TypeError: 'NoneType' object is not callable instead. The Haskell value `greet "themagicalcake"` has type `IO ()`: it is a value that describes IO to be done at runtime. In contrast, the Python value returned by the call `greet('themagicalcake')` has type `NoneType`, and this value does not describe anything. Calling `greet` itself had the side-effect of printing "Hello themagicalcake".
- tom_mellior 6y ago> There is no special syntax for applicative functors because it is hardly necessary. You just write > readHeader = liftA3 Header get get get Seeing Haskell code peppered with liftM_ and similar things is one of the main problems I have with the language. Monads (and applicatives etc.) solve a problem, one might even say that they solve it well, but I've never found that they solve it ergonomically.
- kqr 6y agoYes, I think you have a key point there. Using the same-ish syntax for effects as for pure code is a tricky problem of ergonomics: get it too similar/implicit, and people will be confused about when effects happen and when pure code runs. Get it too dissimilar/explicit, and it gets cumbersome to write. I actually think do notation hits a great middle point, where it's obvious you're in effectful code, yet familiar and similar enough to pure expressions to be convenient.
- deleted 6y ago[deleted]
- bad_user 6y agoArticle says that this: readFile "foo" >>= writeFile "bar" is preferable to this: do text <- readFile "foo" writeFile "bar" text I don't agree. Even if a working knowledge of the monadic bind is required, the second expression is more readable to beginners and can be extended more easily to do other things. Of course, the first expression is written in "pointfree" style, where the code is described as a composition of functions. Even if I like pointfree one liners that are clear (so depending on context), I hate pointfree style applied to entire codebases and I think it's one barrier for Haskell's adoption. --- Article also says: "Newcomers might think that the order of statements determines the order of execution." But that's good, because the order of statements in a "do" expression does determine the order of execution. That's the whole point with monads and the "do" expression. That you can get away with applicatives instead, that's another point entirely. Note that GHC nowadays has a language extension called "ApplicativeDo". So to take an example from the article: do x <- Just (3+5) y <- Just (5*7) return (x-y) In this case you just enable "ApplicativeDo" and it will magically use applicatives without changing the code. The order of execution on the other hand will NOT matter, so you can still pretend that there's an order to it, because such code will be pure and it will not be repurposed to run in parallel or anything crazy like that, the only thing the compiler can do in such cases is to eliminate unused expressions. Funny thing about Applicative being a super class of Monad, but "ap" / "map2" operations need to remain consistent with "bind" by law otherwise the implementation isn't valid. So it doesn't matter if the code above uses applicatives or monads, that's just an optimization, as for all practical purposes the result should be the same, order of execution included. Of course, code using Applicative instead of Monad is more generic, since you have less restrictions, you can work with more types and the function signatures become more clear in what they do (the more abstract they are, the less they can do), so that's the primary reason for why Applicative is recommended whenever you can get away with it. No reason to bring the Monad chainsaw to the party when not needed.
- tome 6y ago> the order of statements in a "do" expression does determine the order of execution Not really, unless you take a very specific definition for "order of execution". There are plenty of monads where order of statements is unrelated to the order of execution, Identity, for example!
- infinity0 6y agoThe article is obsolete and should be deleted, there is ApplicativeDo these days.
- rtpg 6y agoI feel like there’s so much self owning by Haskell and Lisps in making multiline statements such a chore. Why does introducing let add indentation? I know why but it’s not useful! I’m just trying to provide a name for an expression, and my pseudo code wouldn’t have an indentation Do notation is an out, but it’s its own special thing. So you get people writing nomadic interfaces because it’s the most syntactically convenient.
- tome 6y agoThen use {} around your binders. This sets x to 11, for example. x = let { y = 5 ; z = 6 } in y + z
- kazinator 6y ago> Why does introducing let add indentation? Scheme has lexically scoped (define var value). It de-sugars into let under the hood. Not everyone in Lisp likes such a thing, though. I prefer my lexical scopes to be enclosed. In C coding, I stick to "variables at the top of a block". If I need a scope somewhere, I put it in braces: x = f(); { int z = whatever(); foo(z); } // here we know that the scope of z has ended. In C99 and later, you can remove these braces, but then the scope doesn't end until the enclosing block (possibly the whole function). That's a conceptual problem for me, because I really want that z to exist only in that two-line bubble. It's more than just a conceptual problem. I might have another such a bubble which also uses the name z, and I want to be able to move them around easily. If I remove the braces, I can have only one z the first occurrence controls the initialization and everywhere else it's of the same type. I could have some goto around the bubble also: goto after; ... { int z = whatever(); foo(z); } after: ... If the braces are removed, there is a problem: the goto jumps into a scope where z is visible, but has skipped the initializer z = whatever(). (This is a huge problem in C++ due to construction being potentially skipped so seem to recall it's a required diagnostic in that language, but not in C.) Basically "just piss new identifiers into the current scope" is a terrible language design in my view. In Lisp, there is a further problem of things being harder to analyze. let is easily destructured by position. The operator is in the car, the bindings in the cadr, the body in the cddr. (Common Lisp somewhat complicates things with declarations in the body.) With define we have to do the desugaring to let before we can continue to walk the code. Essentially, it behaves like a macro, but not a regular macro: a macro that has influence outside of its form. That's just another reason to eschew it. A good way to avoid indentation is to use sequential let* and then do all your work in successive variable binding statements. Then have a single expression in the body to calculate a value to return. In let* , you're permitted to re-use identifiers, if that helps you: (let* ((x (this)) (x (* x 3)) ...) If you need to evaluate something for a side effect, you can stick it into a progn, or bind it to a dummy variable (which may have to be declared ignore in Common Lisp): (let* ((x (progn (effect) (this))) (dummy (effect)) (y (bar x)) ...) (calculate y)) Now you can insert new variables and computations without any change in indentation. In C, I use the comma operator as progn to keep code as a sequence of initializations, while inserting a needed effect, like in this function: http://www.kylheku.com/cgit/txr/tree/eval.c?h=txr-237#n1465 http://www.kylheku.com/cgit/txr/tree/eval.c?h=txr-237#n1465 static val expand_eval(val form, val env, val menv) { val lfe_save = last_form_evaled; val form_ex = (last_form_evaled = nil, expand(form, menv)); val loading = cdr(lookup_var(dyn_env, load_recursive_s)); val ret = ((void) (loading || uw_release_deferred_warnings()), eval(form_ex, default_null_arg(env), form)); last_form_evaled = lfe_save; return ret; } The nulling out of last_form_evaled is inserted into the unbroken sequence of initializations via the comma operator.
- deleted 6y ago[deleted]