John Carmack: Thoughts on Haskell [video]
functionaltalks.org
functionaltalks.org
Languages talk about being multi-paradigm as if it's a good thing, but multi-paradigm means you can always do the bad thing if you feel you really need to, and programmers are extremely bad at doing sort of the the time-scale integration of the cost of doing something that they know is negative. I mean everyone will know...it's like "this global flag: this is not a good thing, this a bad thing - but it's only a little bad thing" and they don't think about how, you know, the next five years how many times that little bad thing is going to effect things. So brutal purity: you have no choice.
In particular, I like the bit about integrating your technical debt function with respect to time, over the time you have to live with the debt, and how bad programmers are at thinking in those terms. We tend to think about how much technical debt we have at a given fixed point in time, but the area under the curve is what bites you.
Essentially brutal purity is a really strong anti-kruft coating. Since kruft can't stick to your code, it tends to stay nice and shiny well into the future, unlike the dirty impure code elsewhere that builds up a nice thick layer of kruft over time and eventually collapses under that weight (or else some poor soul ends up spending a bunch of time either scrapping all the kruft off, or just throws it out and builds a new one).
OO/imperative programming isn't all "bad." Being functional doesn't really ensure that the code is "good."
The same applies to programming paradigms.
One can write simple, structured code for simple problems in any language. But complex problems require languages that don't attempt to babysit programmers.
(IMNSHX, YMMV)
Scala is quite flexible and can accept good and bad code, which is good. I was only calling out FP abuses and its promotion as some sort of holy grail that is so much better than objects. If you compare Scala FP to Java OOP, ya, Scala FP is better, but if you compare Scala FP to Scala OOP, it's a much fairer battle and I put my faith in the latter (with the caveat that I can always use Scala FP when it makes sense to do so!).
Instead one should be able to just hack away until the code kind of does what they want (even though they can't prove anything about that piece of code) because that is the only way to really manage inherently complex problems?
The older I get the less I find writing code an attractive method for understanding a problem. I also find utilizing something like a type system to enforce invariants in code liberating - and actually conducive to solving problems.
You could have it output the value after each function is applied, but that would either break purity (by having I/O) or, per the GP's point, be tedious to write. At this point, the Haskell community made the decision that in debugging, "screw purity", and the output is effectively untyped.
Certainly, you can use tests of expected invariants (eg QuickCheck), but that just tells you that the whole thing fails.
That is, I think, also the GP's point: that the same things that make your final code good in Haskell, also make it hard to write.
(No to say I don't like haskell; this is just a peeve,)
Long answer: You can, but Haskell is very serious about keeping pure code separate from impure code. Any function which performs I/O is impure, so its output must be an IO Thing instead of just a Thing. This means anything that uses its output must accept an IO Thing instead of an ordinary Thing, and so on. Of course, the way I/O works means that a 'Type1 -> Type2' function into can be transformed into an 'IO Type1 -> IO Type2' or a 'Type1 -> IO Type2' function (this applies to many things other than I/O -- consult one of the many monad tutorials for more on that). Haskell even includes some special syntax for invoking these transformations that makes things look rather like an imperative language, but adding in a debug print still requires you to rewrite a lot more code than you would have to in another language. Alternatively, you could use unsafePerformIO (whose type is 'IO a -> a') to hide from the type system the fact that you've done some I/O, but you may run into trouble with the printing not behaving as you expect (due to Haskell's lazy evaluation).
You can, and this is not just a technicality - it can be useful in debugging.
It's certainly true that laziness means that when things print will be unintuitive if your intuition is trained on strict languages, so any short answer conveying as little information as just "yes" or "no" is liable to be confusing, though...
I.e., if your program needs top 3 results in a race, then you can safely use a function that returns all the results, but since you afterwards use only first three then the remainder aren't calculated, that code most likely isn't run and the order of any "embedded" print statements would be undefined.
That being said, standard Debug.Trace module allows simple adding of debug prints anywhere.
If you don't care when it happens, you can use unsafePerformIO - which is just fine for debugging, though there are often better approaches.
As mentioned, it'll only print when the wrapped value is actually evaluated, which won't always correspond to it's position very well.
However, it's very much worth noting: if you're in IO then you can just sprinkle debug statements. If you're not in IO, then your function isn't effect-full and you can run it (or pieces of it) safely with whatever args you care about (also, quickcheck is amazing).
Again, QuickCheck doesn't help with that.
Another option would be to pull the file in question up in ghci and play directly with the components of the function - in pure code that should be safe in a way that it won't be in effect-full code.
I also dispute the assertion that QuickCheck doesn't help - if you're getting weird behavior, then hopefully you can characterize that weird behavior, and get some example failed values out of QuickCheck - and getting a picture of what values the function is failing for can absolutely be useful.
For instance, if you defined
f x y = g (x + y)
then you could instead write f x y = trace "calling f" (g (x + y))
to insert a debug statement.(dllthomas linked to the library but didn't point out how it works, and unrealistically expected people to like, follow the link or something. argv_empty is correct that side effects cannot be obtained without either wrapping functions in the IO monad or calling unsafePerformIO. Debug.Trace.trace calls unsafePerformIO behind the scenes.)
Programming is not just about implementing solutions, but also about finding solutions.
But types are fairly conservative, dynamic languages still provide quicker turn around times even if semantic feedback can't be provided. One of my research projects is focusing on getting the benefits of both.
Interestingly the best reverse engineers I've ever met, who are total descriptivists with a debugger, are also some of the most pedantic prescriptivists when it comes to writing actual code. It comes full circle, I think because they understand the reasoning behind the API contracts better than the API documentation tells.
When constructing a set of rules in the first place (or when first formalizing an existing, but implicit, set of rules), the human can do the requirements gathering, design, bug-finding, etc. But humans are terrible at simulation--they can't foresee what each and every consequence a particular rule might have. When constructing a rule-set, a human's productivity is enhanced by the computer taking over the "simulation" part, to explore the consequences for them, where the human can then change the rules in response.
Type systems of all kinds, though, basically assume that the rules defining the code are invariant. They don't help with simulation at all.
Types prove that something cannot happen.
Sadly, even when your system is pure and elegant and you've proven it all out, you still need runtime asserts (and more advanced things like quorum voting on the results of computations, with the ability to fail nodes experiencing problems) before you can be "sure." Ada wasn't enough for NASA; they needed redundant processors too.
CS is weird as a discipline: we get all the tools of mathematics (digital logic, proof-verification), and all the tools of engineering (rate-of-failure calculations, high-assurance systems)... and then it turns out that the problem-space (ensuring perfect automation over trillions of repetitions of a task) is so difficult that it requires both! Arguing types vs. asserts is a "rabbit-season, duck-season" debate. Anyone who isn't using both a type system and runtime checks, is working in the dark.
However, I can and should probably point out that useful runtime checks can be derived from static properties of your code--and a Sufficiently-Smart Compiler[1] would automatically insert them as it compiled your code.
---
[1] not all that rare these days, GHC's stream fusion goes way beyond "sufficiently smart"
This isn't unique to Haskell by any means, but it's the only real complaint I have about Haskell as a language for non-toy projects. The benefits definitely make it my go-to language. It's hard to list them all, but by far the nicest feeling is the correctness: when your code compiles, 60% of the time your program works every time. (Not to imply that tests aren't necessary--QuickCheck is great for that.) It's an otherworldly feeling to write a program not in terms of what to do, but what kinds of filters you want to put on something, and have it just work (and either stay working, or break future compiles if something's changed!) after compiling 10 lines of code, when you would have written at least 50-70 and had to debug it in almost any other language.
Edit: I'll add another complaint: Haskell is like C++ in that it's incredibly easy for a codebase to become completely unmanageable if your team doesn't have a common style/discipline. Go is a nicer language for "average"/"enterprise" teamwork, I think, since it almost forces you to write programs in a way everyone will understand. If you're in a team with good programmers that you trust not to abuse the language, this is a non-issue.
Edit: Okay, another one: If you change your Types.hs, the recompilation can take a long time in a large codebase, similar to C++. But GHC/Cabal keep getting faster.
Think that's it.
It's not noticeable in general, since only the Types.hs affect many different files, and they're rarely changed. Also, 99% of the compiles are partial, i.e. only the files that have changed (or dependencies of them) are recompiled. Partial compiles don't feel slower than ones on my smaller Haskell projects.
Is it annoying compared to e.g. Go? Definitely. But it isn't ruining my life. And there's a lot of optimization, fusion particularly, that goes on in the background.
If it's just me making something, 99% of the time I'll pick Haskell, unless I know everything that I'm going to do is mutate a hash table or array, in which case I use Go. (Not that that's not doable in Haskell, it's just not as intuitive/easy to do efficiently. Keep in mind that I said "if it's the only thing"--Haskell's downsides in this area aren't significant if you're also doing other things, and especially so if just some of those things are pure/don't have side effects.)
The reason why I say "if it's just me" is that Go is much nicer to use in normal teams. To me, it's a Java/Python/Ruby/JS competitor. Let's face it, a lot of enterprise teams aren't as disciplined or as good at/interested in programming as they could be. For this, Go is perfect. You could read that as "Go is for average programmers", and that is true--in a good way. It's extremely easy to pick up for new members of the team (Haskell is very hard, I have to admit!), everybody can collaborate without asking a lot of questions, because of 'go fmt' there's never any indent wars, and it's a snap to compile and deploy binaries.
If I have to do anything like map/reduce/filter, really any kind of operation on a set, I hate using Go. But it's not Go that I hate; it's most imperative languages. I don't want to specify how to do all of that, much less repeat how to do it. (In Haskell you can actually run into performance problems pretty easily because you've gone overboard with filtering sets in ways that would have sounded alarm bells in imperative languages.) Granted, languages with generics are better for this, and to me it's the major thing Go has to gain from generics--but as you implied, I just enjoy the "freedom to think about important things" that you get when you're thinking about results and not operational steps. It's unfortunate that it's so hard to know what this feels like without actually picking up a functional language.
When friends ask me which language to look at, I say "get to know Python, then learn Go" nowadays, mainly because pointers and pass-by-value can be a little hard to understand. Go is a great Python replacement. Web applications and APIs/backends I've particularly enjoyed implementing in it. Haskell is for when you've spent so much time coding in imperative languages that it's all boring, and you want to step into an interesting, but extremely frustrating (at first) new world.
A lot of people say e.g. Haskell would be as easy to pick up if it was your first language. I don't think that's true, if only because of the amount of syntax you have to learn--Scheme is probably better--but I do wish I could go back and try.
I do hope I can advertise it at some point, as the "Haskell in Industry" page is sorely lacking non-finance companies, and I'm sure there are a lot of companies out there using it.
If you want to see where Haskell is being used in industry, check the Haskell in Industry page out: http://www.haskell.org/haskellwiki/Haskell_in_industry
And if you really just want to see what it is like for game development, check out Frag: http://www.haskell.org/haskellwiki/Frag
Haskell is very well suited to large applications in my personal experience. My feeling is that it is actually worse for small-scale applications where you don't need the type system guarantees it provides.
I tend to disagree with this. Yes, GHC is a large application and most certainly developed by experts, but it's also a project with a very long history, whilst Haskell (language, extension, conventions,...) have changed over the years, and this shows in several places.
Next to that, GHC code isn't very idiomatic at times (e.g. for performance reasons, or because it can't depend on too many external high-level libraries).
Go seems to have been used in lots of networking-related scenarios, but I have yet to see either a major graphical application or even well-known open source game written in it yet.
I would love to see what the architecture of a 3D renderer looks like in Go, as an example.
Like you, I also share the same interest with Haskell.
Diabolic, stateful database connections blocking a Haskell treatise! They should have copied the database on each request instead.
Explain?
That being said, whether data sharing is a good thing or not depends on the situation. For example, copying to a cache can expose more memory parallelism in an application. I feel like you are taking this opportunity to take a jab at FP here ("the purists detest mutation"). While FP is my preferred paradigm of development, I wasn't trying to push it on anyone--I was merely making a technical point.
If you do happen to be doing something where the difference is truly going to matter, or you're writing a library anticipating needing that kind of performance, using mutability internally in your functions isn't discouraged as long as they remain referentially transparent: http://clojure.org/transients
Likewise mutable data structures while not the default, can be implemented in Haskell, they just have some caveats attached and shouldn't be used lightly. In other words, all things being equal, use the immutable data structure, but if you have a good reason to, you can use a mutable data structure instead, just be sure you know what you're getting into.
:) You could say that "all things being equal" never happens because mutable data structures have worse persistence if they're not used in a way that their performance matters. But also, different defaults make sense for different problem domains.
I was a bit confused because O(log_32(n)) = O(log(n)) = log(32)+log(n) = O(log(32n)), so no matter how I parsed it I would just get O(log(n)) :)
Eventually by the time they came to an agreement on hash tables (C++11), a lot of vendors had their own versions of it as non-standard additions to the library. To avoid conflicts with these existing hash tables, they named it "unordered_map".
> Many operations have a worst-case complexity of O(min(n,W)). This means that the operation can become linear in the number of elements with a maximum of W -- the number of bits in an Int (32 or 64).
[1] http://hackage.haskell.org/packages/archive/hashmap/1.3.0.1/... [2] http://hackage.haskell.org/packages/archive/containers/lates...
In the face of these techniques (and others, e.g. re-structuring things to use zippers and being clever manually or whatever), I suspect the actual amount of redundant data might be quite a bit less than you might think.
Pure FP = don't mutate variables in the program. Of course the actual implementation can reuse memory blocks or else pure FPers would have to keep buying new memory!
So, no, even in pure functions like
f x = x + 1
x is a bound variable. It doesn't 'vary' in the sense that the value it refers to can be mutated, as in a imperative language, but it varies between calls to the function f.From http://en.wikipedia.org/wiki/Variable_(mathematics):
"Varying, in the context of mathematical variables, does not mean change in the course of time, but rather dependence on the context in which the variable is used."
"The identifier in computer source code can be bound to a value during run time, and the value of the variable may thus change during the course of program execution. Variables in programming may not directly correspond to the concept of variables in mathematics."
val y = Console.readInt
var x = y + 1
x = y + 2 //valid
y = 5 //error, vals can't be changed in the same block- val a = 1;
val a = 1 : int
- fun f() = a;
val f = fn : unit -> int
- f();
val it = 1 : int
- val a = 2;
val a = 2 : int
- f();
val it = 1 : int // a is still bound to 1 as far as f() is concerned
Now in Javascript:
> var a = 1;
undefined
> function f(){return a};
undefined
> f();
1
> a = 2;
2
> f();
2
>
If you bind a val, then you define a function that references that val, then you later shadow the val binding, then call the function again, the function still sees the earlier val binding, because it's a closure of the environment at the point where the function was defined, not at the point where it was called. This is unlike variable assignment in imperative languages.
For example, in programs with mutable state, it's not uncommon for modules to return deep copies of objects to avoid mutation-related bugs. Java's String objects are immutable precisely to avoid having to do this.
It's the opposite: functional programs prefer immutable data and the only way to achieve this is to copy a lot more data than you would if you were just overwriting existing objects.
To perform a query, you connect to a database and request the present state of the database. This is an immutable representation of the database at that point in time, and you can query it however you like, or even hold on to it forever. Of course, the database isn't actually fully copied.
How is consistency handling performed? Does it rely on knowing my snapshot version and checking for write conflicts (a la oracle SERIALIZED level), or is it based on explicit locking?
Datomic has no notion of mutation/destruction. Data is stored in the form of facts which are asserted or retracted. Retracted data can still be retrieved from an earlier moment in time, so it isn't really gone. In many ways, it has a lot in common with DVCSs such as git (though it has only a single authority with commit access: the transactor).
My question is if/how you do conditional updates. Say I'm storing some particular concept C as a collection of facts, and I want to update fact C-1 to '15' if fact C-2 is '0'. In an RDBMS I might select for update fact C-2 to make sure it didn't concurrently change underneath me while I'm making the change - is there an equivalent in Datomic? I understand that under normal circumstances reads are completely decoupled from writes, but what if I want to make a write if and only if a certain state holds?
You mean, Hickey just fakes it? Now I'm deeply disappointed.
It doesn't seem like he has put the same amount of effort in experimenting with Lisp. He doesn't mention any attempt to port Wolfenstein over to Common Lisp. Instead he seems content speculating from the same position many Lisp doubters have after reading a few books and working on some exercises (which is ironic considering his impetus for the Haskell project). I hope he gave Lisp the same treatment as Haskell before he drew any conclusions but it doesn't seem like he has from this speech.
Lisp for game development could be an interesting avenue (and has precedent in AAA console development). The dynamic vs. static argument isn't the interesting feature. Personally I think the symbolic model of computation is far more compelling. I've read posts by programmers who've written a high-level language for writing financial trading algorithms in Common Lisp that compile down to optimized VHDL for running on FPGAs. Sure you don't have a static analyzer to tell you you've done something wrong before you run your programs but I've rarely seen that becoming an issue in practice at that level. There are plenty of Common Lisp libraries that have been around for a long time that don't require much maintenance which makes me wonder where this belief that dynamic languages don't produce solid, maintainable code comes from.
In my rather limited experience I find the over-specification required by statically typed language to be a impedance to writing robust, compose-able software (at least it's much more difficult and tends to lead to Greenspunning if you try to go that route).
Either way... a very interesting talk and it's cool to hear that he's experimenting with this stuff. Carmack is in a rare position to have such a breadth of experience and deep technical knowledge that even just messing around with this stuff might make waves throughout the industry.
Lisp vs ML vs others is a very subjective issue since languages involve different kinds of trade-offs, so optimal choices depend on the project or on personal style.
Also, this is John Carmack. For me he was a God 10 years ago and he's still one of the best and most practical developers we have today. Seeing him talk about Haskell is amazing.
BTW, I've been a big fan of Scala lately, and I don't see myself using much Haskell precisely because its tools for modularity seem limited. In Scala you can use OOP to build abstract modules, with the much hyped Cake pattern being based on that. Dynamic languages are naturally more modular, however I still prefer static languages.
When Carmack speaks, I do listen. He's a very good programmer and has the level of experience and technical expertise few will get to enjoy. I think it's awesome to see how far he has come from being a C purist, to C++, and now nice functional languages like Haskell.
> Dynamic languages are naturally more modular
Why is that? I'm pretty convinced pure and especially lazy languages allow for great modularity.
See http://augustss.blogspot.nl/2011/05/more-points-for-lazy-eva... for an interesting view on this.
Consider putting a new "module" on a bicycle. If you were doing it statically, it would have to have the screws in the proper place to be able to attach as it needs. Dynamically, however, you just use a zip tie to hold your piece onto the bike.
To be sure, if you buy a nice bike, many of the common attachments have "static" points where things can be added. If you want to place a holder for a phone, however, you are much more likely to do something that is much more adaptable at fitting things on.
becomes much more easy to bolt on funct
He's been a heavyweight proponent of static typing for a while - 2-3 years IIRC - and so it doesn't totally surprise me that Haskell is up his alley. It's particularly notable that he prefers it as a technical lead as a low-pass filter on bugs.
But my basic issue with talking about Carmack with respect to programming languages is that he never struck me as an expert on languages. Or even programming for that matter. The source code to Quake was on the messy side of things. His rationale for switching to C++ always seemed a little on the thin side of things, and seemed more like industry pressure than anything. He's great on concepts, on graphics, and keeping up with OpenGL and whatever NVidia/ATI are doing. But he's just now coming around to Scheme and Haskell. I don't really think his perspective on languages carries that much weight to be honest. But I'll listen to him talk about it anytime.
He builds big and complicated programs that people actually use.
That's kind of an insult to, well, every working programmer.
The fact that he's not a long time user, or particularly a fan of functional programming, but a relative newcomer to it just adds more weight as he isn't some known fanboy that's going to praise FP no matter what. He's got extensive background in C, and at worst, average C++ skills if not considerably better than that, so he makes a fairly good case study of how practical it is for a non-functional programmer (but someone who's really experienced with imperative/OO programming) to pick up something like Haskell and actually do something useful with it.
It's also good to listen to the things he doesn't like about Haskell, E.G. debugging, and see if anything can be done to improve that.
I'm not saying that's bad. But it's obvious he doesn't keep on top of trends that well. Or languages, for that matter.
Are you serious?! If you're only intermediate - then there's no hope for me!
So, while becoming an expert haskeller is enviable, intermediate still suggests an ability to tackle a Fantastic array of problems.
In fact I'd go so far as to say that the pool of wicked smart people who would love to build stuff in Haskell represents a significant opportunity to start ups right now. How have you found hiring for your company?
Beginner: Can read the basic syntax, knows about E.G. Monads and folds
Intermediate: Understands and can use the more advanced classes like Applicative, Arrow, lenses, and how to use things like fix
Advanced: ??? Makes new powerful classes/libraries like Pipe, Conduit, or Netwire? Can easily translate a problem domain into a concise and elegant type representation.
Maybe there needs to be some more layers in there, I don't know... also the advanced level feels weak to me. I'm still somewhere between beginner and intermediate myself.
http://www.st.cs.uni-saarland.de/edu/seminare/2005/advanced-... (PDF)
The article is essentially just a link to the fourth part of his keynote at Quakecon ( http://www.youtube.com/watch?v=1PhArSujR_A )
Looks like we brought down another one. I was looking forwards to reading this too.