Why Lisp is a Big Hack and Haskell is Doomed to Succeed (2011)
axisofeval.blogspot.ca
axisofeval.blogspot.ca
If the Haskell environment was more like a virtual machine - like in Java - where you could connect into a side-channel and see what types of data were persisting in memory as the program ran - you'd at least have a chance of debugging this sort of thing. But instead it compiles to machine binaries.
There doesn't seem to be any interest in the Haskell community in making tools to deal with this sort of thing - they say "you should learn not to make resource-leaking code". Which is the same thing the Lisp hackers say - "just learn not to make type errors".
-- No, no, no, no!
checkValid :: Int -> ()
checkValid x = if x >= 5
then error "Invalid number!"
else ()
-- This works fine.
checkValid :: Int -> Either String ()
checkValid x = if x >= 5
then fail "Invalid number!"
else return ()
This may sound a bit harsh, but I think you deserved to lose that 10% if you had such a fundamental misunderstanding of "lazy evaluation".Unfortunately, grades now also serve as some poor measure of intelligence or competency in many people's minds.
If the project was the entire grade, then yes that's a bit much. But if we're talking about 1 of 10 assignments, then 10% really isn't that much.
I've had plenty of assignments were the granularity was just something like excellent, good, pass, fail.
This project has been the most learning-filled project of my college career. I wouldn't trade that for any easy A. :)
I think Haskell's ultimate role is a bit like Ruby's: It can't really win, but it's destined to be influential. That's a much harder road for Lisp, as its greatest strengths are also its greatest flaws.
LINQ is "practical Haskell," so is most of Rust (if "practical" means approachable to imperative programmers.)
Besides being on the JVM (which is a big plus), Scala hardly makes anything more practical than it is in Haskell. In fact, Scala programmers tend to migrate to Haskell (and ML languages) rather than the other way around.
Haskell is indeed influencing, but it is not remaining stagnant.
Clojure is probably the one you're thinking of that doesn't support tail-call elimination at all. Rich Hickey thought that if you couldn't do it cleanly in all cases (like mutual recursion), you may as well just come up with something else. So instead of optimizing recursive calls, Clojure has the recur function.
I asked him about it on twitter since it reminded me of thunks, he said " a trampoline executes a sequence of thunks, yeah" [1]
[0]: https://www.youtube.com/watch?v=hzf3hTUKk8U [1]: https://twitter.com/runarorama/status/449070763421618176
Yes. Yes it does. Scala is a bodge that is destined to be replaced (or at least rewritten), and I say that as a huge fan of the language. I would happily bet on Haskell outlasting Scala in the long run.
(But I use Scala today, because in the long run we're all dead. Scala inherits a lot of useful production infrastructure from the JVM, and on recent progress it looks like Scala can get faster compiles quicker than Haskell can get better infrastructure. Which means that today, in many environments, Scala is the better choice)
I'm not sure that I would agree that you're flying "blind or dangerously" either way--at least not when it comes to building up large unevaluated thunks. I would agree somewhat if you were referring to code using unsafePerformIO, which makes it the programmer's responsibility not to break referential transparency. While that doesn't tend to be an issue either, Safe Haskell does mostly solve it.
Even the company that was on HN recently cites thunk leaks as their biggest problem with Haskell: http://engineering.imvu.com/2014/03/24/what-its-like-to-use-...
I've never seen a Java application that hasn't succumbed to an unexpected NullPointerException at least once over its entire development cycle. That doesn't mean that the language isn't a reasonable choice. It's just a common problem that you have to accept on that platform.
Space leaks in Haskell are similar. You're going to run into them every so often, and you'd really rather not, but they're easy enough to deal with.
Anyway, in Java 8 they've started taking steps to address the null problem with the new Optional type, and, FWIW, in Scala nulls are a more or less a non-issue when you use the FP side of the language.
When you apply a function f to a, that is not actually evaluated. Rather, a "thunk" is created that will evaluate `f a` only when that value is actually needed.
If, in your program, you never need anything, or don't "force" your function calls and data structures ("pretend" to need something) in intermediary stages, then the thunks may take up a non-trivial amount of memory. This is not a memory leak in the traditional sense, just temporarily increased memory usage.
It is very easy to (pre-emptively) handle most space leaks in Haskell, but you do need to know how they arise.
There are two very simply rules you can follow that take care of the vast majority of space leaks:
1. Make data fields strict unless you actually want them to be lazy, i.e. instead of:
data Foo = Foo
{ bar :: String
, baz :: Int
}
write data Foo = Foo
{ bar :: !String
, baz :: !Int
}
2. When you write recursive functions that depend on values which are not forced (e.g. pattern matched against) in each function call, use either `seq`/$! or bangpatterns to make sure the value is evaluated (to HNF) rather than building up excessive thunks. For example, instead of: acceptLoop :: Socket -> Int -> IO ()
acceptLoop sock connNum = do
econn <- accept sock
_ <- case econn of
Left err -> printf "Error accepting connection %d: %s" connNum err
Right conn -> forkIO $ runConn conn
acceptLoop sock (connNum+1)
write either acceptLoop :: Socket -> Int -> IO ()
acceptLoop sock !connNum = do
econn <- accept sock
_ <- case econn of
Left err -> printf "Error accepting connection %d: %s" connNum err
Right conn -> forkIO $ runConn conn
acceptLoop sock (connNum+1)
or acceptLoop :: Socket -> Int -> IO ()
acceptLoop sock connNum = do
econn <- accept sock
_ <- case econn of
Left err -> printf "Error accepting connection %d: %s" connNum err
Right conn -> forkIO $ runConn conn
acceptLoop sock $! connNum+1
to make sure that connNum is always just a single value rather than a series of unevaluated thunks. That way you won't get a space leak if you rarely have problems accepting new connections.Now, that begs the question, why lazy by default and not opt-in lazy?
From the outside looking in it seems that deep expertise is required in order to launch a Haskell production app with any degree of confidence (i.e. to quickly dig yourself out of runtime issues like space leaks where the means to avoid them may be known, but the means to resolve them when they occur, non-trivial).
Very good question. Actually, I think most haskellers agree that laziness complicates things more often than not, and if we could start over we wouldn't make Haskell lazy by default. (Although that's not to say there won't be even simpler ways to "strictify" things in the future. Also, many libraries already provide functions that are strict in their arguments by default.)
However, there is also agreement that Haskell's laziness is the reason the language got purity right: there was simply no other way, since laziness meant evaluation order was unclear.
> From the outside looking in it seems that deep expertise is required in order to launch a Haskell production app with any degree of confidence (i.e. to quickly dig yourself out of runtime issues like space leaks where the means to avoid them may be known, but the means to resolve them when they occur, non-trivial).
As someone who writes Haskell for a living, I really just follow a few rules like this without thinking too much about laziness, and I tend to not have any problems. I have had maybe one nasty space leak in the past five years.
Yes, sometimes they do come up, but it takes ~5-10 minutes to pinpoint the problem spot with the heap profiler. It is not nearly as messy as using Valgrind to find actual memory leaks.
Following Haskell's evolution somewhat from the outside, this is surprising. (And also somewhat disappointing, as laziness always seemed an important part of Haskell's elegance.) Is laziness now considered something of a failed experiment?
It's part of it, but to a lesser extent than you would expect. The much more important part of Haskell is its no-corners-cut separation of effects, which happened chiefly because, without it, laziness meant that there was no way to know when fireTheMissiles() would actually happen.
> Is laziness now considered something of a failed experiment?
To some extent, yes. No one is saying laziness doesn't make the implementation of some algorithms and data structures extremely elegant, just that, most of the time, you don't actually gain much by leaving your function calls and data types lazy.
I enjoy being able to make infinite/self-referencing data structures, or leaving fields lazy and "performing" all of the function calls in the initialization of the "struct", but have only the functions producing results that will actually be needed matter performance-wise. However, if you don't use any strictness annotation, that benefit doesn't outweigh the problems that can be caused by space leaks.
If you do use strictness annotation, I don't think it matters too much whether the language is lazy or strict, you just have to write strictness annotation instead of laziness annotation.
While not universal, I feel confident stating that the opinion is shared by the majority of long-time Haskell programmers.
So the question becomes whether strict-by-default or lazy-by-default is better given that both kinds of evaluation have their place. I personally have come to believe that lazy-by-default is nicer since there are fewer places where I really demand strictness... but it also basically forces your language to be pure.
The other advantage of laziness is that it is a big aid to composition, modularity and concision. It allows you to perform common sub-expression elimination which can be a major boon for code readability. See this article for some examples:
http://augustss.blogspot.ca/2011/05/more-points-for-lazy-eva...
data Foo = Foo
{ bar :: !String
, baz :: !Int
}
This is actually a great illustration of why "making data fields strict" is a lot more tricky than it sometimes looks. All that> bar :: !String
is going to get you is the first cons cell in weak head normal form.
If my string is "Hello", then in unevaluated form I have
<THUNK>
Using the strict data field gets you weak head normal form, which is
<THUNK>:<THUNK>
Probably not what you had in mind. As far as Haskell is concerned those two thunks might be
undefined:undefined
Using a strict Text will get you where you want to go, because when you reduce that to weak head normal form you will get a fully formed Text.
Watch out with lists, which is all that a String is.
I find myself using deepseq the most when I want to be sure a data structure (and any exceptions any pending operations might throw) has been fully evaluated before passing it off to another thread, not to prevent space leaks.
"I'd take a NullPointerException over a memory (edit: space) leak any day of the week; the former is instantly resolved, the latter, god knows."
If someone really feels that your remark should be downvoted, I hope they post an explanation about why.
> I'd take a NullPointerException over a memory (edit: space) leak any day of the week.
That's your preference and I can understand that preference from your point of view because in order to debug a space leak in a Haskell program you would first have to learn Haskell.
I'm not the one who downvoted, but I also didn't think it contributed much =(
Why? NPEs are easily solved, even for beginners. Stack trace says blah blah blah occurred at line X in class Y. Easy fix, totally low hanging fruit for beginners and experts alike.
The space leak issue, OTH, may be easily avoided (if one is well versed in Haskell best practices), but resolving them when they occur is something else entirely. Seriously, you have to break out a memory profiler in order to find out where the issue _may be_, not exactly where it is on line X of class Y.
So, yes, I never get them (in Scala), but I'll stand by taking an NPE over a space leak any day of the week.
For instance, what if your null pointer in language x causes a silent fail? Now your apples are beginning to look like oranges.
Haskell has a self correcting compiler.
You do not need IO limitations for logging. You can use Debug.Trace
Haskell does have a heap analyzer. Like C, you can choose to compiler an instrumented binary with debug symbols.
There are a huge suite of tools coming around this year. Debugging and analysis has been a huge theme recently. Simon Marlow's recent book gives a taste, as does the latest Communities And Activities Report.
Actually there's a great deal of interest in dealing with lazy evaluation and making it more explicit so that you're less likely to leak resources.
<sarcasm> We should just stop writing the bugs. If, instead, we focused in writing code that runs correctly, everything would be much better. </sarcasm>
Errors are unavoidable, no matter what language you use. The important thing is that you can catch them in testing. I don't value static typing very highly because type errors show up quickly. Pass the wrong type, and execution fails. Ideally this happens in your unit tests. During development. Because you're doing TDD.
Resource leaks, on the other hand, are usually very hard to catch, and often don't show up until production.
As I slowly learned the language the proper way, I am now able to do anything I did in a imperative/OOP/whatever language, but now I have that foundation of type safety. Like many will say, if it compiles, it most likely just works. Coupling this with automatic checking via GHC-mod and I am just as performant (in terms of writing code) and encounter half as many bugs as any other language I've ever used. Haskell isn't a panacea, but it's a very good language.
I believe the more you learn, the more this becomes true as well. To begin with, you'll write a lot of code that has potential bugs (like missing cases when pattern matching, using types that you really shouldn't, using error when something like Maybe or Either would be more flexible etc.). eventually you realise how to write code that can avoid a lot of these problems.
Another doubt to "eating all languages lunches" comes from having multiple different paradigms in programming languages. I can imagine Haskell eating, say, Prolog's lunch - for example, Norvig has shown how Prolog could be implemented as embedded in Lisp. But I suspect it's going to be harder to repeat in Haskell the strong points of Forth (stack computations), Tcl (strings as universal media?) or J (composability of primitives), even though some approximations could be made.
It's fishy idea to search for a singular "perfect" language - unless that's something like English, with all its imperfections built-in.
foo x | trace (show x) False = undefined
| otherwise = ... x ...
? That's not comparing anything to undefined, now is it a pattern guard, it's executing the trace function which then returns False leading to the next guard being evaluated and the actual computation being performed. The undefined is just there because it always type checks, and will never execute.At the same time, its type system will be pluggable too, so that people can keep improving the type system (dependent types, dynamic types, etc.) without changing the language.
Oh yeah, and of course its code generation/execution model will be pluggable as well. Do you want to interpret it? Sure! Do you want to compile it so it performs better on your target computers? No problem! Compile it to JavaScript? Why not?
In that way everyone can be programming in the same language that's flexible enough for everyone's use, but at the same time can contain DSLs. Pluggable syntax/custom type checking means you can embed SQL code and make sure it's valid before running it. It also means SQL could be a custom SQLStatement data type with its own semantics.
In short, only a language that allows every type of programming can be the "one true language"
At the extreme end, take a look at Dylan.
Though, I was really referring to your other points. It seemed every one of your "questions" is directly addressed.
And because of that, I don't think a perfect language is even theoretically possible. Everybody has their own syntactical preferences, and allowing all of them means fragmentation.
(With PHP, it was apparently a bit subtle; ISP's can host it easily and it works well with live updates via ftp, and that beat out language features.)
Objective C is a third example: It's popular because iPhone.
On a related note, I'm watching with interest how the new Java 8's pluggable type systems[1][2] would play out (I understand the project is expected to have a big released April 1st). Those are pluggable intersection types, that can be inferred and injected to legacy code that was written without them.
[1] http://docs.oracle.com/javase/tutorial/java/annotations/type...
clojure have a lot of libraries, and leiningen
and echo the "worse is better" slogan i think that languages that offer a little more feature above the current mainstream languages, will success more than languages that offer a lot more
most programmers are doers, they prefer to spend more time doing rather than learning
languages that are too smart ... are less likely to succeed not until the day ... they become only a little bit better than the mainstream
we move slowly from c to c++ to java to ruby ... the next big language is one that is only a little bit better than ruby ... not a lot better
i think clojure fit the bill
i am sure most of the ideas in clojure wont be alien to most rubyist ... but still its a fairly large departure from ruby
a language that is only one or few steps above ruby, will have to use closer syntax ... be more or less focused on OO rather than functional programming
Also, FTA:
> Haskell is clearly moving towards dependent typing, which in theory, allows the expression of arbitrary invariants that are maintained statically, without having to run the program.
Well, "arbitrary" within limits. Dynamic type checking is still strictly more powerful than static type checking -- in the sense of what Boolean statements it can test about particular values -- no matter how you slice it.
(No, I'm not arguing for dynamic type checking.)
Both languages have their place. Sometimes functional and type-safe isn't the best way to go about something. Sometimes it is. Sometimes it depends on the programmer.
There is no One Right Way or One True Language.
Things like Typed Racket (and its port to Clojure.Typed) show that you can have a lisp with static typing, as well as the traditional strengths of a lisp.
You can also have a lisp like Dylan or Pyret that doesn't even use s-expressions, but is most definitely a lisp.
As an industry (and a research area) there still doesn't seem to be any real consensus around "what's best"; just a bunch of differing opinions and trade-offs. I don't think any amount of evidence (were it even to exist) would convince someone not amenable to strongly-enforced static types to see their value.
The entire practice of software development seems oriented around feelings and past experience. I can appreciate that their are groups of people doing research to try and bring rigor and quantified data to the process, but if at the end of the day, a developer can spend a weekend putting together a node.js web app and have that take off and prove successful, you've pretty much lost any opportunity to convince them that they should stop using their tools and switch to some different tools.
I don't actually think there's anything wrong with that either; good for them for being suspicious.
I decided to investigate Haskell about 8 months ago when I had an opportunity to write a big system for my job. It fit well within my constraints and requirements, and the little I knew of it at the time seemed like it would be a good language to spend time getting to know.
I liked that everything in the language seemed like it got there through reasoned debate and experimentation, and that it seemed like a language-feature sandbox that more mainstream languages were eventually pulling from (Perl advocates say the same thing about Perl, mainly that it already has all the features that other languages are now trying to figure out how to implement). I liked that they don't seem to punt on the hard problems (which over time become more and more of the problems left for languages to address), even if that means that doing complicated things in Haskell is complicated.
I don't know how I'd feel about Haskell suddenly becoming super popular though. Even aside from the "God I hate that this band I've been into for a while is now suddenly popular" trendiness, I don't think the community would be able to handle sanely what an influx of massive amounts of new users would do to things. It's hard enough getting all the category theorists and abstract algebra professors to deal with the fact that Cabal takes lower and upper bounds on dependencies.
If I could spend the entirety of my career using Haskell for everything, maybe that would be great. I haven't gotten good enough yet to have strong opinions about its failings, so I'm still very much in the honeymoon period.
But that seems like a silly thing to shoot for, even if I feel the same way about Haskell in 10 years that I do now. And it seems silly to expect that everyone else would feel the same way.
It's a great intellectual position for happy hour at the campus pub. Yet from a practical standpoint, it's hard to see how programming languages requiring more attention to type system will facilitate banging out code for ordinary problems more quickly.
There are times when it is really important to be able to prove code is correct and times when it is enough to just provide a plausible answer. The market for ML on Rails remains without validation.
And I'd love Bash embedded in a DT. Even if I never write the DT components being able to nicely map Bash -> DT is powerful.
Finally, I'd love Bash with sum types.
It's not a free lunch, but damn is it cheap.
This not the same as 'wrong code' in the abstract. The code will run fine if I don't pass in mismatched data, or more generally bad data. And if I am passing in bad data, static typing doesn't give me good answers, it just keeps the program from crashing. Don't get me wrong, there are times when crashing is bad. But there are times when the cost of a runtime error is nominal and the value of flexible code is high.
Static typing trades one type of cognitive overhead for another. The Java program of 500 classes is its manifestation.
Static typing can make "how do I get this to compile?" a design criterion Consider year 2038 problem. In MySQL, various date types are coerced to the timestamp type by design. Otherwise the program would not compile. Compilation takes precedence over problem solving.
But the other context in which we select and choose and construct data types is because a language insists upon it. Here our choices are not based on how to best represent the world, but by how to package our metaphor into a pre-existing schema. The very first time we compile our code, we have been forced by the compiler to crystallize our code based on an early guess.
When a flat roofed building uses scuppers to provide emergency overflow drainage, it is good if water passing through them makes a mess of the plantings below and perhaps stains the facade. It indicates that the primary drains are clogged before the roof collapses. Likewise, runtime type errors might be preferable to zeros silently inserted into a database.
Static and dynamic typing each catch some types of errors at runtime at the expense of masking other types of errors at runtime.
I think types make us write out the why next to the what. That why might be a domain model justification, or something much more trivial. It's also completely possible to encode an untyped regime in a type system. You're always crystallizing your design, you just can either provide good information to understand its failings and be more prepared to fix them. Or not and chase logic errors throughout an undocumented, dynamic system.
I'm not willing to concede that strong static typing is a universal truth though. Too many people much smarter than me seem to disagree.
i think people argue a lot about which language is better, because programming languages need to be popular
they need to be popular, because this is the best guanranty they will have lots of good quality libraries
and a bad language with good libraries trumps a good language with no libraries
if haskell becomes "ruby" popular, and haskell i believe is actually one of the more popular functional languages, you will only benefit
I think people argue about which language is better for much the same reasons they argue about which mobile phone operating system is better, or which console, or which text editor. They've invested (in the case of programming languages, potentially many years) in something, and want to feel like they've invested wisely. I think that's also probably just human nature.
I disagree with the notion that programming languages need to be popular, at least, as popular as they needed to be in the past.
20 years ago it was important for languages to be popular because that was the only way they'd reach enough critical mass to be discoverable. Growing up, there was only one book store within driving distance of my house that had any programming books. That meant if you wanted to learn programming, you were limited to what had been "blessed" by industry (in my case, one book on C++ that was actually just a syntax reference). That seems to no longer be true (the internet has made discovery of programming languages easier in much the same way it's made the discovery of everything easier).
There is certainly a benefit to having critical mass, but that mass seems to be much smaller than has ever been required in the past. I've seen Haskell developers on several occasions talk about the fact that Haskell's popularity has been in this nice "happy medium" that provides enough eyeballs to flush out issues and provide feedback, but also allows them to not worry about breaking things.
A language getting popular is not all benefits with no drawbacks. It gets harder to make breaking changes to the language (Javascript can be considered a good cautionary tale about what can happen when a language becomes so popular that you are held hostage by backwards compatibility). And you can't read a single article on here about some less popular language that doesn't prompt at least one comment about how because the language isn't popular, the quality of developers using it is higher (which is always posited as a plus for hiring developers in that language). Paul Graham mentioned it regarding Python explicitly in one of his essays.
More people being part of a community means more everything. More of what's good (libraries, eyes looking at bugs, ideas on solving problems) and more of what's bad (bike-shedding, shitty libraries, conflicting programming idioms).
I have no idea if there's some magical sweet spot, where you get all the benefits of a language being popular without any of the detriments; but I suspect it doesn't exist. As it stands, yes, I'd like there to be more Haskell libraries. I'd like there to be more blog posts about using Haskell for things that aren't abstract math. But I'm not necessarily itching for Haskell to become much more popular.
We're already starting to see some of the industry people that use Haskell start to explicitly target evangelism (like the work FP Complete is doing). I think it's great that they want other people to use this thing that they like, but I am worried anytime a community switches from "stealth" mode to "evangelism" mode. Incentives around addressing problems starts to become perverse when you have to worry about messaging, and when you're trying to convert other people. It seems like it evolves eventually to cargo-culting.
being more popular will only slighly enlarge the core, and keep it healthy (if a member drops, someone else comes in)
popularity is good for OOS projects ... i also disagree that a larger community will increase the bad, again the core developer size will probably remain small enough to work coherently
more testers cannot be bad more libraries can never be bad, it will just increase the likeleehood of having good ones happen
another reason for languages need for popularity is that it really take a lot of time and effort to master one if you spend years mastering a tools, you sure want it to be popular for more years ... to get a chance to use this knowledge
Odd, but not uncommon. Programmers are prone to falling in love with their tools. I've seen a lot of one-language-programmers proclaiming the advantages of their language choice over every other programming language. Luckily, this is not the case with this article. Still, I don't think there is One Language to Rule Them All.
With CLOS (and :before methods!), and optional type declarations, and the awesome implementations which do type inferencing, you tend to catch far more bugs compared to say, something like Python.
Yes, dear brethren, Common Lisp code (with SBCL) will not compile if the ftype conflicts with the inferred types of the arguments.
Edit: Besides, the author, does not seem to have the same opinion about Lisp vs Haskell today.