The person you are responding to didn't say that, I did.
The abstractions I'm pointing at are cases where mutation or side effects are the desired result of execution. Ultimately this always runs up against having to grok a lot of different monads and that's simply never going to be as easy to understand as calling "print" or "break". Haskell works really well if the problems you're solving don't have a ton of weird edge cases, but often reality doesn't work like that.
The other thing is laziness which makes it hard to reason about performance. Note that I didn't say it's hard to reason about execution order--I think they did a good job of handling that.
Don't get me wrong, Haskell's dogmatic commitment to functional purity has led to the discovery of some powerful abstractions that have trickled out into other languages. That's extremely valuable work.
> The person you are responding to didn't say that, I did.
Ah, thanks, I got confused.
> Haskell works really well if the problems you're solving don't have a ton of weird edge cases, but often reality doesn't work like that.
In my experience it's completely the opposite, actually. I can only really write code that correctly handles a ton of weird edge cases in Haskell. It seems that many people think that Haskell is supposedly a language for "making easy code elegant". The benefit of Haskell is not elegance or style (although it can be elegant). The benefit is that it makes gnarly problems tractable! My experience trying to handle a ton of weird edge cases in Python is that it's really difficult, firstly because you can't model many edge cases properly at all because it doesn't have sum types and secondly because it doesn't have type checking. (As I understand it they have added both of these features since I last used Python, but I suspect they're not as ergonomic as in Haskell.)
> this always runs up against having to grok a lot of different monads and that's simply never going to be as easy to understand as calling "print" or "break"
Actually, I would say not really. The largest number of monads you "have to" learn is one, that is, the monad of the effect system you choose. Naturally, not every Haskell codebase uses an effect system, and those codebases can therefore be more complex in that regard, but that's not a problem with Haskell per se, it's an emergent property of how people use Haskell, and therefore doesn't say anything at all about whether Haskell is usable as a general purpose language. For example, consider the following Python code.
def main():
for i in range(1, 101):
if i > 4:
break
print(i)
You can write it in Bluefin[1], my Haskell effect system as follows.
main = runEff $ \ioe ->
withJump $ \break -> do
for_ [1..100] $ \i -> do
when (i > 4) $ do
jumpTo break
effIO ioe (print i)
Granted, that is noisier than the Python, despite being a direct translation. However, the noise is a roughly O(1) cost so in larger code samples it would be less noticeable. The benefit of Haskell here over Python is
1. You don't get weird semantics around mutating the loop variable, and it remaining in scope after loop exit
2. You can "break" through any number of nested loops, not just to the nearest enclosing loop (which is actually more useful when dealing with weird edge cases, not less)
3. You can see exactly what effects are possible in any part of the program (which again is actually more useful when dealing with weird edge cases, not less)
> Granted, that is noisier than the Python, despite being a direct translation.
My complaint isn't the noise. My complaint is: can you explain what withJump does? Like, not the intention of it, but what it actually does? This is a rhetorical question--I know what it does--but if you work through the exercise of explaining it as if to a beginner, I think you'll quickly see that this is isn't trivial.
> 1. You don't get weird semantics around mutating the loop variable, and it remaining in scope after loop exit
Is this an upside? It's certainly unintuitive, but I can't think of a case this has ever caused a problem for me in real code.
> 2. You can "break" through any number of nested loops, not just to the nearest enclosing loop (which is actually more useful when dealing with weird edge cases, not less)
Again, is this actually a problem? Any high school kid learning Python can figure out how to set a flag to exit a loop. It's not elegant or pretty, but does it actually cause any complexity? Is it actually hard to understand?
And lots of languages now have labeled breaks.
Arguably the Lua solution (gotos) is the cleaner solution here, but that's not popular. :)
> 3. You can see exactly what effects are possible in any part of the program (which again is actually more useful when dealing with weird edge cases, not less)
What does this even mean? In concrete terms, why do you think I can't see what effects are possible in Python, and what problems does that cause?
In all three of the cases that you mention, I can see a sort of aesthetic beauty to the Haskell solution, which I appreciate. But my clients don't look at my code, they look at the results of running my code.
The fact that you need a blog post to tell people how to resolve an issue exemplifies my point that this is not resolved. Nobody needs to be told how to turn off laziness in Python, because it's not turned on.
The fact is, Haskell does the wrong thing by default here, and even if you write your code to evaluate eagerly, you're going to end up interfacing with libraries where someone didn't do that. Laziness still gets advertised up front as being one of the awesome things about Haskell, and while experience Haskell developers are usually disillusioned with laziness, many Haskell developers well into the intermediate level still write lazy code because they were told early on that it's great, and haven't yet experienced enough pain with it to see the problems.
Haskell has a long history of a small base library with a lot of essential functionality being provided as third party libraries, including mtl and transformers (monad transformers), vector (an array library). Even time (a time library) and text (Unicode strings) are third party, by some definitions (they aren't part of the base library but they are shipped with the compiler).
Some people think that's fine, some people think it's annoying. I personally think it's great because it allows for a great deal of separate evolution.
Thanks for your detailed reply! As a reminder, my whole purpose in this thread is to try to understand your comment that Haskell is
> impractical to be used as a general-purpose language (because I don't think it's intended as a general-purpose language)
From my point of view Haskell is a general purpose language, and an excellent one (and in fact, the best one!). I'm not actually sure whether you're saying that
1. Haskell is a general purpose language, but it's impractical
2. Haskell is a general purpose language, but it's too impractical to be used as one (for some (large?) subset of programmers)
2. Haskell is not a general purpose language because it's too impractical
(I agree with 1, with the caveat I don't think it's significantly less practical than other general purpose languages, including Python. It's just impractical in different ways!)
That out of the way, I'll address your points.
> can you explain what withJump does? Like, not the intention of it, but what it actually does? This is a rhetorical question--I know what it does--but if you work through the exercise of explaining it as if to a beginner, I think you'll quickly see that this is isn't trivial.
Yes, I can explain what it does! `jumpTo break` throws an exception which returns execution to `withJump`, and the program continues from there. Do you think explaining this to a beginner is more difficult than explaining that `break` exits the loop and the program continues from there?
> It's certainly unintuitive, but I can't think of a case this has ever caused a problem for me in real code.
> Again, is this actually a problem? Any high school kid learning Python can figure out how to set a flag to exit a loop. It's not elegant or pretty, but does it actually cause any complexity? Is it actually hard to understand?
Yes, I would say that setting flags to exit loops causes additional complexity and difficulty in understanding.
> And lots of languages now have labeled breaks.
I'm finding this hard to reconcile with your comment above. Why do they have labelled breaks if it's good enough to set flags to exit loops?
> Arguably the Lua solution (gotos) is the cleaner solution here, but that's not popular. :)
Sure, if you like, but remember that my purpose is not to argue that Haskell is the best general purpose language (even though I think it is) only that it is a general purpose language. It has at least the general purpose features of other general purpose languages. That seems good enough for me.
> What does this even mean? In concrete terms, why do you think I can't see what effects are possible in Python, and what problems does that cause?
def foo(x):
bar(x + 1)
Does foo print anything to the terminal, wipe the database or launch the missiles? I don't know. I can't see what possible effects bar has.
foo1 :: e :> es => IOE e -> Int -> Eff es ()
foo1 ioe x = do
bar1 ioe x
foo2 :: e :> es => IOE e -> Int -> Eff es ()
foo2 ioe x = do
bar2 (x + 1)
I know that foo2 does not print anything to the terminal, wipe the database or launch the missiles! It doesn't give bar access to any effect handles, so it can't. foo1 might though! It does pass an I/O effect handle to bar1, so in principle it might do anything!
But again, although I think this makes Haskell a better language, that's just my personal opinion. I don't expect anyone else to agree, necessarily. But if someone else says Haskell is not general purpose I would like them to explain how it can not be, even though it has all these useful features.
> In all three of the cases that you mention, I can see a sort of aesthetic beauty to the Haskell solution, which I appreciate. But my clients don't look at my code, they look at the results of running my code.
Me too, and the results they see are better than if I wrote code in another language, because Haskell is the language that allows me to most clearly see what results will be produced by my code.
> The fact that you need a blog post to tell people how to resolve an issue exemplifies my point that this is not resolved. Nobody needs to be told how to turn off laziness in Python, because it's not turned on.
Hmm, do you use that line of reasoning for everything? For example, if there were a blogpost about namedtuple in Python[1] would you say "the fact that you need a blog post to tell people how to use namedtuple exemplifies that it is not a solved problem"? I really can't understand why explaining how to do something exemplifies that that thing is not solved. To my mind it's the exact opposite!
Indeed in Python laziness is not turned on, so instead if you want to be lazy you need blog posts to tell people how to turn it on! For example [2].
> The fact is, Haskell does the wrong thing by default here
I agree. My personal take is that data should be by default strict and functions should be by default lazy. I think that would have the best ergonomic properties. But there is no such language. Does that mean that every language is not general purpose?
> even if you write your code to evaluate eagerly, you're going to end up interfacing with libraries where someone didn't do that.
Ah, but that's the beauty of the solution. It doesn't matter whether others wrote "lazy code". If you define your data types correctly then your data types are free of space leaks. It doesn't matter what anyone else writes. Of course, other libraries may use laziness internally in a bad way. I've fixed my fair share of such issues, such as [3]. But other libraries can always be written in a bad way. In Python a badly written library may cause an exception and bring down your worker thread when you weren't expecting it, for example. That's a weakness of Python, but it doesn't mean it's not a general purpose language!
> Laziness still gets advertised up front as being one of the awesome things about Haskell
Hmm, maybe. "Pure and functional" is the main thing that people emphasizes as awesome. You yourself earlier brought up SPJ saying that the next Haskell will be strict, so we both know that Haskellers know that laziness is a double edged sword. I'm trying to point out that even though one edge of the sword of laziness points back at you it's not actually too hard to manage and having to manage it doesn't make Haskell not a general purpose language.
> and while experience Haskell developers are usually disillusioned with laziness, many Haskell developers well into the intermediate level still write lazy code because they were told early on that it's great, and haven't yet experienced enough pain with it to see the problems.
Hmm, maybe. I don't think people deliberately write lazy (or strict) code. They just write code. The code will typically happen to have a lot of laziness, because Haskell is lazy by default. I think that we agree that that laziness is not the best default, but we disagree about how difficult it is to work around that issue.
I would be interested to hear whether you have more specific ideas you can share about why Haskell is not a general purpose language, in light of my responses.
The abstractions I'm pointing at are cases where mutation or side effects are the desired result of execution. Ultimately this always runs up against having to grok a lot of different monads and that's simply never going to be as easy to understand as calling "print" or "break". Haskell works really well if the problems you're solving don't have a ton of weird edge cases, but often reality doesn't work like that.
The other thing is laziness which makes it hard to reason about performance. Note that I didn't say it's hard to reason about execution order--I think they did a good job of handling that.
Don't get me wrong, Haskell's dogmatic commitment to functional purity has led to the discovery of some powerful abstractions that have trickled out into other languages. That's extremely valuable work.