I 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?
> 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?
Try 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.
I 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.
I 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.)
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.
And this only increases with the experience of the chefs involved. When an experienced chef discusses a dish with another experienced chef, they will talk in common high-level abstractions like roux and emulsifier without going into detail in how to first make those components.
And then at the next level up is complete base recipes, like "this is essentially a bearnaise except with _____" or "you start with a ______ but instead of _____ you _____". So the recipes are expressed with combinators over modifiers and existing recipes.
This reminds me of a website called Cooking For Engineers[1] which uses diagrams to represent the parallel and sequential parts of a recipe (e.g. [2]).
You'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.
> 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.
The 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).
> 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.
Yes, 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.
I'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.)
You 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?
I 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.
Non sequitur. Excel shows that declarative systems can be popular and easily accessible by non-programmers. Not that any declarative system will automatically be popular!
It is an interesting observation that declarative logic apparently is more intuitive to non-programmers than it is to programmers trained in imperative programming.
To me a more likely explanation is that non-Turing-complete languages are easier to understand overall, but among Turing-complete languages, imperative is easier to understand. The popularity data seems to bear it out.
> 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.
I think that comes from their mathematics background.
I'd like to wheel out the "left handed scissors" argument again; the large and persistent arguments over what is most intuitive should be taken as evidence that different people find different things intuitive or comfortable.
Which is more natural to use, a symmetrical mouse or one with a slight bend to it in a comma shape to more closely fit being held in the right hand? What happens when a left handed person picks it up?
I 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.
> 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)
Yes I've basically reached the same conclusion, unless we have a major shift in how programming itself works (AI assisted or more declarative like I said or whatever). It goes further than business/marketing though, even programmers do it to themselves.
Many languages have a package repository like CPAN for Perl, but you'll still find multiple modules trying to do the same thing but slightly differently, and even when there's a clear winner there's often a lot of complexity in using it.
Some things like sending an email are relatively well handled while others like XML parsing have a lot of warts. Then you get that urge to rewrite because there's no possible reason it has to be this complicated, and you become what you hate and add to the mess :)
> It goes further than business/marketing though, even programmers do it to themselves
that is true as well... something of a blind-spot since it happens right under our nose (or maybe said better, right behind out eyes where we cant see it)
I 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.
> 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.
IMO, 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.
Actually 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.
Honestly, 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.
This 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!
I too can't help but chuckle at some of the substance less commentary at HN, where chuckling is a substitute for inability to contribute anything to a discussion.
isn't basically imperative syntax ?
hint: if you can get from it to C (or pascal or python or whatever) simply by adding a couple types, semicolons and replacing operators, it's C.
There is no such thing as imperative syntax. Syntax is completely orthogonal to being imperative, which is semantic not syntactic.
It isn't "almost C-like syntax", unless you define that to mean "if I do any amount of search and replace I can make it into valid C code that has completely different semantics". "x <- a" in haskell is not at all the same as "x = a" in C.
And that isn't the reason I said your comment is wrong. Your comment is wrong because do notation is not the result of "40 years of research in functional PL", nor is it an accomplishment. It is simple syntactic sugar that didn't come from research at all.
Your comment could be accurately summarized as "I would like to express how smug I feel for not learning something".
would you consider showing me a previous appearance of ApplicativeDo (not do notation of course, that shit is almost older than me) in scientific literature before that paper ?
As surely, if a concept had existed for a long time it would be more relevant to start by the original paper that introduced it ?
Of course, the only thing 40 years of PL research resulted in was the do notation.
I was under the impression that the do notation was syntax sugar for any kind of Monad - List, nondeterminism, asynchronous iO, parser combinations, exceptions etc. Stuff that appears as language features in other languages, but are just a library in Haskell.
But your chuckle has shown me that this code is just plain old C, with links to documents you haven't even read. Thank you for enlightening me, great chuckler.
I am honestly amazed at such an incredible bad reading of my comments. Next time I'll just write a phd chapter instead as apparently this is what is needed sometimes.
You are not capable of writing a PhD chapter. You lack the basic scientific thinking or method required. I suggest you focus on your core competency - chuckling.
Here is some basic reading material for you. Lets see if you can follow this
This implements async await in Haskell via a library using Monads. Hint: you cannot do this in C via any library. You need to modify the language. If you are uneducated about Monads, you might think this is similar to some kind of C code. It is not!
Here is the same do syntax being used to parse source files instead of doing async/await IO. Yes this parser will also perform backtracking etc, which will not be obvious to anyone who thinks this is some kind of transliteraton of C
I am still humbled by your persistence in looking for deep truths in forum comments.
> This implements async await in Haskell via a library using Monads. Hint: you cannot do this in C via any library. You need to modify the language. If you are uneducated about Monads, you might think this is similar to some kind of C code. It is not!
it's still pretty much
> almost C-like syntax once again.
at no moment, not a single time, did I say in any way that I was talking about semantics but this is apparently a hill you wish to die on... so, by all means have fun !
Maybe you'll be more convinced by the exact text of the paper that introduced do-notation though ?
> Using monads gives functional programming an imperative flavour. Gofer supports a, so called, do notation which makes this imperative flavour more apparent.
or, let me rephrase that whole thing for you in a language that you seem to speak quite fluently:
class BasicallyC m where
(>>=) :: m a -> ( a -> m b) -> m b
(>>) :: m a -> m b -> m b
return :: a -> m a
fail :: String -> m a
class BasicallyC m => ImperativeFlavour m
You very conveniently skipped that you cant build async /await in C without modifying the language.
You also failed to show how parser combinator grammars such as yacc/bison/BNF forms are basically just imperative code.
Monads model effect systems; not imperative code.
I am impressed by your ability to argue about stuff you know nothing about, and linking to papers and documents you can't even read. Have you considered running for US president?
> You very conveniently skipped that you cant build async /await in C without modifying the language.
but async / await is semantics, not syntax, thus completely irrelevant to the conversation. you want an example of building async / await into the syntax of C as a library ? tada !
or maybe you would prefer the more "modern" version, still compatible with pure C89 syntax though ?
#define async
#define await (void)
int main(int argc, char** argv) {
auto x = async printf("foobar");
/* wow, you can even compose it ! */
auto y = async 2 * x;
await x;
}
Don't think you have noticed. But there is a chuckling epidemic on HN. It acts as a tool to appear smug and superior while not providing any sensible argument or logic.
The last time there was someone chuckling, I was way more civil.
It took decades years of research in mathematics to come up with a way to rigorously do the kind of calculus that Riemann and Cauchy could do intuitively. But when students tried to do analysis the way Riemann and Cauchy did, they just proved (or rather "proved") all kinds of absurdities. Going back, it's the same story with calculus itself: Newton and Leibnitz could do it intuitively, but it took hundreds of years to come up with a rigorous foundation that allowed mere mortals to do the same kind of thing safely.
I think there's other way of looking into this: in Haskell we have well defined semantics (when you look at function you have explicit information about what is happening) and we managed to get to familiar syntax. In C we have very messy semantics: "everything can happen everywhere". Java stays somewhere in between (though closer to C side): when you have a method in some class it most likely touches only visible methods/fields of its arguments and fields of this class (obviously there are some escape hatches like unsafePerformIO, but most of your codebase will not use this).
I believe being able to understand code on high level quickly is a good thing in engineering. Wouldn't it be nice if looking at some part of code you could say that it actually is calling microservices X, Y and Z and no other microsevices? In Haskell you could have that. I don't see any fundamental reason why such functionality couldn't be brought into an imperative language at some point (but maybe I'm ignorant about PL theory). (FWIW I don't believe in Haskell going mainstream at any point).
> Wouldn't it be nice if looking at some part of code you could say that it actually is calling microservices X, Y and Z and no other microsevices?
What always makes me a bit uneasy about that is the absence of proper escape hatch in functional langs. I am not old and yet I have already been in time-critical situations - where all you have is sometimes only a few minutes - to do a quick hack that will save a few more hours to actually investigate and do the proper fix (think physical installations).
And, I would really not call unsafePerfomIO a proper escape hatch given the big attached warning that comes with it:
> If the I/O computation wrapped in unsafePerformIO performs side effects, then the relative order in which those side effects take place (relative to the main I/O trunk, or other calls to unsafePerformIO) is indeterminate.
> And, I would really not call unsafePerfomIO a proper escape hatch given the big attached warning that comes with it:
> If the I/O computation wrapped in unsafePerformIO performs side effects, then the relative order in which those side effects take place (relative to the main I/O trunk, or other calls to unsafePerformIO) is indeterminate.
Isn't that exactly the situation you'd be in in a non-functional language by default? E.g. in C, evaluation order for function arguments is indeterminate, so the order of any side effects that each argument does is also indeterminate. The only way to force a particular evaluation order is to arrange your code to have suitable sequence points in place - which is not really any easier than composing IOs properly in Haskell.
> Isn't that exactly the situation you'd be in in a non-functional language by default? E.g. in C, evaluation order for function arguments is indeterminate
I don't know any non-meme program where entire call stacks would be in a function call. The worst cases I've seen of this were things such as
f(i, i++)
which is a far cry from the issues you'd have in anything doing lazy evaluation of a program graph
I must say that it looks super unusual, at least for the codebases I know, for non-trivial g, h.
e.g. I just ran
rg "[a-zA-Z0-9_]+\(.*\(.*\).*\(.*\).*\)" | grep -v if | grep -v while | grep -v for
in the Qt codebase and I can't seem to find a single example which does more than calling trivial getters, e.g. sort(v.begin(), v.end()) or foobar(point.x(), point.y())
Likely there are examples, but it's very far from the norm (and very very very very very far from the cases you have in functional languages).
The do notation allows the reader to misunderstand code quite deeply, contorting sound, standard and relatively simple functional programming techniques and idioms into an obfuscated and deceptive syntax that looks like something it is not.
Do notation is jokingly called a programmable semicolon. True, it is often used to model imperative computations with it, so that is the association people have with it. Also people come mostly from imperative backgrounds and look, we can bind something to a value. Finally something I get! And that is fine, let them play around with it like that.
But if they look closer, they find out it is not really imperative. Why is there a difference between let and bind? Why can't I reassign values. And they start to use more exotic and also more interesting monads and boom the imperative feel goes away.
For example with the interesting LogicT[1] monad. Or what about the probability monad[2]? Or the continuation monad[3]? Or the reverse state monad[4]? How imperative are those really? It really depends how you use it. And I think it adds more to the magic, that you think it is just imperative code at first, because suddenly you can create code that behave very different under various monads that represent computation models. I think that is a cool thing about this confusion.
I understand that if you are a professional that needs to get work done, it can be annoying.
> I don't see any fundamental reason why such functionality couldn't be brought into an imperative language at some point (but maybe I'm ignorant about PL theory).
Nim can kinda do this with its effect tracking feature (although I haven't used Nim enough yet to know if anyone is really using it), e.g.
type
AEffect = object of RootEffect
BEffect = object of RootEffect
proc callA() {.tags: [AEffect].} = echo "A"
proc callB() {.tags: [BEffect].} = echo "B"
proc somethingInBetween() = # Tags are inferred here.
callA()
proc doSomethingWithA() {.tags: [AEffect].} =
somethingInBetween()
# trying to `callB()` here would fail.
proc doSomethingWithB() {.tags: [BEffect].} =
callB()
# trying to `callA()` here would fail.
The `tags` pragma adds specific tags to the proc and the compiler ensures that it doesn't call anything with any other tags. Similar mechanism is used for exceptions too.
That's a good thing. C's syntax is fine; C's guarantees and semantics is the problem. Here we have a similar syntax, but infinitely better guarantees and well-defined sematics.
I mean it's fun because you get a pithy statement out of it - but at the heart of this is the human notion of computation of which the two leading models of expression are the lambda calculus and the Turing machine.
They're both equivalent so it should almost be expected that if you go far enough along the horseshoe you end back on the other side.
A bit like how Java eventually got lambdas.
But when the capitalists come up with communism, it is interesting to observe the implementation they come up with after travelling the road they have come, much like when the communists come up with capitalism...
that's..not the right takeaway from -XApplicativeDo, or do notation in general even.
I'm actually not sure what the C-like syntax you're referring to is? The fact that it reads like imperative code (even though it could be defining a parser or something not imperative)? Or the fact that it uses the word `return` (many use `pure` instead..`return` is free for anyone to bind a variable too)?
ApplicativeDo allows the previously well established monadic do syntax to be applied in cases where the type you're working with is only applicative. It's not new syntax - it's been around since 1996.
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!
> 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".)
In 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.
> 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) 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.
In 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
In 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.
No, 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). Which is the solution for IO that Clean, the other lazy functional language, uses.
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".
...and the ability to treat I/O computations (these "descriptions" of I/O to perform) as plain values in the code is the reasons Haskell is sometimes affectionately known as "the best imperative language".
It's not that you couldn't do the same thing in Python, it's that you get more fine and precise control over what happens by default in Haskell.
It would work in the sense that it would not throw an exception, but it wouldn't work the same way in every situation. Haskell's `greet "themagicalcake"` does not correspond to Python's `greet('themagicalcake')`, nor does it correspond to Python's `lambda: greet('themagicalcake')` in every situation. E.g. you can write
main = greet "themagicalcake"
in Haskell to get a program that just prints
Hello themagicalcake
but the Python program
if __name__ == "__main__":
lambda: repeat3(greet('themagicalcake'))
Do we even need side effects? Most programs seem to be about transforming some input to an output and/or storing/retrieving state in a transactional manner. Perhaps I/O and transactional storage could be kept outside a language and instead be managed by its containing environment?
For example, console I/O: The input from the console could be transformed (possibly with some state) and sent back out (possibly triggering an associated state update) as a fairly natural pure function call. It would have the type state -> input -> (state', output), and the 'runtime' would be responsible for gluing this all together.
Suppose it's not the console. Suppose it's a disk file. You can write to it. You can read back in what you wrote out. That means it's a giant global state. If I understand correctly, writing to it cannot be pure.
> 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.
Yes, 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.
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.
> 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!
I don't agree. Bind creates a data dependence that forces an evaluation order.
do
x <- f
y <- g x
No matter what Monad you're speaking of, in this expression "x" is going to be evaluated before "y", what we sometimes call a happens before relationship, because evaluating "y" depends on evaluating "x".
Unless you're using some non-standard definition of evaluation, this is not true when using a lazy strategy.
Consider the term ((const c) x) for g x which reduces to c without ever evaluating x.
What are you basing your disagreement on? Can you explain to me in which sense "x" is evaluated before "y" in this example?
{-# LANGUAGE BangPatterns #-}
import Data.Functor.Identity
import Debug.Trace
f = let !_ = trace "Evaluating f" ()
in return 1
g x = let !_ = trace "Evaluating g" ()
in return (x + 1)
example :: Int
example = runIdentity $ do
x <- f
y <- g x
return y
> example
Evaluating g
Evaluating f
2
I 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.
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):
122 comments
[ 5.2 ms ] story [ 60.4 ms ] threadImperative 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?
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_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?
Non-technical answer: Handing over your implementation to an average programmer.
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.
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.
- Wash the rice - Boil the rice gently for 20 mins - Whilst the rice is cooking, sauté the finely diced onions.. - Before they've turned brown...
And then at the next level up is complete base recipes, like "this is essentially a bearnaise except with _____" or "you start with a ______ but instead of _____ you _____". So the recipes are expressed with combinators over modifiers and existing recipes.
[1] http://www.cookingforengineers.com/
[2] http://www.cookingforengineers.com/recipe/172/Pan-Pizza/trn
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.
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.
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).
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.
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.)
"Statecharts"? Some other kind of boxes-and-arrows Moore/Mealy machine visualisation?
It is an interesting observation that declarative logic apparently is more intuitive to non-programmers than it is to programmers trained in imperative programming.
I think that comes from their mathematics background.
I'd like to wheel out the "left handed scissors" argument again; the large and persistent arguments over what is most intuitive should be taken as evidence that different people find different things intuitive or comfortable.
Which is more natural to use, a symmetrical mouse or one with a slight bend to it in a comma shape to more closely fit being held in the right hand? What happens when a left handed person picks it up?
> 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.
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)
Many languages have a package repository like CPAN for Perl, but you'll still find multiple modules trying to do the same thing but slightly differently, and even when there's a clear winner there's often a lot of complexity in using it.
Some things like sending an email are relatively well handled while others like XML parsing have a lot of warts. Then you get that urge to rewrite because there's no possible reason it has to be this complicated, and you become what you hate and add to the mess :)
that is true as well... something of a blind-spot since it happens right under our nose (or maybe said better, right behind out eyes where we cant see it)
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.
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.
http://alumni.cs.ucr.edu/~ratana/PaneRatanamahatanaMyers00.p...
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.
And type names with "state" or "metadata" in.
I do not want to go back to map and bind.
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!
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.
I too can't help but chuckle at some of the substance less commentary at HN, where chuckling is a substitute for inability to contribute anything to a discussion.
This could be doing any one of:
- flatmapping over lists
- doing a computation with Maybe types, producing Nothing if a or b are nothing
- doing IO
- managing futures
- pretty much anything, actually
Don't confuse 'x <- a' with assignment, or 'return' with returning a value.
Another way of looking at it, (f x y) might be invoked:
- once
- never
- more than once
- later
It isn't "almost C-like syntax", unless you define that to mean "if I do any amount of search and replace I can make it into valid C code that has completely different semantics". "x <- a" in haskell is not at all the same as "x = a" in C.
And that isn't the reason I said your comment is wrong. Your comment is wrong because do notation is not the result of "40 years of research in functional PL", nor is it an accomplishment. It is simple syntactic sugar that didn't come from research at all.
Your comment could be accurately summarized as "I would like to express how smug I feel for not learning something".
are you kidding me ? if a 2016 paper from Simon Peyton Jones isn't research then what is ? (https://dl.acm.org/doi/pdf/10.1145/3241625.2976007)
But your chuckle has shown me that this code is just plain old C, with links to documents you haven't even read. Thank you for enlightening me, great chuckler.
Here is some basic reading material for you. Lets see if you can follow this
https://hackage.haskell.org/package/async-2.2.2/docs/Control...
This implements async await in Haskell via a library using Monads. Hint: you cannot do this in C via any library. You need to modify the language. If you are uneducated about Monads, you might think this is similar to some kind of C code. It is not!Here is the same do syntax being used to parse source files instead of doing async/await IO. Yes this parser will also perform backtracking etc, which will not be obvious to anyone who thinks this is some kind of transliteraton of C
https://markkarpov.com/tutorial/megaparsec.html
mySequence :: Parser (Char, Char, Char) mySequence = do
Hint: If you want to do something like this in C, you use yacc/bison, a parser generator tool - there is no library. And it is fucking ugly.Hint 2: There are monads for parsing, async IO, exceptions, nondetermenism, monte carlo .........
Hint 3: The parser libraries are 25 years old.
Hint 4: Monad libraries represent less than 1% of PL research over the last 40 years.
Hint 5: C# LINQ is basically a Monad.
Hint 6: When confronted with something you don't understand, it is better to be quiet instead of opening your mouth and advertising your ignorance.
Hint 7: If your conversational style consists of chuckling and providing "Hints", be prepared for receiving a taste of the same medicine.
> This implements async await in Haskell via a library using Monads. Hint: you cannot do this in C via any library. You need to modify the language. If you are uneducated about Monads, you might think this is similar to some kind of C code. It is not!
it's still pretty much
> almost C-like syntax once again.
at no moment, not a single time, did I say in any way that I was talking about semantics but this is apparently a hill you wish to die on... so, by all means have fun !
Maybe you'll be more convinced by the exact text of the paper that introduced do-notation though ?
http://citeseerx.ist.psu.edu/viewdoc/download?doi=10.1.1.11....
let me quote it :
> Using monads gives functional programming an imperative flavour. Gofer supports a, so called, do notation which makes this imperative flavour more apparent.
or, let me rephrase that whole thing for you in a language that you seem to speak quite fluently:
You also failed to show how parser combinator grammars such as yacc/bison/BNF forms are basically just imperative code.
Monads model effect systems; not imperative code.
I am impressed by your ability to argue about stuff you know nothing about, and linking to papers and documents you can't even read. Have you considered running for US president?
but async / await is semantics, not syntax, thus completely irrelevant to the conversation. you want an example of building async / await into the syntax of C as a library ? tada !
or maybe you would prefer the more "modern" version, still compatible with pure C89 syntax though ? please go on, this is super entertaining !hint: you are wrong.
The last time there was someone chuckling, I was way more civil.
https://news.ycombinator.com/item?id=22712767
This thread is silly.
I believe being able to understand code on high level quickly is a good thing in engineering. Wouldn't it be nice if looking at some part of code you could say that it actually is calling microservices X, Y and Z and no other microsevices? In Haskell you could have that. I don't see any fundamental reason why such functionality couldn't be brought into an imperative language at some point (but maybe I'm ignorant about PL theory). (FWIW I don't believe in Haskell going mainstream at any point).
What always makes me a bit uneasy about that is the absence of proper escape hatch in functional langs. I am not old and yet I have already been in time-critical situations - where all you have is sometimes only a few minutes - to do a quick hack that will save a few more hours to actually investigate and do the proper fix (think physical installations).
And, I would really not call unsafePerfomIO a proper escape hatch given the big attached warning that comes with it:
> If the I/O computation wrapped in unsafePerformIO performs side effects, then the relative order in which those side effects take place (relative to the main I/O trunk, or other calls to unsafePerformIO) is indeterminate.
> If the I/O computation wrapped in unsafePerformIO performs side effects, then the relative order in which those side effects take place (relative to the main I/O trunk, or other calls to unsafePerformIO) is indeterminate.
Isn't that exactly the situation you'd be in in a non-functional language by default? E.g. in C, evaluation order for function arguments is indeterminate, so the order of any side effects that each argument does is also indeterminate. The only way to force a particular evaluation order is to arrange your code to have suitable sequence points in place - which is not really any easier than composing IOs properly in Haskell.
I don't know any non-meme program where entire call stacks would be in a function call. The worst cases I've seen of this were things such as
which is a far cry from the issues you'd have in anything doing lazy evaluation of a program graphe.g. I just ran
in the Qt codebase and I can't seem to find a single example which does more than calling trivial getters, e.g. sort(v.begin(), v.end()) or foobar(point.x(), point.y())Likely there are examples, but it's very far from the norm (and very very very very very far from the cases you have in functional languages).
But if they look closer, they find out it is not really imperative. Why is there a difference between let and bind? Why can't I reassign values. And they start to use more exotic and also more interesting monads and boom the imperative feel goes away.
For example with the interesting LogicT[1] monad. Or what about the probability monad[2]? Or the continuation monad[3]? Or the reverse state monad[4]? How imperative are those really? It really depends how you use it. And I think it adds more to the magic, that you think it is just imperative code at first, because suddenly you can create code that behave very different under various monads that represent computation models. I think that is a cool thing about this confusion.
I understand that if you are a professional that needs to get work done, it can be annoying.
[1]https://hackage.haskell.org/package/logict [2]https://hackage.haskell.org/package/probability [3]https://hackage.haskell.org/package/mtl-2.2.2/docs/Control-M... [4]https://lukepalmer.wordpress.com/2008/08/10/mindfuck-the-rev...
Nim can kinda do this with its effect tracking feature (although I haven't used Nim enough yet to know if anyone is really using it), e.g.
The `tags` pragma adds specific tags to the proc and the compiler ensures that it doesn't call anything with any other tags. Similar mechanism is used for exceptions too.They're both equivalent so it should almost be expected that if you go far enough along the horseshoe you end back on the other side.
A bit like how Java eventually got lambdas.
But when the capitalists come up with communism, it is interesting to observe the implementation they come up with after travelling the road they have come, much like when the communists come up with capitalism...
I'm actually not sure what the C-like syntax you're referring to is? The fact that it reads like imperative code (even though it could be defining a parser or something not imperative)? Or the fact that it uses the word `return` (many use `pure` instead..`return` is free for anyone to bind a variable too)?
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!
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".)
I can confirm that, the recent 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.
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)
Please at least keep everything flowing in the same direction!
do x <- b =<< a ...
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). Which is the solution for IO that Clean, the other lazy functional language, uses.
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
you get the following output: But if you execute the similar-looking Python script you get 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".It's not that you couldn't do the same thing in Python, it's that you get more fine and precise control over what happens by default in Haskell.
repeat3(lambda: greet('themagicalcake'))
For example, console I/O: The input from the console could be transformed (possibly with some state) and sent back out (possibly triggering an associated state update) as a fairly natural pure function call. It would have the type state -> input -> (state', output), and the 'runtime' would be responsible for gluing this all together.
> 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.
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.
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:
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.
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!
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.
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:
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:
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:
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): 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
The nulling out of last_form_evaled is inserted into the unbroken sequence of initializations via the comma operator.