Haven't seen it listed neither here nor there, so not sure if I'm the only one, but: for me, the first and currently blocking obstacle is of "graphical" syntax. I'm of the kind of people who hear the words they read as a voice in their head, so when every line is interspersed with multiple "random" <:> >>= <=< @>-,-'-- and whatnot other ascii-art I can't verbalise, I distictly feel my brain stumble, mumble, and grind to a halt and sad emptiness. And I don't even know how to google any one of them. Don't know how to solve this for myself. I've once found a list of suggested names for some common ones, but - I don't like having to learn all that by heart before I even start with the language; and also I feel pretty certain those are not all, and many libraries seem to like to invent their own new ones, so back to square one.
That's I believe the main reason I find SML more attractive: it seems to use operators to a much, much lesser extent, apparently preferring alphanumeric function names in the core, and in 3rdparty libraries too.
I felt the same way (and still do in general, with notable exceptions). I.e. to put this first: I think a lot of the Haskell community is obsessed with mathematical "cuteness", which basically they take to mean "infix-operator-heavy" notation.
Nevertheless, I have learned to like some operators,
<$> <*>, *>, <*
are ones that come to my mind. The operator
<$>
is basically fmap as infix notation, so `fmap f [1,2,3]` would be `f <$> [1,2,3]`. This makes mapping as easy as a normal function call (and the operator name `<$>` was not choosen arbitrarily. `$` by itsself is the function application operator, so `f $ 3` does on a single element, what `f <$> x` maps over an instance of Functor.
Now that is the first time I see an explanation which tries to point out some sense in the apparent madness. Appreciated! Would you or someone else knowledgeable be willing to create a guide like this, but spanning all the magic runic incantations known to Haskell wizards? Showing similarities, analogies, etc? And targeted at dumb apprentices like me? I'd be happy to serve as a somewhat (though not perfectly) dumb specimen to bounce explanation attempts off! (email in my profile - just added, in case someone was crazy enough to try to pull that off)
It's nice that someone was trying to make things sensible. But what are the precedences of those operators? Maybe you would argue that you can expect them to be whatever makes the most sense. But what about the one case when they aren't what you expect and you're getting some confusing error (or, gasp, incorrect runtime behavior). Maybe a library designer made a bad decision about which precedence to give to a certain operator. Or maybe the choice was simply not so obvious.
I guess the solution is just to put parentheses everywhere, but why is it that, whenever I show people code where I'm doing that, they always point out "Oh, you don't need parentheses there." I feel like that just goes to show that the community underestimates the confusion most people feel when they encounter a jumble of custom operators and have to recall to themselves which parts of an expression are evaluated first or even in what direction. This language feature feels more like an antipattern.
I really think the benefits of making operators so easy to define are quickly outweighed by the drawbacks for most programmers. The infix culture seems like one of the biggest barriers for this language.
I concur with your uneasines about operators, I often find them used "too often" in Haskell, I have been a Clojure/Scheme guy in the past.
But: For Functors and Applicatives for example, they do make sense. If you are using them, you can use them in any program you write. Just like we found addition and multiplication with numbers build an abelian group and chose to represent these operations by "+" and "-" (with the difference that in Haskell Programs, Functor-capable data structures pop up everywhere).
So that being sad about the motivational level, I would like to address some of your arguments against operators on the syntax level.
Precendence can be a problem and obviously it is something that is and has to be learned and might confuse beginners.
Nevertheless, Haskell's typechecker will often tell you when you missed up. I started of parenthesizing the hell out of these expressions and let hlint tell me when I could remove the parens, after a few days I hardly made any of these mistakes anymore.
While - as a Lisp guy - i do prefer functions over operators quite a bit, it is not like it is a terribly difficult thing. People have committed to learn inheritance, and multiple inheritance. Write classes where the hold and manage a lot of state and let that state interact, etc. That stuff is magnitudes more complicated than Haskell operators, already on a conceptional level.
It helps to read $ as "value". Then "function $ x" is "function value (of/at) x". Works in some other languages too, e.g. $VARNAME = value (of) VARNAME.
Haskell is too sugary; there's too many ways to write out exactly the same thing (semantically identical, not computationally). And most resources choose one way to do things over another and it all becomes fractured and difficult.
In comparison, J is all about symbols, but provide a canonical dictionary and strict semantics about how those symbols work. Still complex to learn, but easier overall.
The "separatorless" style is also another aspect of the syntax that's turned me off Haskell --- most other languages have characters like ';' that serve to delimit statements, ',' to separate list elements, and ample use of parentheses to clarify how things should be parsed (e.g. identifier followed by '(' signals "function call", and the matching one signals the end of the argument list.) In comparison, Haskell code reads to me like a giant run-on sentence where you need to make much greater use of context in parsing.
I suppose the other extreme is Lisp, which I've played around with and found quite pleasant because of its very unambiguous syntax.
In recent years, I've switched to a Haskell style that is much more "separator-ful". I chain my computations together using the `&`, `<&>` and `>>=` operators. These operators are flipped function application, flipped fmap, and bind.
They are all declared as left-associative with precedence 1, which means that they read left-to-right. i.e: they're the separators, so you don't need extra parenthesis.
So you can have something like:
employees database
<&> salary
& sum
& writeReport title
>>= sendReport destination
This lets you read your data processing pipe-line, while also seeing what kind of effects are being composed together at each step.
I agree that Haskell library writers can assume too much knowledge of operator precedence on the part of their readers. It can often make it very hard to read unfamiliar code.
Haskell does seperate list items (and tuple elements, and record member assignments) with commas.
You are, of course, free to object to the lack of separators between (and parentheses around) function arguments. I find it pretty natural when everything is curried.
It can be overwhelming, and trying to use Google is terrible. There's a really nice Haskell specific search engine though: https://www.haskell.org/hoogle/ - you can query module/function names, type signatures, etc.
A lot of these weird "operators" are also typically defined with a synonymous named function. These types of operators are usually functions after all, not actual language operators.
It seems like my ability to hold logic in my head is partly visuo-spatial, and the terseness makes it easier for me to tell what's going on. (I also do Scientific Computing, so a general resemblance to mathematical notation is hugely helpful for me reading code).
I think similarly (I sometimes write code like this
let A be sth
X be sth else, ...
and then the equations and predicates and pseudocode using one-letter names). Then I change these to regular variable names of course (my friends don't think it's readable).
I also really like the fact that in clojure identifiers can use math symbols exactly for this reason. I love the fact that + is a regular function, that I don't have to wrap it to reduce or map with it, and don't have to remember how to turn it from infix to prefix or vice-versa. That it works with any number of arguments. Same with <, >, =.
This the prettiest definition of dot product I've seen in any programming language
(reduce + (map * v1 v2))
It's great because with 2 very universal language constructs you get to express tons of stuff cleanly. There's no incidental complexity there.
But I just can't force myself to like Haskell operator-happy style. I've tried learning Haskell a few times, and never sticked to it for more than a week, because operators everywhere just make my uneasy, and there's too much specialized version of functions.
I tried to learn common lisp a few times before clojure and nvere managed to get through the weird historical naming nonsense. Haskell sometimes feels like common lisp in that respect.
Edit: Granted ">>=" (bind is the word you are after) is kind of special and you need to understand more of the underlying language mechanics to get it. The provided explanation tells you little unless you can grok the function signature. I think it's almost universally accepted that a lot of the documentation is atrocious.
If you are ever confronted with a non-trivial parsing problem, have a look at Haskell again and use Attoparsec. <$>, <>, >, <*, <|> will become incredible concise tools for expressing your ideas!
I suspect this is due to its mathematician-heavy user base, who are very used to just defining new operators with a squiggle on the blackboard. The worst language for this was APL, where most of the common operators could not be typed on a normal keyboard.
Interesting that Haskell goes in so hard for operator overloading. In other languages I've used, while you can overload a bunch of operators, it's not a common thing to encounter, outside of math code, where it makes the most sense. Technically you could do all sorts of crazy operators in C++, but that tends to be frowned upon in that community.
It's not operator overloading. It's using symbols as function names. Some operators like (<>) may have different implementations based on the Monoid instance of the underlying types, but they are based on laws which is different from say Python where >> can mean anything based on the context (even though it is used sparingly there).
I'm of the kind of people who hear the words they read as a voice in their head, so when every line is interspersed with multiple "random" <:> >>= <=< @>-,-'-- and whatnot other ascii-art I can't verbalise, I distictly feel my brain stumble, mumble, and grind to a halt and sad emptiness.
Incidentally, this is why prefix notation seems so foreign. (+ 1 2) reads as "plus one two."
It's possible to overcome this, with dedication. (And it's worth doing.)
One thing that long ago helped me to apprehend and appreciate the power of functional programming was to start thinking descriptively (in terms of facts and propositions) rather than prescriptively (in terms of imperatives and commands).
Thus, I read “(+ 1 2)“ as “the sum of 1 and 2” rather than ”add 1 and 2“. Similarly, I read ”(eq (+ 1 2) 3)“ as ”the difference between 3 and the sum of 1 and 2 is zero“.
Haskell has 3 major problems that are completely distinct in my opinion.
Biggest technical problem: lazy evaluation. Other people have said more than enough here.
Biggest cultural problem: technical oneupmanship and code golf. Haskellers can get so caught up in no-compromises stylistic competition that it makes it hard to get anything done. If you finally finish up something that accomplishes your goals, your teammate is gonna come in and rewrite everything you just wrote so he can feel happy with it too. And then lecture you about how lenses are wonderful and how you need to learn the principle of least power.
Biggest community problem: a small minority of extremely obnoxious and toxic assholes. They raid other communities and make fun of their languages and members. They rant against and mock everything deemed inferior. They treat people like they're idiots for not understanding basic but unfamiliar principles of the language. And nobody ever seems to shut them down, despite the fact that the majority are pretty welcoming.
> They rant against and mock everything deemed inferior. They treat people like they're idiots for not understanding basic but unfamiliar principles of the language. And nobody ever seems to shut them down, despite the fact that the majority are pretty welcoming.
I'm happy to try to shut them down but I've pretty much never seen it (perhaps because I'm not in those other communities). If you do, please point me to it. Contact details here: http://web.jaguarpaw.co.uk/~tom/contact/
There were a few big controversies on Twitter and Reddit that I can recall. In one instance in particular, someone called out some Haskell users trying to make Haskell look better by publishing code examples of common algorithms that "cheated" (i.e. the Haskell quicksort that's so popularly paraded around isnt a true quicksort). After the person argued this, they were banned from the Haskell subreddit.
I find it very hard to believe that someone would be banned from the Haskell subreddit for (correctly) pointing out that what has (curiously) entered Haskell lore as Quicksort is not really Quicksort. We all know this.
> Biggest technical problem: lazy evaluation. Other people have said more than enough here.
I am actually wondering about that. It is considered essential by the classic paper "Why functional programming matters", for better program composition.
In particular, I wonder how are you supposed to pass around IO monad (or any other thing whose evaluation will give you side effects) as a value, if you have strict evaluation. I thought the whole point of Haskell was to lazily create a "recipe" as an IO monad, which will then be evaluated after it is returned from function main. In particular, I wonder how Idris deals with this problem, as I read it has strict evaluation.
Idris, as you mention, is eager and strict -- and is still pure and uses IO in the same way as Haskell.
Evaluation in Haskell does not cause side-effects, including evaluation of IO action values. It is the execution of these IO actions (by the RTS, due to their inclusion in `main`).
For example:
let x = map print [1..10] :: [IO ()]
x is just a list of values, each representing the action "print the number N" (for varying N). Evaluating this list doesn't cause printing. Now if you use:
let y = sequence_ x :: IO ()
You now have another pure value, y, which represents the action to print all the values from 1 to 10. Evaluating y still doesn't cause anything to happen.
main = y
NOW you've actually scheduled the effect denoted by 'y' to execute.
In other words, main is assigned a composition of effects, that by virtue of being inside main, will get executed. This has nothing to do with evaluation order, and when or how these effect descriptions got evaluated.
> the whole point of Haskell was to lazily create a "recipe" s an IO monad
This "recipe" explanation is one way of explaining how you can think of IO in an impure language. I do wonder if we've taken that explanation too far. I'm pretty sure nobody thinks of it like then when they're actually coding.
Lazy IO's an issue, but Lazy Evaluation? It's not that different from writing SQL, and the places where it is, it's a lot more consistent, predictable, and tuneable. It's usually just bad defaults like Lazy IO, or String instead of Text that get you, but there's some consensus to move away from that.
There are definitely some toxic assholes, as in any community, and it's really shitty if they're going to, e.g., /r/java and harassing people. I feel pretty strongly that the opposite is a more common problem, though, particularly on HN: the anti-intellectualism against functional programmers, and Haskell users in particular, is rampant and gets pretty offensive. Example off the top of my head - https://news.ycombinator.com/item?id=10527745 - article simply explaining a math concept gets, as what was the top comment at the time, a condescending comment calling it all "mental masturbation". I've stopped reading HN so much because it's pretty clear that people trying to learn abstract algebra and apply it to code aren't welcome here.
I don't think people mean to attack you, and it's really difficult for me to understand why the comment you linked could be considered offensive.
I understand you have a certain love for Haskell, and that's a wonderful thing. But not everyone appreciates it as much as you do. One person's artful philosophy is another person's mental masturbation. And both people are entitled to their opinions.
Perhaps you decided to stop reading immediately after you searched for that comment, because you'll see the top reply agrees with your opinion.
There is no "anti-intellectualism" here. There are people who want to get something done at any cost, and there are people who think the journey is more valuable than the destination. Both people have a reason to think they're correct, and neither perspective is any more valuable.
You're on a forum run by a Silicon Valley VC, for goodness' sake. What opinion do you think you'll hear most here? Probably not the same one you'd hear from the wonderful folks on #haskell. It's not "anti-intellectual", it's different.
(also, there are more FP lovers here than you think.)
The grandparent brought up the concept of a community problem where members from a community actively seek out and mock members of another, completely unprovoked. I won't argue whether members of the Haskell community do it or not - I haven't seen it, maybe I've just missed it, but it's terrible if they do. Whether you think it's a particularly good example or not, I'm merely pointing out that it happens a fair bit in the other direction here on HN, and the community either supports or turns a blind eye to it.
That's what's obnoxious about that comment - it's completely unprovoked trolling, and yet it was heavily upvoted. Of course the first reply disagrees with it - If I go to a thread about a Ruby library and insult people for wanting to use Ruby, you'd expect the top reply to disagree. What you'd hope not to see is for that trolling comment to be overwhelmingly upvoted to the top, and for the community to be making excuses as to why in this instance it's ok.
If someone's not interested in some content, or disagrees with it, that's one thing, but to actively find, mock, and seek community support in ostracizing people who are interested in learning it because of that interest - I'd argue that's anti-intellectual by definition. But whatever you want to call it, it's, as the grandparent comment stated, a community problem. Sadly for the author of that comment, the HN community excuses that sort of behavior - it's just different from the wonderful folks in whatever language community they're used to.
In my opinion, Haskell community was very warm and helpful, and also able to admit shortcomings.
This was before Stack Overflow reshaped the landscape. On Freenode, ##java was a pretty hostile place (having to deal with people asking to do homework for them or being overall clueless) with attitude "if you see a problem with Java, the problem is you". The culture was so toxic that "forces of evil" ended up stealing the channel from under its original founder and banning him.
On #haskell, you would be immersed in polite chat revealing the development in all things FP, plus even pretty stupid questions were answered in such way that they enlightened both the asking person and bywatchers.
Also they had quite a few awesome bots that let anyone practically develop directly in IRC.
> In my opinion, Haskell community was very warm and helpful, and also able to admit shortcomings.
I just wanted to second this. On a whim, I started attending various community events in/around NYC a few years ago, knowing next to nothing about the language, and found _everyone_ (no exaggeration) I interacted with, saw present, etc. to be welcoming, helpful and encouraging.
>> Haskell sucks because both the standard library and the ecosystem lack guiding ideas. The former often feels like it is an amalgamation of PHD theses that have been abandoned soon after being complete.
My main gripe is that the syntax is too sugary. I've often wondered to myself what a Haskell with syntax more in the spirit of Python would look like. I just wish I didn't always feel like I had to literally memorize what various custom operators meant with flashcards or something. Also, Haskell has relatively complex parsing and evaluation rules. The meaning of well-written code shouldn't be obscured by syntax.
And, along the lines of the syntax, the culture of style in Haskell. Single letter variables abound. Not just throwaway vars where the meaning is pretty clear from context. Entire libraries written with only single letter or acronymic variable names. Good luck understanding what a library author was trying to do by reading their code. I think this culture problem is largely being driven by the fact (which I mentioned) that Haskell's base syntax is already very opaque. I understand that the language takes some cues from the mathematical community (not surprising given its history). However, the language needs to ditch this to gain broader acceptance.
And, again, leading into my next point: the culture of the community in general is too academic. I'll make another comparison with Python. The Python community, among scripting languages, leans more toward the academic side but in a good way. They've found a nice balance. It's easy to get started and, if you want to learn more about the details, you can do so. With Haskell, you immediately get hung up on the details. Most of the details have to do with how Haskell fits into the framework of category theory. However, most programmers don't give a shit about provability. They just want to write software that is faster, safer, and more concise without being terse. I'm not convinced that it's impossible to have this in a functional language with the same goals as Haskell. Beginners should be able to get started quickly and without much confusion. If they're curious about the underlying principles, they should be able to gradually peel back the layers and learn more.
My biggest complaint would be about incomplete documentation of GHC. Given that Haskell is a pure functional language, it should be possible to easily use any part of the compiler, and include it in your own project. However, the documentation is rather incomplete, difficult to understand, and always behind the current state of the code. That said, the scientific literature on Haskell is the complete opposite, and contains beautiful work.
Also, I'm wondering why there are so few fully compliant implementations of the internet RFCs. Such specs seem an excellent opportunity to showcase the power of functional languages, yet the implementations keep lacking.
The Roslyn C# compiler is exposed as various libraries (and is open source) and is quite nice to work with. I was able to utilize it to make a C#-to-HLSL/GLSL compiler fairly easily.
My answer would be lazy evaluation by default. It's extremely unusual, and I've never seen a convincing enough justification for it, and it gives rise to performance bugs (space leaks) that can be fiendishly difficult to track down and fix and are disastrous in production. With the arrival of Idris, I think we can pretty conclusively say this was a mistake and that the Miranda branch of the PL family tree is likely to prove to be an evolutionary dead-end.
My perspective on this is that lazy be default made sticking to purity much more compelling as if you just dropped print statements in you weren't sure exactly when they get evaluated. This lead to important developments like IO (they started with user input just being a lazy list! Very possible to accidentally block trying to read too much), which might not have happened otherwise.
But now, I do feel like lazy is best kept for core data-structures and places where you know you can use it, and a strict by default variant of Haskell would be better, now that we have learned most of the lessens from lazyness.
"My perspective on this is that lazy be default made sticking to purity much more compelling as if you just dropped print statements in you weren't sure exactly when they get evaluated."
And that's basically a death sentence to the feature because it reduces it to a silver lining of desperate post-mortem optimism. As far as my understanding goes, lazy evaluation was originally included in the language because it was perceived to be a powerful optimization technique enabled by pure functional programming. You reduce the amount of work a program is doing dynamically at runtime while not trading off on readability or modularity. All win. Except there were subtle trade-offs that have made themselves clear over the years and like you said, most would agree that lazy-by-default is too extreme of a feature and doesn't really pay its share of the rent at the end of the day.
My main gripe with lazy evaluation is that it's implicit behavior. It's to evaluation what garbage collection is to memory or dynamic types are to types. It hides something from the programmer that is useful to know more often than not.
> My main gripe with lazy evaluation is that it's implicit behavior. It's to evaluation what garbage collection is to memory
Are you saying you would like to return to the languages with explicit memory management? Or why is, generally speaking, GC considered good and LE considered bad?
> Are you saying you would like to return to the languages with explicit memory management?
Not really, but that does seem to be the logical conclusion at the end of the day. Haskell makes a really good case for the use of a garbage collector via pure functional programming, but I'm open to more fine-grain programmer-driven mechanisms for handling memory. I need to try out Rust's borrowing system on a meaningfully large project before I can start saying anything conclusive.
Hallo. I see some of you are listing things here. It really would be very helpful to me if you could list them on Reddit as well, using the format of the thread, so that I only have one source to deal with.
I don't mean to instigate any Reddit vs. Hacker News rivalry, or whatever, but I just don't have time to go over several sources for my talk and PDF. So this is just a tip for if you want me to include your Haskell "complaint".
Glad to see the thread has sparked discussion here too!
I had chance to debug one tool built using Haskell. I was amazed how Haskell solution for clean and easy to understand (it was a parsing library generating better optimized SQL queries).
The main problems are:
- it is hard to find examples/snippets of real-world solutions on net (majority of examples are like Hello world and these examples are not useful)
- lack of documentation about building real-world solutions
Maybe more work should be done in explaining how Haskell can be used to solve some real-world problems. Parsing seems like place where Haskell shines and more examples should be available.
So maybe just focus on "parsing with Haskell" and win that space.
Having these kinds of threads, I think, is very important for the community. On a related note, there has been quite some fallout from the haskell compilation times got worse thread, which resulted in the development of tooling around improving the situation.
Folds can be left-associative or right-associative. (l or r suffix).
Folds can be eager or lazy (prime suffix for eager).
Folds can work with at least 1 element to avoid the empty case ('1' suffix) (e.g: a function like `maximum`).
This gives 2^3 cartesian space of folds. Each and every one may be useful in some cases. For ordinary lists, though, foldr and foldl' cover virtually all uses (just use 'error' for the empty case when you need to).
Monad transformers don't require O(n^2) anything. You're talking about the 'mtl' package on top of the 'transformers' package. The 'transformers' package is useful on its own and doesn't have this problem.
The 'mtl' automatic lifter framework does require something in the order of O(n^2) instances (for n=6 or so). However, this is not really just 6 copies of the same code over and over. The threading of monadic state through different monads varies. For example: `Reader.local` is implemented very differently for `WriterT` and for `ContT`. So there really are in the order of O(n^2) different interactions to write code to account for.
The non-primed versions simply shouldn't exist (everyone agrees) and in my opinion the 1 versions shouldn't exist either. This takes 8 down to 2, and they are genuinely two different functions so both need to exist.
I agree with the poster complaining about the wide variety of symbolic operators: readability becomes extremely important as programs grow large, and Haskell simply doesn't have it.
Lazy evaluation really doesn't pay off in my experience. Any major Haskell program turns out off for performance reasons. And when you don't, performance isn't the only problem: lazy programs often produce really inscrutable errors.
I only dabbled in it a little for fun, but in my beginner's opinion:
Pros: The compiler eliminates a lot of potential bugs, so once my code compiles, I've saved hours that would have been spent debugging if I were using, say, C++.
However,
Cons: It takes more hours to figure out why my code is not compiling!
Basically, instead of debugging my code, now I have to "debug" GHC, trying to figure out what it is thinking and why it's rejecting my code.
Put another way, C++ allows me to try "quick and dirty" solutions first, and then later iterate and improve. Haskell requires me to be perfect at first try.
One thing that helps A LOT when it comes to those compiler error messages is writing lots of type signatures. They help keep the error messages more "localized" than they would if you rely on global type inference. I would recommend at least writing a type signature for every top-level function in your programs.
That said, Haskell still has many features that make errors more complicated than other languages. One example is that the use of whitespace for function calls means that some things (like forgetting a comma in a list) become type errors instead of syntax errors. Another example is the overloaded numeric literals, which add "invisible" typeclasses to any code you write that uses numbers.
Coming back to your comment, I think that the "quick and dirty" bit has more to do with you being more used to C++ than you are with Haskell. You can also write dirty code in Haskell if that is what you really want :)
If you prefer the "quick and dirty" way, you can use the flag -fdefer-type-errors to turn type checker errors into runtime errors that throw exceptions.
When you gain experience in both C++ and Haskell, the time you spend debugging runtime errors in C++ diminishes somewhat. But the time you spend debugging type errors in Haskell diminishes to almost nil.
Experience and practice makes perfect -- debugging a type error becomes easy. Maintaining a large C++ application and changing something is more scary (even for the expert) than changing something in a large Haskell program.
Haskell sucks because you can't really get anywhere before understanding monads well, and monads are (take your pick) a) too hard for most programmers, or b) too distant from the abstractions used in most programming languages.
Not that's you're done mastering Haskell when you've figured out monads, of course. There are plenty of harder abstractions running around. But monads are the orgo of Haskell, the place many earnest dreams go to die.
I don't feel at all that this is true. It is not necessary to understand monads to get good work done in Haskell for basically any purpose. Authoring monads is something usually left to library authors, and for basic users it's just a nifty interface that lets you do handy things in imperative style haskell ('do' syntax).
I don't want to be defensive in this thread, since obviously getting real about deficiencies in Haskell is a great way of making a path for positive change, but the spreading of the idea that monads are magical and hard is dangerous.
It's similar to how back in the day Java and PHP developers dismissed Ruby on Rails for being magical and hard to understand with weird syntax. The truth is that Ruby on Rails was very easy to use regardless of its rather complex innards, and once you get a good handle on Ruby, even the Rails innards weren't all that difficult to understand and extend.
The hard part of learning Haskell is getting to grips with functional style programming and understanding the power of its type system. Learning that is rewarding and has a low entry-barrier, it's even something I'd recommend to someone who has never programmed before. Don't worry about monads.
This would be a reasonable critique except that monads are really why Haskell ends up being so powerful and composable. Sure, they can be tough to wrap your head around, but without them Haskell wouldn't be particularly special or useful.
It's more complicated than that. Monads don't make a language more powerful, but they are an absolute necessity given Haskell's design, which is built around extensional/value semantics, which basically approximates a program/subroutine as the function it computes; this is a useful approximation as it lets us treat computations as if they were referentially transparent. That approximation has a cost, though, as computations are not quite functions (they are processes that compute functions), and while the approximation works well enough much of the time, some things require recapturing the more accurate definition of computations as continuations (i.e. a computation can block and then resume). If your language does not have continuations, only functions, monads achieve the same thing. If, OTOH, your language has continuations (as most non-pure languages do, although most don't have reified continuations, which are just as "programmable" as monads only more composable), monads don't really help.
It's not really Monads making the language more powerful.
It's the taking away of non-pure primitives and libraries -- and replacing those with type-labeled effects.
That these effects are composed monadically is a minor detail and unfortunately the thing that's emphasized.
Haskell gains power in its framework for restricting code. In Haskell, you can know so much about what code doesn't do -- and that's what makes Haskell special and powerful compared to other languages. You have to explicitly opt out of these restrictions - making the majority of code easier to reason about, simply because there is so much it cannot do.
> It's the taking away of non-pure primitives and libraries -- and replacing those with type-labeled effects.
PFP and typing are quite orthogonal. You can have effect systems in non-PFP, continuation-based languages (i.e. languages that don't equate computations with functions). You can have non-pure, non-monad-based type-labeled effects.
> Haskell gains power in its framework for restricting code.
Sure, but Haskell does this within a very specific design, based on extensional functional semantics. There are other ways of restricting what code can or cannot do that don't have the same semantics. Personally, I think that various effect systems may hold promise and are certainly interesting enough to try, but the PFP abstraction has so far failed to yield results commensurate with its cost.
> But the benefits are reaped from the combination.
Well, I happen to think that some forms of rich typing are quite beneficial, but that value-semantics is a negative. I don't see Haskell having any tangible benefits whatsoever over, say, OCaml (other than ecosystem-related stuff), which has one (relatively rich typing) but not the other (PFP).
> You say this based on what?
Based on the fact that the few Haskell shops out there -- despite them being composed of avid enthusiasts and people who devote a lot of thought into the Haskell way of thinking, are not reporting even 2x productivity gains (although I don't know what huge means to you). I mean, some say they feel those gains, but when you look at iteration speed, time to market etc., you see negligible advantage if at all. As to the cost, I won't argue with you, but I encourage people who are interested in languages as well as in software engineering to try Haskell and judge for themselves.
There are a lot of tangible benefits over, say, OCaml. Let me use one particular representative one: STM.
Haskell can successfully implement a performant STM with static guarantees regarding transactions. How would you add practical, guaranteed STM to OCaml?
> are not reporting even 2x productivity gains
An overall 2x productivity gain is huge. A 10% productivity gain is worth millions over the course of a year, for even a medium-sized software shop.
There are other, very productive languages (at least for initially writing programs) but they tend to be far less reliable. There are other relatively reliable languages (e.g: Ada) but they are far less productive.
> How would you add practical, guaranteed STM to OCaml?
I don't see the relevance. Clojure has STM and isn't pure at all. I can't see why OCaml cannot do the same.
> There are other, very productive languages (at least for initially writing programs) but they tend to be far less reliable. There are other relatively reliable languages (e.g: Ada) but they are far less productive.
I understand that the Haskell community wishes this to be true -- and maybe it is -- but to date there's no data to support this. Whatever little data we have on bugs (the "GitHub study", which might not be dependable, but that's all we have) shows negligible-to-nonexistent advantage to Haskell over other languages.
Not guaranteed STM. If you do IO in Clojure's STM, you just get a runtime error (assuming the IO does not forget to use the runtime-check that it isn't executing in STM context).
> but there's absolutely no data to support this.
Data of this sort is extremely expensive to collect reliably.
I remember reading about a "GitHub study" that did not correctly classify what a "type error" is. Is that the one?
> Not guaranteed STM. If you do IO in Clojure's STM, you just get a runtime error
Two things. 1/ "Guaranteeing" effects has little to do with PFP. 2/ There's no data to support that this level of guarantees has any effect on program quality. I'm all in favor of effect systems (though not PFP) because they're worth a try; but there's a long way to go from "interesting" and "it actually works!" especially if you have no data to support this.
> Data of this sort is extremely expensive to collect reliably.
Fine, but the alternative is to use an unproven, negligibly adopted (Haskell industry adoption rates are between 0.01-0.1%), badly tooled language, that requires a complete paradigm shift on faith and enthusiasm alone. I don't need, or want, to prove that Haskell isn't effective (TBH, I really wish Haskell, or any other novel approach did have some big gains); it is Haskell's proponents that need to support their claims with at least some convincing evidence.
> Is that the one?
I don't remember, but what does it matter? Again, I don't want to prove Haskell's ineffectiveness; it's people who want to convince others to use Haskell that should collect some evidence in its favor.
I started off by explaining why Haskell needs monads and why they don't add power, and later why PFP and restricting what code can do are orthogonal. Peaker then spoke of tangible gains, and I said that something is not tangible if you can't show it, and then you nudged the thread in the direction of your pet project which, apparently, is patronizing people.
More to the point, if you claim Haskell (or, in particular, monads) has theoretical benefits, you need to be able to explain them (and restricting effect is not a theoretical explanation as it doesn't require monads); if your explanation is "this has benefits in practice" then you're really claiming empirical, rather than theoretical, benefits, but then you need to be able to support those. If you go around saying monads have theoretical benefits but when debated claim empirical benefits and then don't support those, expect to be called out for selling snake-oil (and just to be clear, my point can be summarized as follows: Haskell takes a very clear, very opinionated theoretical approach[1], which is beautifully elegant but is not theoretically better or worse, just very different, with its particular pros and cons. Empirically, I claim, Haskell has not yet shown significant benefits).
[1]: Subroutines as functions; mutation as effect; HM types (+ typeclasses). Type system aside, there are obviously many alternatives (other than "classical" empirical languages). For example, languages that require full verification for safety-critical realtime code often employ the synchronous model. In that model, each subroutine isn't a function (but a continuation), but the program itself can be viewed as a function from one program state to the next, and mutation isn't a side-effect, but is very much controlled (see https://who.rocq.inria.fr/Dumitru.Potop_Butucaru/potopEmbedd...). This is a model that lends itself very nicely to formal reasoning, and there are others.
> if your explanation is "this has benefits in practice" then you're really claiming empirical, rather than theoretical, benefits, but then you need to be able to support those
I disagree strongly with your position on this.
Peaker has clearly found, as I have, that Haskell is more effective for him. We have pointed out repeatedly what the benefits are (as well as pointed out the drawbacks). It would be simply impossible for the stated benefits not to be beneficial in practice. The only question is whether the benefits are outweighed by the drawbacks. If you have already paid the one-off cost of learning the gnarly corners of Haskell then those drawbacks are significantly diminished.
You must realise that your insistence on empirical research is an idosyncracy and that people make decisions on programming languages all the time without such research. They are not, in its absence, merely "guessing" which language to use. They are making a decision based on their understanding of their own needs and on the strengths of the languages under consideration.
Furthermore, I wouldn't trust any empirical research on languages any more than I would trust empirical research on cholesterol[1].
In the absense of convincing empirical research pointing in either direction I think people should be free to make informal claims that "Haskell is more reliable than Python and more productive than Ada" based on their own experience and the experience of their colleagues. It's all we've got to go on. It's not ideal, but it's also not wrong.
> It would be simply impossible for the stated benefits not to be beneficial in practice. The only question is whether the benefits are outweighed by the drawbacks. If you have already paid the one-off cost of learning the gnarly corners of Haskell then those drawbacks are significantly diminished.
Provided that you think that the only significant drawback is the learning curve. I think that the PFP abstraction itself is a drawback, and that you can get most of Haskell's benefits (leaving aside their real-world value for a moment) without it. In particular, I think that you cannot point at any real-world benefits of the Haskell approach over, say, the OCaml approach, and that's even before applying things like effect systems to OCaml.
> people make decisions on programming languages all the time without such research
Of course, but if they do, they cannot claim real benefits that aren't real. The reason some people use Haskell is that it fits well with how they like to think about and write code; the reason many people don't use Haskell is because it doesn't fit with their preferred style, and because there is no compelling evidence for why they should even try to change their methodology.
My only "insistence" is that you either claim actual benefits and present empirical data to support it, or don't present empirical data but claim only personal preference. What you don't get to do is say, "Haskell leads to code with significantly fewer serious bugs" while at the same time not show any evidence that it does. The reason you don't get to do that is that such a claim is one with serious (theoretical and financial) implications, and strong claims require strong evidence (or at least some more convincing evidence than what we have).
> Furthermore, I wouldn't trust any empirical research on languages any more than I would trust empirical research on cholesterol
OK, but you're saying that I should go full vegan based on even less than that.
> It's not ideal, but it's also not wrong.
How do you know it's not wrong?
But let me refine this. I agree that it's very likely that "Haskell is more reliable than Python and more productive than Ada", but that's not the real argument. The real argument is that Haskell is a lot more reliable than Python and a lot more productive than Ada. I don't see how you can possibly claim that based just on personal experience.
But let me refine this further: there's personal experience and personal experience. There's personal experience based on measuring actual project costs and comparing them -- even though projects are not exactly comparable -- now that's not ideal but not wrong, and there's personal experience based on gut feeling. I don't know how you can say that that's not wrong. Also, there's collected personal experience from hundreds of projects in many domains and various sizes -- that's not ideal but not wrong -- and there's personal experience from a handful of projects, nearly all quite small, in one or two domains. I don't know how you can say that's not wrong (unless you qualify the domain, which you don't).
At this point in time, all we can say about Haskell is this: some people greatly enjoy Haskell's programming paradigm; some people report possibly significant but not big gains in the handful of medium-to-large production projects where the language has been used. So far the approach is showing some (though not great) promise and requires further consideration.
> Provided that you think that the only significant drawback is the learning curve.
No, you misread me. I acknowledge other significant drawbacks, such as immaturity of tooling and infrastructure.
> I think that the PFP abstraction itself is a drawback, and that you can get most of Haskell's benefits (leaving aside their real-world value for a moment) without it.
OK, that would be great! I am genuinely interested in understanding how to do that. I would love to see PFP as a drawback and obtain its benefits without requiring its rigors. So far I have failed to understand your ideas about how to do that and I can only continue to see PFP as a massive boon.
> In particular, I think that you cannot point at any real-world benefits of the Haskell approach over, say, the OCaml approach, and that's even before applying things like effect systems to OCaml.
(One of) the real-world benefit(s) is that I can write a substantial part of a program and know from inspecting only a single line (its type signature) what effects it performs. How can OCaml give me that benefit?
> What you don't get to do is say, "Haskell leads to code with significantly fewer serious bugs" while at the same time not show any evidence that it does. The reason you don't get to do that is that such a claim is one with serious (theoretical and financial) implications, and strong claims require strong evidence (or at least some more convincing evidence than what we have).
I think that says more about how you interpret informal comments on the internet than it does about those making the comments.
> > Furthermore, I wouldn't trust any empirical research on languages any more than I would trust empirical research on cholesterol
> OK, but you're saying that I should go full vegan based on even less than that.
Interesting. Where did I say you should go (the equivalent of) "full vegan"?
> > It's not ideal, but it's also not wrong.
> How do you know it's not wrong?
At least, it is not known to be wrong.
> But let me refine this.
[... snipped useful elucidation ...]
> At this point in time, all we can say about Haskell is this: some people greatly enjoy Haskell's programming paradigm; some people report possibly significant but not big gains in the handful of medium-to-large production projects where the language has been used. So far the approach is showing some (though not great) promise and requires further consideration.
Entirely agreed. I think my gripe with you at this point is that you read too much in to people's informal claims and cause unnecessary aggravation by derailing threads. It's quite clear that you could actually contribute constructively, so I wish you would. Please can you explain in detail how I can get the benefits of PFP without its drawbacks?
> I would love to see PFP as a drawback and obtain its benefits without requiring its rigors.
> (One of) the real-world benefit(s) is that I can write a substantial part of a program and know from inspecting only a single line (its type signature) what effects it performs. How can OCaml give me that benefit?
That's a property, not a real-world benefit. It's like saying that one of the real-world benefits of Haskell is that its logo is green. In any case, don't know about OCaml, but in Java I just click a button, and get the call tree for the method (or, inversely, get the reverse tree for all methods eventually calling printf). More to the point, see some of Oleg Kiselyov's work on type systems for continuations here: http://okmij.org/ftp/continuations/
> I think that says more about how you interpret informal comments on the internet than it does about those making the comments.
I think that you should decide whether this is a serious discussion or grandstanding.
> Where did I say you should go (the equivalent of) "full vegan"?
Maybe you didn't say that I should, but you did say that it's better to be vegan (i.e. make a very significant "lifestyle" change without any evidence to its effectiveness).
> At least, it is not known to be wrong.
That's true. But I wouldn't go around telling people that being vegan has great health benefit if all we know is that it's not been found to kill you.
> I would love to see PFP as a drawback and obtain its benefits without requiring its rigors. So far I have failed to understand your ideas about how to do that and I can only continue to see PFP as a massive boon.
OK, but first, a few things: 1/ I don't know if by "rigors" you meant difficulties or rigorousness, but if the latter, then I don't see why you conflate rigor with the pure-functional abstraction. Most formally verified, safety-critical software is written in languages that are far more rigorous than Haskell, yet are not pure-functional. Which leads us to 2/ these are not "my ideas"; if correctness is your goal (as it seems to be), most languages guaranteeing correctness do not espouse the PFP abstractions. Haskell, Coq, Idris and Agda are used far less than other approaches to ensuring software correctness. Finally, 3/ I'd like to be careful when I say "benefits", because we don't know whether they are true benefits, neutral or even detrimental to software at large. All I can say that in this context, when I say "benefits" I mean things that I (and you) believe to be positive and see as potentially advantageous in the "real world".
Now, I will give you two examples (of languages used more than Haskell/Coq etc.) for "correct" languages, both of them are very rigorous in the sense of being completely formal, yet they do not suffer from PFP's downsides mainly by being measurably much easier to learn/teach/adopt. They are not generally applicable, but neither is Haskell. The first is the set of synchronous langauges, now used by the industry to design safety-critical realtime software, as well as a lot of hardware. Instead of PFP, it relies on what's known as the "synchronous hypothesis". It has been proven over three decades, as an effective, practical method of writing verifiably correct software by "plain" engineers in hundreds of critical real-world systems. You can read more about it here[1]. A generalization of the approach is called Globally Asynchronous, Locally Synchronous, or GALS, and I believe it has the potential of being a great, more widely applicable way of writing software in a way that lends itself to careful reasoning.
The second (you probably saw it coming), is TLA+. It's not a programming language, so I won't compare it to Haskell but to Coq. Unlike Coq, TLA+ does not rely on PFP, or Curry-Howard (neither do other verification tools, like Isabelle) but goes a step further in not being typed at all. It is not functional yet fully mathematical ("referentially transparent"), and its main advantage is that while having the same "proving strength" as Coq when it comes to verifying algorithms[2] while taking days to learn instead of months, and not requiring any mathematical background more than what any engineer already has. I guess that the answer to "how?" would be "in a manner bearing a lot of resemblance to the synchronous hypothesis".
There are, of course, other formal approaches (like CSP), but synchronous programming in particular has had a lot of success in the industry.
If you want to know about (typed) monads vs. continuations, and their relationship to typed effects, I'll refer you again to my blog post on the subject: http://blog.paralleluniverse.co/2015/08/07/scoped-continuati... and to Oleg Kiselyov's work, which I've linked to above.
[2]: Coq may be more powerful when proving general mathematical theorems, but Coq was designed as a general theorem prover, while TLA+ is a language for verifying software in particular.
Firstly, and briefly, I don't agree with your approach to epistomology. I think we're never going to agree there. Let's just agree to be mutally antagonistic on that front so we can get to the important issue, which is improving software development.
Secondly, I'm interested in general purpose programming, so as useful and interesting as your explanation of synchronous languages and TLA+ are, they are not relevant to me.
I am interested, though, in your thoughts on effects, monads and continuations. I've read everything you've written on the topic including your code on Github (and much of what Wadler and Oleg have written) but I'm afraid I'm no closer to understanding what you're getting at.
Does your notion of "continuation" require threads? If so, Python fails to have "continuations", right?
> I'm interested in general purpose programming, so as useful and interesting as your explanation of synchronous languages and TLA+ are, they are not relevant to me.
There's nothing non-general-purpose in that approach. See, e.g., the front-end language Céu[1], by the group behind Lua (I think). The short video tutorial on Céu's homepage can give you a good sense of the ideas involved (esp. with regards to effects), and their very general applicability. I find that just as the functional approach is natural for data transformation, the synchronous approach is natural for composing control structures and interaction with external events. I think it's interesting to contrast that language with Elm, that targets the same domain, but uses the PFP approach. The synchronous approach in Céu is imperative (there are declarative synchronous languages, like Lustre, that feel more functional) and allows mutation, but in a very controlled, well understood way. The synchronous model is very amenable to formal reasoning, and has had great success in the industry.
It's just that hardware and embedded software has always been decades ahead of general-purpose software when it comes to correctness and verification, simply because the cost difference between discovering bugs in production and bugs in development has always been very clear to them (and very big to boot). There have been several attempts at general-purpose GALS languages (see SystemJ[2], a GALS JVM language, which seems like a recent research project gone defunct). OTOH, I believe Haskell would also be considered by most large enterprises to not be production-quality just yet.
Also, I believe that spending a day or two (that's all it takes -- it's much simpler than Haskell) to learn TLA+ would at least get you out of the typed-functional mindframe. Not that there's anything wrong with the approach (aside from a steep learning curve and general distaste in the industry), but I am surprised to see people who are into typed-pure-FP who come to believe that this is not only the best, but the only approach to write correct software, while, in fact, it is not even close to being the most common one. In any event, TLA+ is very much a general purpose language — it’s just not a programming language — and it will improve your programs regardless of the language you use to code them: it is specifically designed to be used alongside a proper programming language (it is used at Amazon, Oracle, Microsoft and more for large, real-world projects). What's great is that it helps you find deep bugs regardless of the programming language you're using, it's very easy to learn, and I find it to be a lot of fun.
> I am interested, though, in your thoughts on effects, monads and continuations.
Hmm, I’m not too sure what more I can add. Any specific questions? Basically, anything that a language chooses to define as a side-effect (and obviously IO, which is “objectively” a side effect) can be woven into a computation as a continuation. The computation pauses; the side effect occurs in the “world”; the computation resumes, optionally with some data available from the effect. Continuations naturally arise from the description of computation as a process in all exact computational models, but in PFP computation is approximated as a function, not as a continuation. To mimic continuations, and thus interact with effects, a PFP language may employ monads, basically splitting the program/subroutine into functions that compute between consecutive “yield” points, and the monad’s bind that serves as the effect. Due to the insistence of such languages on the function abstraction, having the subroutine return just a single value, composing multiple monads can be challenging, cumbersome and very not straightforward. Languages that aren’t so stubborn may choose to have a subroutine declare (usually if the language is typed, that is) a normal return value, plus multiple special return values whose role it is to interact with the continuation’s scope. An example of such a typed event system is Java’s checked exceptions. A subroutine’s return value interacts with its caller in the normal fashion, while the declared exceptions interact with the continuation’s scope (which can be anywhere up the stack) directly. This normally results in a much more composable pattern, and one that is simpler for most programmers to understand.
> Does your notion of "continuation" require threads? If so, Python fails to have "continuations", right?
"My" notion of continuation requires nothing more than the ability of a subroutine to block and wait for some external trigger, and then resume. Languages then differ in the level of reification. Just as you can have function pointers in C, but that reification is on a much lower level than in, say, Haskell or Clojure, so too languages differ in how their continuations are reified. So, a language like Ruby, is single-threaded and does not reify a continuation at all (I think). You can't have a first-class object which is a function blocked, waiting for something. Python, I think, has yield, which does let you pass around a subroutine that's in the middle of operation, and can be resumed. In Java/C/C++ you can reify a continuation as a thread (inefficient due to implementation). In Go you can do that only indirectly, via a channel (read on the other end by a blocked lightweight thread). In Scheme, you can have proper reified continuations with shift/reset (and hopefully in Java, too, soon, thanks to our efforts).
Why, restricting code, of course! A subroutine can be a continuation (as it is in non-PFP languages) and have the type-system restrict its actions rather than be modeled as a function (as it is in Haskell).
My definition of pure language is "The language can encode a large class of non-side-effecting computations, and you can tell that they are non-side-effecting from their type". If you are in a non-pure language you cannot distinguish side-effecting from non-side-effecning computations just by looking at their type and thus, working with my definition, it seems impossible to have type-labelled effects in a non-pure language.
The problem is that there are two things here: purity, and functional.
I don't know how to define purity alone, but let's say I take your definition, namely, the language defines a set of operations (that must include IO, and may or may not include mutation) and call them side-effects, and that the compiler enforces the transitive closure of said effects used by a subroutine.
But Haskell employs a very specific design to achieve that, which is pure-functional, i.e., the subroutines in the language must be mathematical functions (or, in Haskell's case, partial functions), and mutation is declared to be a side-effect (which can be said to be a corollary of the first design choice). Neither is required by your definition of purity. A subroutine may be a continuation -- i.e. be able to block and resume -- and still be required to declare its side-effects, and mutation need not at all be considered a side-effect (see, e.g., how synchronous languages are still "referentially transparent" while allowing mutation).
So you like PP (pure programming) but not PFP (pure functional programming)? Fine. For me the functional part is a means but the purity part is an end. If you can explain to me how to achieve purity in a non functional setting I will be extremely happy.
Could you perhaps give me a toy example of a pure imperative language and a type system where effects are handled through continuations? I really don't see how that is possible.
I don't know that I like pure programming; I'm just saying that if that's what people like in PFP, you can get that same sense of purity without PFP. Personally, though, I believe that people who like PFP simply find the paradigm to fit well with how they like to think of programs.
> I believe that people who like PFP simply find the paradigm to fit well with how they like to think of programs.
I don't think so actually, which I why I'm so keen to get to the bottom of your idea.
In my experience people find IO in Haskell very unnatural initially. It certainly took me months to get it. Now that I understand it it causes no additional overhead but I think it would have preferred to avoid it. I would still prefer to program in a more "natural" style and still get the benefits of purity. I just don't believe it's possible.
There seem to be two options
1. Work out how to explain IO more straightforwardly so it consumes less effort to understand it
2. Work out how to get the benefits of pure functional programming in a more natural, imperative setting.
> Haskell's design, which is built around extensional/value semantics, which basically approximates a program/subroutine as the function it computes; this is a useful approximation as it lets us treat computations as if they were referentially transparent. That approximation has a cost, though, as computations are not quite functions (they are processes that compute functions)
This sounds "not even wrong" to me. Care to explain in more detail?
> If, OTOH, your language has continuations ... monads don't really help.
What exactly is unclear? In Haskell, a subroutine or a subprogram is modeled as a mathematical function. Programs are not exactly functions[1] but continuations (i.e., they are a process which can be paused and resumed). There's absolutely nothing wrong with abstracting computations as functions (and there's much to be gained, which is why programmers are encouraged to write pure functions where possible, regardless of the language they're using), but sometimes you need to use their continuation-quality, and that's when you need monads.
> Haskell has continuations. Your move.
No, it doesn't. It models continuations as monads which is precisely my point. If Haskell subroutines were continuations, it wouldn't need monads, just as OCaml doesn't.
[1]: If they were, then computations would be extensionally equivalent like functions, but they're not: an observer can tell if you're running bubble sort or merge sort, even though they're computing the same function (which is why type theory introduces has the concept of definitional equality). A lambda term or a Turing machine are not functions but continuations; you can start reducing a lambda term (or running a TM), block it (i.e. capture the reduced term/TM state), and then resume it.
I think you must be using a very non-standard definition of continuation. What exactly is your definition of continuation? In what way exactly does OCaml "have" them that Haskell doesn't?
You are right in that I am using the word continuation to highlight an important property of computational models (be they lambda calculus, Turing machines, or FSMs), namely the ability to stop and resume a computation. In imperative languages, subroutines are continuations in the sense that they can block and then be resumed (although only some languages have reified continuations that allow direct manipulation of the suspended continuation, as opposed to supporting just scheduling by the OS, although theoretically scheduling by the OS is enough to capture the expressive strength of continuations, albeit that it suffers from various performance problems). In Haskell, subroutines are functions and have no notion of "blocking". Because that notion is essential to computations, it is emulated in Haskell by composing functions together with monads rather than being supported "natively".
But Haskell pure functions do "block" when they request memory from the allocator, for example. How do you distinguish these two kinds of blocking so you can claim that OCaml has it and Haskell doesn't.
In the sense that this blocking can be fully or partially reified. I don't know much about OCaml's implementation of their lwt library, so I'll give an example from Java: In Java you can create a thread running a single subroutine, have that block by calling `park`, and then hold on to that blocked function in a form of a reified object (the thread), which you can resume at any time. This is not possible in Haskell, you cannot hold onto blocked subroutines (i.e. continuations); instead, you hold onto a chain of functions connected via a monad (or monads).
> This is not possible in Haskell, you cannot hold onto blocked subroutines (i.e. continuations); instead, you hold onto a chain of functions connected via a monad (or monads).
I don't understand what this means, and I certainly don't understand the difference between the two things.
This means that Haskell doesn't have an object representing a function blocked in mid-operation. The difference is one of abstraction (obviously all languages can ultimately express all computations), which affects composition and mental overhead. These, of course, are empirical effects, and so their merits cannot be justified on any theoretical grounds (theoretically, monads and continuations are the same).
Most people writing Haskell code for real problems should never write their own instances of Monad. When you see someone implementing tons of instances of Monad, it's a sign of over-engineering and Haskell masturbation. Very, very few people are good enough to judiciously understand when and where to write their own instances.
In the majority of cases you should merely be using instances of Monad which solve extremely common problems, like Maybe, List, State, Reader, Writer, and sometimes "functions of a common argument" `((->) r)`. The function one is perhaps the trickiest to understand.
For these examples of Monad, it's really not much work to learn how to apply them. Even just reading LYAH gets you pretty far. You could almost just pretend it is a DSL for composing units of work in each of the classic problems that motivate them (e.g. passing stateful computing, doing non-deterministic computation on a list, etc.).
But I do agree this becomes a huge problem when you join a team and they've architected skyscrapers of customized Monads to do routine stuff. That's a very poor way to build systems in Haskell unless you can guarantee every person on the team is a Haskell expert (hint: you can't).
Your comment strikes me more as a downside of the oneupsmanship of the Haskell community than a problem with Haskell itself.
Understanding monads sufficiently to work in monadic production code requires two things:
- Learning the type signatures of all the primitives of the Functor/Applicative/Monad hierarchy.
- Seeing some common monadic types in action. Basically understand how to interpret the "context" of M[A] and the semantics of bind. So Option[A] is a potentially missing A and bind short circuits. Writer[W, A] is an A with some accumulated W and bind accumulates the W. IO[A] is a description of an IO action that, when run, will yield an A and bind will create a new IO action that will use the result to produce a new IO action.
I actually really like the language, but there are several key points which stop me from using it in any production systems:
First and foremost, the build system. Oh god, the build system. Cabal is wonderful in some respects, but I have often found myself in a situation where the answer is "delete the ~/.cabal and ~/.ghc-pkg". This should not happen.
Laziness. You get concise programs, but then the memory performance is unknown until you read it and/or learn to read GHC's IR. You know, I'd really enjoy being able to know roughly how much memory I need to buy up for the systems I'm deploying.
Experimental or non-standard extensions. Just no. I'm not going to rely on non-standard or experimental "stuff" that tie me to a single compiler. This is made all the worse in that they're all hard-bound to various GHC versions and you're expected to use them in production (I'm looking at you, web frameworks). We have standards, stick to them.
It is for these reasons that I can't recommend Haskell to people as anything more than an interesting language to learn. Until the above is fixed (many of which are cultural) I will not use Haskell in production.
How about OCaml? It's not as theoretically advanced as Haskell, but it gets work done, is easy to read, fast, easy to reason about the complexity of, has decent libraries, and is generally quite pleasant.
I would be interesting in learning more about the "easy to reason about the complexity" part of OCaml. The problem I'm having with Haskell isn't that it's too slow, but that it's so high-level and gets so aggressively optimized at compile-time, that the performance of the resulting binary has performance characteristics that are unpredictable and non-deterministic. Performance can regress between GHC releases, for instance. This just seems to be a problem of high-level programming languages in general, they're really slow until you start optimizing them at compile-time. And with each one of those optimizations, you get another layer of indirection with regards to how disconnected the speed of the code you wrote should be vs. how fast the final product actually is. Haskell has no business being as fast as it is already, but GHC gives it to us at the price of long compile times and very fuzzy performance properties that are very finicky and expert-friendly.
The problem I see here is just the unsolved computer science problem of how to reduce the complicated, drawn-out, and error-prone job of a C programmer into the succinct job of a functional programmer without fundamentally pretending modern computers work in a way they don't.
The complain list on Ocaml is almost always smaller than on Haskell. It might be easier to just fix Ocaml's issues and use it. If not permanently over Haskell, then temporarily until Haskell can get their more complex issues solved.
I think Haskell is optimized for _rewriting_ code. Personally, I think this is far more important than reading code!
Being able to read code is great (e.g. Python), but if you can't change things without fear the natural tendency of the codebase won't be in a good direction.
Haskell can actually take a while on the first write, but once you have the rails of the type system down it's sooooo easy to change things, even in a codebase you're new to or haven't touched in a while.
I think that's changing. A lot of the over-proliferation of operators came from missing functions (like Data.Function.(&)), missing typeclasses (Like with Data.Lens or other generic operators), or missing sugar (like Applicative Do or Idris' bang notation).There's definitely some cleanup work since the ecosystem's changed so much, though.
Haskell, like Perl, gives users more freedom to express themselves than other languages.
Unlike Perl, many (most?) Haskell features are oriented towards ease of statically understanding programs without executing them. Haskell culture is much about reasoning power around Haskell programs.
Reasoning about programs is the ability to read code in depth.
Very interesting and constructive thread. A lot of information about various Haskell warts, problems people have, and possible solutions to overcome some of them.
Some examples which from my point of view seem pretty clear cut:
Laziness and purity fall squarely under "language specification". There's no way of seeing them as a problem with libraries or community.
'I think a lot of the Haskell community is obsessed with mathematical "cuteness", which basically they take to mean "infix-operator-heavy" notation.' is a problem of community (and arguably language specification), certainly not of tooling or infrastructure.
"String should not be [Char]" is a problem with libraries (and arguably specification). Certainly not community or tooling.
I can tolerate it in Elixir/Erlang because GC is per-process-id (of which there could be dozens or hundreds in a typical Erlang/Elixir app) and not remotely a global world stopper.
When using certain styles in C or C++, memory management is really a tiny aspect you have to spend a tiny minority of your time thinking about -- but you gain so much determinism of execution in return.
I've been burnt by garbage collection killing my programs' performance many times -- having to tune the garbage collector has taken more time than just writing manual memory management in some cases!
Also, dynamic memory allocation and garbage collection just injects so much dynamism and uncertainty in your program's execution. I dislike it for the same reason I dislike dynamic typing. I want to be able to statically reason about the resource use of my program, just like I want to statically reason about any other aspect of my program.
Deterministic! My usual coding projects (distributed, networked, multi-process/multi-thread) are so far from determinism and my problems so many levels above the concerns of C/C++.
Both are needed. Just saying I'm glad other devs work on the lower level stuff for me ;)
Non-determinism comes in many forms. I don't think it's an all or nothing thing.
Eliminating non-determinism is always nice, even if lots of other non-determinism remains.
Having predictable and understandable resource use is a nice plus. Removing dynamic memory allocation also tends to increase locality, remove indirections, and remove error handling paths from your code.
Memory management isn't solved though. In a lot of applications, it takes up a significant amount of memory and/or cpu time, which can be of importance if you have time constraints, space constraints, and/or power constraints. Deterministic memory lets you, for example, know that the 256 bytes on your microcontroller is more than enough, or that you will always hit the timings needed to guarantee that your project will always output at 60 frames per second. Yes, there are a lot of times you can just throw RAM at the problem to make it go away, but there are a lot of really interesting problems out there that do require you to be able to intimately manage resources.
ATS claims to support memory safe functional programming without GC. I never used this language, though.
IIRC the idea is that allocating memory or writing something through a pointer returns a magic "cookie" value which certifies that the pointer now points to a block of certain size or data of certain type. This cookie can be bound to a variable like any other value and passed to other code together with the pointer so that the code can know that it's safe to access the pointer.
There are rules - cookies can't be pulled out of thin air or cloned, have to be destroyed during deallocation, etc. All of this is checked during compilation and removed from the final executable.
Having done a lot of C code without dynamic memory allocations at all -- chasing memory corruption was a thing, but a rare thing. I spent more time chasing performance issues and tuning GC in some projects than I did chasing memory corruption in my C projects.
I didn't say there were no corruption issues. Just that they're rare and a very small minority of the time is spent chasing them.
In return, get determinism and optimality of execution.
I think in ~10 years of C development, I had seen maybe 1 corruption bug slip through the testing suites to production. The vast majority are caught in unit tests, system tests, or QA.
I think it took 2-3 days of a 2-3 devs, but that's mostly because any production bug was very very expensive (Need to be very careful not to damage production machines).
But the search for the corruption was usually a couple of hours, perhaps a day's project.
If you utilize tools like valgrind, poison data that is released to pools, follow strict coding conventions, heavily test-cover your code, and become gdb-savvy, corruption bugs become not that expensive.
> If you utilize tools like valgrind, poison data that is released to pools, follow strict coding conventions, heavily test-cover your code, and become gdb-savvy, corruption bugs become not that expensive.
Watch Herb Sutter's CppCon 2015 presentation.
Apparently among the top of C++ developers that managed a ticket to get there, only 1% of the audience uses such tools.
"Haskell sucks because compilation takes too long."
"Aye. And it takes too much memory too."
"My major gripe with haskell was that I could never tell the space/time complexity of the my code without serious analysis (that among other things involves second-guessing the compiler's ability to optimize). This makes writing good quality code harder than it needs to be."
"Debugging boils down to wolf-fences and print-statements most of the time"
"Profiling is needed for any stack traces, and is not enabled by default, so you end up spending hours rebuilding your sandbox just to get a stack trace of an error."
"Coming from a Scala background, the tooling seems terribly cumbersome and immature. I couldn't get it to work in IntelliJ."
"You need to turn on dozens of language extensions to do pretty much anything."
"I'm a developer on a pretty large Haskell codebase, and we're in a corner of 'can't add any libraries without them conflicting with each other." Which is OK, by itself, but leads to my biggest complaint of 'why aren't API changes reflected by major/minor versioning'."
"Haskell sucks because both the standard library and the ecosystem lack guiding ideas."
For those who, like me, wondered what wolf-fencing is:
"Wolf fence" algorithm: Edward Gauss described this simple but very useful and now famous algorithm in a 1982 article for communications of the ACM as follows: "There's one wolf in Alaska; how do you find it? First build a fence down the middle of the state, wait for the wolf to howl, determine which side of the fence it is on. Repeat process on that side only, until you get to the point where you can see the wolf."[10] This is implemented e.g. in the Git version control system as the command git bisect, which uses the above algorithm to determine which commit introduced a particular bug.
Binary search sounds kind of 2D. Wolf fence also lets you work with unsorted data.
I.e. if I asked you to find a squeaky noise coming from somewhere in your house. Does it make sense to "binary search" your 3D 3-story house?
Wolf fencing makes more sense.. stand on floor 2, see if it is on your floor. Or is it coming from the stairwell to floor 3, or the starwell to floor 1? You just used 1 step to narrow your search space down by 1/3.
How would you sort your house so you can binary sort it?
Well, what you are doing is a "generalized" binary search. Suppose you have a four-stor(e)y house. You can start from the second floor and proceed a la standard binary search. You're putting an implicit ordering on the parts of your house, essentially, and then doing a search.
And the bit about cutting the search space to a third (which is what I believe you meant, instead of cutting it /by/ a third) is something you can also do when searching a list, but IIRC halving is preferred because it has better worst-case time-complexity. You can always split a list into 1/3 + 2/3 instead of 1/2 + 1/2, but you risk having to search the 2/3 every time, etc.
How can wolf fencing work on unsorted if you have to find out where it happens? Wouldnt that involve iterating on each item to find out where the wolf is?
You can search N objects in less than O(N) time if you have a way of testing multiple objects at once. Pre-sorting is one way to obtain this capability, but it's only one of many. Often the capability comes from the problem domain itself (or, more accurately, how you model time in your problem domain). In debugging, "one run of the program" often decently approximates a constant unit of time, and you can (theoretically) bisect once per run, giving you the ability to search without an explicit presort. In the wolf analogy, your ability to roughly determine which direction the wolf call is coming from does the same thing.
In both cases you could argue that you are "cheating" because there is a computational process of some kind doing at least O(N) work each time (the computer in the debugging example, the universe in the wolf example), giving O(NlnN)+ overall, strictly worse than an O(N) linear search. However, you have to ask yourself in this case if it makes sense to equate the time taken by the computer/universe to run one unit of its computation with the time taken to run one unit of your* search, and in many cases it doesn't. In the end it's just a model and it's up to you to choose what to measure, approximate, and exclude.
If someone tries to argue that there is fundamental philosophical truth that makes one model better than the other, start digging down into the physics of the CPU -- how much O(N^N) quantum computation did the universe have to do in order to bring you a single CPU cycle? God only knows, but the time it takes is nearly constant, so it makes sense to ignore. Even the most hard-core theoretical algorithms expert is just as "guilty" of this kind of approximation as you are :-)
Binary search is looking for a value. Wolf-fencing is looking for a side-effect of that value.
As per analogy, you're not checking an animal in Alaska to see if it is a wolf, you are waiting till the wolf howls (side effect of wolf-existence) then focusing your efforts in that area.
So you can't really use wolf-fencing to say... find a number in an array; it makes no sense.
It seems to be used only in the context of debugging. While with git bisect, we are also repeatedly ~halving the remaining search space, the list of commits isn't "sorted" like the precondition for binary search.
You could argue that commits are sorted against time. But I would say that's different because we don't know which commit hash we're looking for -- we just want to find the point in time when our code stopped working, not that I've ever had to use it before ;).
*> the list of commits isn't "sorted" like the precondition for binary search.
Good point.
One may think that in some sense, it is sorted: The history is sorted into "good" commits (before the bug was introduced), which are followed by "bad" commits (after the bug was introduced).
However, the bisection algorithm also works if "good" and "bad" commits are mixed. Even then it will find one commit where the bug was introduced (although there may be other places where it had been fixed and later reintroduced).
This works because bisection doesn't require a sorted list, it just needs a continuous function. And any sequence is just a special case of that.
> However, the bisection algorithm also works if "good" and "bad" commits are mixed. Even then it will find one commit where the bug was introduced (although there may be other places where it had been fixed and later reintroduced).
I don't understand this comment. Need for sorting the list is essentially the same in both cases, because "the bug was reintroduced" is essentially equivalent to "a lower number showed up later". A binary search makes the assumption of sorting because it's premised on the idea, "Whatever number I find, every number that comes after it will be larger". Same with bug-finding in a git history, it assumes "If I find a point in the history where the problematic behavior exists, it exists at all points after that for the same reason."
If you had an "unsorted" history where a bug was fixed and then reintroduced in another way, your binary search may fail to find the immediate cause of your problems in the same way that an "unsorted" binary search would fail to find what it's looking for.
> because "the bug was reintroduced" is essentially equivalent to "a lower number showed up later"
Yes, and this is perfectly okay for "git bisect".
> Need for sorting the list is essentially the same in both cases.
No, bisection does not require sorting, it just needs a continuous range whose endpoints have different signs. Then, it will always find one point where the signs on both sides differ. (In real analysis, this means finding a zero. In discrete maths, it means find the introduction of a sign change.)
In other words, when starting with a range from an old "good" commit to the current "bad" commit, it will always find a "good" commit directly followed by a "bad" commit.
This is why "bisection" is a good term for this Git algorithm, while "binary search" would be misleading. (Even though bisection and binary search are almost identical algorithms. They mostly differ in their input assumptions and output guarantees.)
Bisection makes no assumtion on any sort order, but guarantees to find one point of bug introduction in logarithmic time. It does not give any guarantee to find the lastest point of bug introduction -- unless, of course, your history does have just one such point.
What's with capturing large predators? My book on complex analysis euphemizes building a descending sequence of sets which converge to a point by
> If R is the continent of Africa and we select R_n as that subregion which contains the biggest lion in Africa, we have an algorithm for capturing a big lion—we simply build a cage around point z_0.
"My major gripe with haskell was that I could never tell the space/time complexity of the my code without serious analysis (that among other things involves second-guessing the compiler's ability to optimize). This makes writing good quality code harder than it needs to be."
This problem is significantly worse in Haskell than in most other languages. Usually you lose at most a constant factor in performance if the compiler doesn't optimize as much as you expected, but the lazy evaluation can actually change the asymptotic memory complexity. I have been bitten by lazy evaluation several times by writing code that I expect to run in constant space and then finding it require linear amount of memory because of the evaluation order.
> My major gripe with haskell was that I could never tell the space/time complexity of the my code without serious analysis
This is really a serious issue. The only other language where I ever had this issue was SQL. I had a project where we have lots of nested Views. We had to rephrase the SQL in sometimes very non-obvious ways, introducing intermediate Array results, CTEs, switching between sub SELECTs and JOINs -- all this just to convince the Query Planner to use a different Query Plan. That made a difference between < 1 second or a few minutes.
In the end, this is why I prefer languages OCaml or SML over Haskell. (Rust on the horizon)
Roughly speaking, in Haskell you can prove "just" correctness, while in OCaml you can prove correctness as well as performance.
"My major gripe with haskell was that I could never tell the space/time complexity of the my code without serious analysis (that among other things involves second-guessing the compiler's ability to optimize). This makes writing good quality code harder than it needs to be."
It seems to me that there is some connection/analogy with garbage collection. Garbage collection also makes reasoning about space usage of programs difficult, since it happens in some future, yet unspecified, time. Yet GC is almost always regarded as good (although historically there was a lot of fight against it), yet lazy evaluation is regarded as bad by many. Maybe the issue really is "good enough compilers"?
It's really easy to fuck up what will be evaluated, when, and wind up with disastrous performance ramifications.
As an example, I wanted to accumulate a list of summary data as I iterated through a recursive function. Through a coding error, the elements of the list (although not the list itself) were not evaluated until all the iterations were complete. This meant that, instead of a list of structs, which I thought I had, I had a list of zero-argument functions that, when called, would yield structs, _each of which contained a reference to a large array computed at an earlier iteration of the computation_.
So, I hemorrhaged space. This was a fuck-up, not a missed optimization.
I think this shows that the tooling is just as important, if not more important, when choosing a language. Good debugging tools are a must simply because things always go wrong, and if I don't have an IDE that can highlight the line for me and allow to mouse hover over variable values, then the whole experience becomes a lesson in frustration.
Additionally if you expect me to 'write, build, run, repeat' just to test my code, then you better hope you have a fast compiler else I'm going to start associating using your language with the same amount of interest as watching paint dry.
> if I don't have an IDE that can highlight the line for me and allow to mouse hover over variable values, then the whole experience becomes a lesson in frustration.
What does that even mean in a language with no explicit model for code sequencing?
Well Haskell has a well defined execution order. I think the large problem is that compiled Haskell doesn't represent the original source at all making this tricky if not impossible.
The key word being independent. There is still a natural baked in dependency ordering. That is all that I meant. I realize I could of phrased it better.
Mine is that it's built on a lie. A nobel lie, but a lie none the less. In order to be pure it has "stop the world and record it" with monads to do any IO. It pretends that the world can be snapped shotted and immutable. In reality the monad is only an extra box with a belief that the world stops. The world in fact does not.
It's that a monad looks pure, but actually isn't. Call addition with the same args and you should get the same answer. Calling next on a stream of io is destructive.
This is not true. The arguments for `next` include both:
* The target stream.
* A "state token" or universe.
You get different results with different state tokens.
At a high level, the thing to consider is that we can model all imperative programming with functional programming -- we can model Turing Machines with Lambda Calculus -- as well as go the other way around, because Turing Machines and the Lambda Calculus are, for all intents and purposes, equivalently expressive.
Modeling IO with state monads isn't a hack; it's the point.
Thank you for the explanation. Haskell's not as bad as I thought in this respect. My understanding was IO streams were a single input function (ie the stream). As a result the destructiveness would be less obvious.
Can you replay a get from stream by passing in an older token?
Because the token is passed only within the implementation of `>>=`, which is private to the IO module, you never have access to it. So no need for the runtime to keep snapshots lying around.
Monads provide a way to combine them and a way to inject values into them; but not all monads provide the inspection interface that would allow you to examine that state token. The definition of a monad guarantees only the injection and the combination (the "unit" and "multiply").
Haskell sucks because mutability and strictness are extremely important for writing large complex programs, and Haskell deliberately makes both of these things inconvenient.
Also, Haskell's ecosystem and tooling is still crappy, probably because writing complex programs in Haskell needlessly difficult (due to laziness and immutability).
And the "type-safety" guarantees are a red herring. They are not that useful in practice.
> Haskell sucks because mutability and strictness are extremely important for writing large complex programs, and Haskell deliberately makes both of these things inconvenient.
That's funny, because I'd say that immutability is essential for writing large on complex programs.
Strictness, on the other hand, hmm, I can take it or leave it.
> Haskell's ecosystem and tooling is still crappy
No argument here.
> because writing complex programs in Haskell needlessly difficult
Argument here :)
> And the "type-safety" guarantees are a red herring. They are not that useful in practice.
I've found the opposite. Type safety has saved me so many headaches I'm never going back.
Well, I can't say much other than that I encourage everyone to try Haskell for themselves and form their own opinion -- if they can find the time to get over the language's extremely steep learning curve.
That's I believe the main reason I find SML more attractive: it seems to use operators to a much, much lesser extent, apparently preferring alphanumeric function names in the core, and in 3rdparty libraries too.