Why does Haskell, in your opinion, suck?
reddit.com
reddit.com
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.
https://hackage.haskell.org/package/base-4.8.2.0/docs/Prelud...
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.
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.
https://www.fpcomplete.com/hoogle
It searches on all the stackage set of packages, afaik (another explanation: http://stackoverflow.com/a/21955591/293735 )
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.With applicatives, the pattern of
f <$> a <*> b <*> c
is very common (with a type signature f :: a -> b -> c -> .... <*>
, is supposed to be `<$>` (based on the rest of your discussion). <*>
takes function(s) out of a mappable and applies it (them) to argument(s) within a mappable. (+) <$> [1,2,3] = [add 1, add 2, add 3]
(+) <$> [1,2,3] <*> [10, 20] = [(1 +), (2 +), (3 +)] <*> [10, 20] = [11, 21, 12, 22, 13, 23]
(+) <$> Just 3 <*> Just 5 = Just (3 + ) <*> Just 5 = Just 8
(+) <$> readLn <*> readLn = ...
well that depends on the inputs :)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.
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.
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.)
It might just be the Lisp version of Stockholm Syndrome, but it seems perfectly natural to me to gloss that as "Add one and two."
"Would you consider equal the sum of 1 and 2 to 3?"
Though now that I type that out, I'm acutely aware of how strange it sounds. Interesting.
It's also not true in the case of macros, e.g. let. So, you're right. No wonder it seems so foreign.
EQ is object sameness. Number objects of the same value are not necessarily the same object.
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“.
It's nice because it shows it's a value not a question.
But I don't really verbalize code, I think in symbols, maybe that's why prefix doesn't bother me.
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.
https://www.haskell.org/hoogle/ lets you search for functions by name, including symbol-named functions like <$> and co.
(You can also search for functions by type, which is a pretty cool feature, but not what you were asking for.)
I suppose the other extreme is Lisp, which I've played around with and found quite pleasant because of its very unambiguous syntax.
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.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.
I'm very curious about this. Why do you think you have to learn it all by heart before you start with the language?
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).
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.
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.
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/
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.
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.
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.
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.
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.
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 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.)
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.
That's a bit on the nose. [1]
[1] As in "ouch".
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.
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.
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.
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.
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.
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?
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.
https://ghc.haskell.org/trac/ghc/wiki/StrictPragma is coming in GHC 8.
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!
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.
[1]: https://github.com/Gabriel439/post-rfc/blob/master/sotu.md
foldl, foldr, foldl1, foldr1, foldl', foldr', foldl1', and foldr1'.
There are twelve if you count:
scanl, scanr, scanl1, scanr1.
Also, the implementation of monad transformers is disgusting. It requires O(n^2) code generation where n is the number of monads.
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.
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.
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.
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.
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 :)
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.
See http://blog.paralleluniverse.co/2015/08/07/scoped-continuati...
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.
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.
They are orthogonal in 1 technical sense. But the benefits are reaped from the combination.
> the PFP abstraction has so far failed to yield results commensurate with its cost.
You say this based on what? Me and other Haskell users believe that the costs are very minimal and the benefits are quite huge.
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.
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.
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?
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.
> Haskell gains power in its framework for restricting code
and you have nudged the thread in the direction of your pet topic
> it's people who want to convince others to use Haskell that should collect some evidence in its favor
How exactly did we end up here?
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.
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.
[1] http://www.cbc.ca/news/health/old-cholesterol-warnings-steep...
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.
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?
> (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.
[1]: http://link.springer.com/chapter/10.1007%2F978-3-319-11737-9...
[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.
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?
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).
[1]: http://ceu-lang.org/
And what exactly would be the point of those?
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.
What exactly is your definition?
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).
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 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.
I'd prefer to achieve 2, if possible.
This sounds "not even wrong" to me. Care to explain in more detail?
> If, OTOH, your language has continuations ... monads don't really help.
Haskell has continuations. Your move.
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 don't understand what this means, and I certainly don't understand the difference between the two things.
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.
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.
Does using Free count? ;)
- 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.
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.
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.
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.
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.
http://h2.jaguarpaw.co.uk/posts/complaining-about-languages/
I believe it would help everybody understand how we can improve people's experience around Haskell.
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'm doing a lot of Clojure, and can't really see how it would be a pleasant experience without GC but curious to hear any interesting thoughts.
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.
Besides even on Rust, using lock free algorithms without GC help is very tricky.
I never saw a C project with teams bigger than 5 developers without memory corruption issues.
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.
Got a very interesting week tracking down that issue, while getting regular customer calls when the problem would be sorted out.
I surely don't miss it.
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.
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.
I do think garbage collectors are overrated.
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.
Both are needed. Just saying I'm glad other devs work on the lower level stuff for me ;)
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.
"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."
"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.
[https://en.wikipedia.org/wiki/Debugging]
So now you know what that thing you always end up doing is called :)
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 ;).
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.
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.
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.
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?
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.
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.
So apply a binary search to an effect instead of a value.
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 :-)
> 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.
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.
What does that even mean in a language with no explicit model for code sequencing?
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.
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.
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"?
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.
You can perceive yourself moment ago exactly because of that.
Others who have not seen you recently hold the old reference to you.
I would rather say that the world is naturally immutable.
next :: Stream a -> IO a
The function part is pure. The `execution` of the resulting `IO a` action (which is completely separate from its pure evaluation) is not.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.
Can you replay a get from stream by passing in an older token?
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").
That's not how IO in Haskell works, you're basically describing what Haskell would be if it literally had no side-effects ever.
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.
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.