When it works, it's magical. Finally realising that monads allow little bubbles of cause-and-effect. STM, which is utterly magical. Realising that not having side effects is freeing, not restricting. Collapsing huge chunks of code into little bits of pattern matching. Finding myself casually using a higher-order type because it was the easiest way to solve a problem.
When it's warty, it's really warty. Namespaces... oh god, namespaces. About three different rather poor ways to handle exceptions. A billion little unmemorable functions with symbolic names. Total lack of debugging tools. Trying to find where an a out-of-bounds list error was happening. Trying to debug a memory leak caused by insufficient strictness --- why do I need to care about this? The runtime should just take care of it for me; the whole point of a lazy language is that I shouldn't have to care when evaluation happens.
Some of the warts are just embarrassing, and need fixing ASAP. Lack of information on a runtime exception is the biggest. Even BASIC would at least give me a line number!
> Trying to debug a memory leak caused by insufficient strictness --- why do I need to care about this? The runtime should just take care of it for me; the whole point of a lazy language is that I shouldn't have to care when evaluation happens.
This is a completely wrong assumption. The point of laziness is not so you won't have to care about evaluation order. Everyone would like to have a language in which you don't have to think about evaluation order, but that has proven to be rather hard. Not because the people who invented Haskell or implemented the compilers are idiots who don't know what they're doing, but because it a hard problem to tackle. With many pros and cons on both the lazy and the strict side.
This isn't embarrassing at all. At least you need to think less about evaluation order in Haskell than in most other languages.
I think lazy evaluation is very hard, especially when you are dealing with a codebase of of say 50 files with structures containing other structures, the line between lazy and strict is very hard to see.
The difficulty learning lazy evaluation skill goes from
* 0 where you know nothing to 5 where you sort of understand.
* 5 - 90 is a space where you think you know what it is, but you really don't. You'll probably get yourself in trouble more than helping.
* 90-100 is when you grok it and can use it effectively.
I am an avid haskell user for a few years and I am still 50/50 on most of my conclusions about laziness without asking someone else.
I think it can be unexpected if you're used to other semantics, but the behavior of laziness is very well defined.
> let x = [1 .. 100] :: [Int]
> seq x () -- force the first cons
> :sprint x
x = 1 : _ -- we evaluated the first cons
> foldr (\e l -> e + l) 0 x
5050 -- printing this value forces foldr to complete
> :sprint x
x = [1,2,3,4,5...100] -- all conses evaluated
> A billion little unmemorable functions with symbolic names
Pretty much. If you though Common Lisp was bad, this is much worse. And that makes for a major (re-)learning curve every time you stop programming in Haskell for a couple of months.
I actually like the symbolic function names. They're a pattern and method to them that once you learn it, it's super convenient. Perhaps it just needs more obvious documentation.
> Trying to find where an a out-of-bounds list error was happening
I agree that in some real world runtime debugging, haskell falls short, due to the strictness that it imposes and how observation through debugging can change how the code executes. I have also spent a week or more tracking down a memory bug that was solved by a single `!` (to add strictness).
But the glory of haskell, is that you really don't need to debug runtime code. If you take advantage of the types
Having used haskell for about 5 years now, I think anywhere that you can even have an out-of-bounds list error is an anti-pattern. You should restructure your data to match the type.
Using list lookups aren't what lists are for. Unless you are implementing a specific algorithm, manipulation and access of lists should only be done via folds, maps, pattern matching or similar functions.
If you are accessing members of a list directly by their index, you want a different data structure
Personally, I've been spending a lot of my recent time trying to debug a Parsec parser (it seems to be going into loops of backtracking, but I can't figure out where and why), and it's been a clusterfuck.
* `trace` and `traceM` don't actually get evaluated, so I can't use printing to find my bugs.
* Even putting `trace "..." True` as a guard to my parser combinators doesn't seem to actually print anything.
* `seq` and `deepseq` require that I implement all kinds of instances for datatypes whose innards I can't actually access, so I can't force the evaluation.
* I'm getting tempted to use `unsafePerformIO` just to get some damn debug output, but can't figure out how to make it have the right type inside the Parsec monad.
Debugging Haskell sucks when you need to debug dynamic behavior rather than type errors.
> Even BASIC would at least give me a line number!
You can get line numbers and even stack traces if you compile your program (assuming you use GHC) with `-prof` and execute it with `+RTS -xc`. But most libraries try their best to avoid using exceptions altogether and it's generally discouraged to use the non-total functions from Prelude like `head` or `!!` and use safer alternatives instead.
I do remember trying to use -prof, and being unable to make it work --- but it's too long ago to remember the details. I think it was something about being unable to find a prof version of one of the libraries I was linking to?
I did also try the ghci interactive debugger, and couldn't make that work either. Now, I like command line debuggers, but I found the ghci one utterly opaque. I did eventually get it to catch the exception... but then it wouldn't tell me where it happened.
In general, trying to track down the exception was an utterly miserable experience. I do understand that Haskell has special needs when it comes to order of evaluation, due to the way that thunks are unwound, which means that traditional stepping and stack frames are of little use; but this desperately needs to be better. And work out of the box.
Regarding avoiding exceptions... yeah, I'd love to, but it wasn't my code which was throwing the exception.
I loathe and detest user-mode packaging systems, so I actually installed my packages via Debian and then built my app using ghc --make. Which, BTW, works beautifully. So I never had to touch hackage or cabal. Luckily.
(1) Haskell is a compiled language, so you need to compile everything locally
(2) Libraries are unstable, because they are young and authors are too creative, so dependencies are unstable.
Maven, and whatever Python and Ruby have, can ship you everything you need to run. (Except for things that depend on C bindings, where they become just as painful as Haskell libs, for the same reasons)
Totally agree on the runtime exception issue. I feel like even Java with it's typed exceptions are better. With a Haskell function I have no idea what exceptions it will through and worse, if it's in IO, I have no idea what async exceptions it could throw.
Haskell has all the features of languages that become mainstream and mandated top-down on workplaces. I wouldn't be much surprised if in a decade people start complaining about Haskell-shops they way we complain about Java-shops now. (But then, no language could get there without a marketing budget...)
Haskell lets big teams collaborate very well, makes it hard to bad coders to destroy an entire project, consist in a good enough filter so that HR won't hire obscenely bad developers, gives enough space for software architects do their thing, and gives plenty of objectively measurable points to make managers happy.
That said, it's completely immature. Haskell lacks packaging, environment tools, and even a complete standard library (hell, I had to put a package[1] at Hackage because no available TLS library would work!). It has all kinds of interesting advanced features, but the basics are simply missing.
Personally I switched from learning Haskell to learning Racket and couldn't be happier. On the other side of banging my head I think Cabal is the biggest disappointment of my experience and it was the worst package management headaches for my own private builds.
Now that I am done with a book of Haskell and being a total hack at Haskell with very little skills (I did learn quite a bit of Lambda Calculus along the way) I think there is a BIG reason why Haskell has not grown in popularity in language usage and is beat by Scheme and almost beat out by ML. The experience pales to the praise and mind share in the vocal community of Haskell users.
Your comparison isn't sound. LISPs and MLs are as close together as Apples and Oranges on the functional family tree.
Cabal has its problems and it primarily stems from a lack of any good "this is how you should use it" guides. Cabal is not a package manager!
I cannot force you to like something but I will say this in the hope that you re-evaluate your judgement: if you still think Racket is a much better experience than Haskell and its ecosystem, you likely haven't experienced Haskell in-full. I know both languages and I've used both in production software (including many other functional languages) and I can say without any doubt in my mind that I will never select Racket over Haskell for writing software, if presented the choice.
Are there glaring warts in the language? The ecosystem? The tooling? Sure, nothing is perfect and one can easily point out many more warts in other technologies that are far more popular.
RPL has been the one killer feature for my choice of Racket. I wanted to learn Functional programming and do a few scripts. Just after reading a book and a half and trying to get a few things done I really liked Racket when I went through a MOG on Programming Languages on Coursera. It went from ML to Racket and it just clicked for me.
> I think there is a BIG reason why Haskell has not grown in popularity in language usage and is beat by Scheme and almost beat out by ML.
Is it? I would think there is easily WAY more Haskell currently in use in both industry and academia than Scheme. Certainly the mindshare is much, much larger. I have no idea where ML is, but it seems like it should be roughly on the same order as Haskell, and surely beating Scheme.
The Haskell Platform obscuring Cabal's existence bit me. I didn't know how to use Cabal, so for a while I didn't install any packages, or know the correct way to build them.
I can relate to that because it took me a while to using cabal repl actively as i code. Once i discovered cabal repl, I found that i could test my database (yesod) business logic during my normal development cycle. This was very re-assuring to me, coming from an oo, java background. Writing tests is nice, although having a small function to test a particular usecase while development is quite empowering. The only caveat, being, I seem to have to export my tests as part of the main module. There are ways to restructure it, say creating DBObject (export crud/management) DBObject.Internal (will all methods exported).
This is one place i miss the selective export of features that Eiffel provides. I am still not certain as to why other languages (except perhaps sather) completely ignored this seemingly simple syntactic feature that would have saved so many of us from creating parallel hierarchies as in java or artificial structures in haskell.
That's intentional and good. Bleeding-edge packages are a headache to manage, due to complex dependencies. You should really wring everything you can out of Platform, and feel like an advanced Haskell programmer, before dealing with Cabal.
What I'd really appreciate (as one of those people who's read LYAH and is struggling to get going with real code) is a Haskell equivalent of Effective Java or Effective C++.
Not sure if it's relevant, and unfortunately it's a bit outdated, but have you seen Real World Haskell? Also related is Beginning Haskell – A Project Based Approach.
Agreed about the SEO of the documentation... I'm learning Haskell, and just last week I was trying to parse binary files using Data.Binary.Get. I spent a fair amount of time looking for a way to handle parse errors outside an IO monad. It turns out that the function I needed exists (runGetOrFail), but I couldn't find it because the first Google hit for "Data.Binary.Get" was for an old version of the package.
I don't know how one would tell Google to prefer the latest docs, but it would certainly help if the Hackage documentation page header warned you if you were not looking at the latest version of the docs.
> But at the end of the day I’m still a Developer in an Academic world. And more and more Haskell is attracting the kinds of developers who don’t know Lambda Calculus or Type Theory.
Haskell's ability to become a major language depends on its ability to attract, and keep, that kind of developer. There aren't enough developers who will learn all the theory in order to learn a programming language for Haskell to become one of the dominant languages.
There is... certainly a learning curve. The Haskell community is still tiny. Per capita, it's one of the best language communities out there. It just hasn't had the thousands or millions of person-hours necessary to make everything pretty and intuitive.
I generally say that, all else equal, and if I were to use a GC'd language-- obviously, there are projects that mandate C or other low-level languages-- then I would use: Python for a short-term (days) project, Clojure for a medium-term (< 3 months) one, and Haskell for a long-term (> 3 months, or multi-developer) one. You don't find yourself needing static typing if you're doing a small, self-contained, line-of-business task like a script. You start to really want it for multi-developer programs and for infrastructural jobs that'll take months to complete (and, at age 32-- that's ancient in programming years-- the only programming challenges that really interest me are the hard ones where Haskell's type discipline pays off in a major way).
Haskell will probably never beat Python in terms of ease-of-use, and (while I would prefer Haskell over other offerings for hard-core machine learning infrastructure) it has a ways to go before it can take over the exploratory data science world (we need data frames, and it's not at all clear what the "right" way to impose type discipline on such beasts is).
All of that said, Haskell is way better than it used to be in terms of user experience. It's gone from god-awful (circa mid-2000s) to hard-but-acceptable, and in the context of a longer-term project (i.e. at least 2 months) the uptake is a rounding error and the pain is paid off.
Furthermore, I don't think that Haskell's opt-out approach to laziness is more than a cosmetic issue. You can have as much strictness as you want; you just have to ask for it. However, high-performance programming in other languages (e.g. Clojure, Python) is also deeply technical and warty. When you start adding type hints and using Java arrays in your machine learning code, it doesn't look like Clojure anymore. My point in all this is that HPC is technical and rough in all of the major languages, and Haskell's laziness is just a small part of that.
"You don't find yourself needing static typing if you're doing a small, self-contained, line-of-business task like a script. You start to really want it for multi-developer programs and for infrastructural jobs that'll take months to complete (and, at age 32-- that's ancient in programming years-- the only programming challenges that really interest me are the hard ones where Haskell's type discipline pays off in a major way)."
IME, this seems to be roughly, "What are the odds you'll need to refactor significantly because the world changed around you?"
When it works, it's magical. Finally realising that monads allow little bubbles of cause-and-effect. STM, which is utterly magical. Realising that not having side effects is freeing, not restricting. Collapsing huge chunks of code into little bits of pattern matching. Finding myself casually using a higher-order type because it was the easiest way to solve a problem.
When it's warty, it's really warty. Namespaces... oh god, namespaces. About three different rather poor ways to handle exceptions. A billion little unmemorable functions with symbolic names. Total lack of debugging tools. Trying to find where an a out-of-bounds list error was happening. Trying to debug a memory leak caused by insufficient strictness --- why do I need to care about this? The runtime should just take care of it for me; the whole point of a lazy language is that I shouldn't have to care when evaluation happens.
Some of the warts are just embarrassing, and need fixing ASAP. Lack of information on a runtime exception is the biggest. Even BASIC would at least give me a line number!