Getting the transcript
Reading the captions from YouTube. A video nobody has opened here before takes 10 to 30 seconds; this page fills in on its own.
Getting the transcript
Reading the captions from YouTube. A video nobody has opened here before takes 10 to 30 seconds; this page fills in on its own.

James Faure · @James_Faure
Words
3,868
Runtime
26:17
Speaking pace
147wpm
Reading time
16min
147 words per minute, below the 160 25th percentile of 349 measured videos. That distribution comes from the 349-video hook study.
Opening (first 30 seconds)
Haskell is my main language, with C as a low-level backup. So, this is personal. Quick disclaimer, though. Haskell deserves respect for its age, ambition, pioneer credentials, and the hard-won wisdoms its cautionary tales left behind. Purity is an uncompromising central principle carries it, despite and I wish I were joking almost everything else being a misstep. You all seem to like negative criticism, and it's fun and destructive to explore how systems break.
74 words, the words spoken in the first 30 seconds at 147 words per minute.
Free, no signup. See how the first 30 seconds hold attention, with rewrites.
Sentence shape
| Measure | This transcript |
|---|---|
| Sentences | 270 |
| Average words per sentence | 14.3 |
| Longest sentence | 41 words |
| Questions asked | 4 |
| Sentences containing a number | 14 |
Most used terms
Filler phrases
51 in total: uh 19 · like 18 · basically 7 · sort of 3 · actually 2 · I mean 1 · um 1.
A literal whole-word count of the same phrase list the Prepublish browser extension uses, so a phrase inside another word is not counted and a phrase used in its ordinary sense still is. It is a count and not a judgement.
What this transcript is
Every word below is the caption track YouTube publishes for this video, pulled from the video itself and reproduced unchanged. It is not Prepublish's writing, not a summary, and not a re-transcription: it is the video's own published captions. English captions, generated automatically by YouTube, in the video’s original language. Source: the video on YouTube. A channel that would rather this page did not exist can ask for its removal through the contact page, and it is removed.
No Script X-ray for this video: YouTube shows a Most replayed graph only once a video has enough views.
Haskell is my main language, with C as a low-level backup. So, this is personal. Quick disclaimer, though. Haskell deserves respect for its age, ambition, pioneer credentials, and the hard-won wisdoms its cautionary tales left behind. Purity is an uncompromising central principle carries it, despite and I wish I were joking almost everything else being a misstep. You all seem to like negative criticism, and it's fun and destructive to explore how systems break.
So, as a type theorist and compiler engineer, I won't hold back. Especially because forget falling short of mainstream adoption, Haskell might have branded functional programming itself as impractical. It wasn't always like this. In the 1980s, there was huge hype driven by ambitious research and dozens of practical attempts. Haskell itself was born in a 1987 conference aiming to design a standard lazy functional language to unify the fragmented landscape. 40 years later, this golden age of mathematical programming dissolved so thoroughly that mainstream interest could tunnel vision on imperative memory management schemes.
This time, I'll focus on the Haskell language and the compiler. Basically, the parts you cannot opt out of. So, the most polarizing issue is type classes. Here's where it all begins. Suppose I define a function inc x = x + 1. This can conceivably work for integers, floating-point numbers, big ints, or any number-ish thing. One simple and sound approach is to make multiple specializations, like OCaml does. But, if you wish for one single function to do it all by behaving differently based on the type of number, then you're asking for ad hoc polymorphism.
Intuitively, that seems very desirable. However, it raises a tricky question. What is this plus function? Its type must somehow accept a set of types, and it behaves by selecting an appropriate specialization based on the chosen type when instantiated. Haskell was eager to experiment and went with type classes. The fat arrow lists class constraints on the left. For example, you can define a num class by listing the functions that instances must define, perhaps addition and multiplication, and so on.
Then, when for example you write plus somewhere in your program, a constraint solver will walk the program in search of a unique instance, perhaps addition of 32-bit integers. On the surface, this is reminiscent of code data, but the machinery involved is multiple times heavier and more complex. Essentially, Haskell's plan is to slap a Prolog-like logic language on top of the basic lambda calculus. That extra baggage is then dragged through the entire compilation, generating unholy complications and ending up in the runtime as function dictionaries.
Implicit work happening behind your back can feel convenient, but even avid proponents of type classes will admit they are an expensive complication. They will, however, insist the complication is needed for practical convenience. Thing is, type classes instantly break three pillars of the type system. First, principal types are lost because constraint solving and generalization are mutually dependent, and different orders can give different results.
Note that this loss of principality is fatal to the idea that inference is a relation rather than an algorithm. That trade-off is extremely committal, and frankly sold off cheaply considering we lose a foundational property. Secondly, parametricity is partially broken since type class constraints let types demand an implicit witness. Before this, you could view types as sets, although the goals and philosophy of type theory and set theory are different.
But, due to type classes arising separately from the base type theory, it is very hard to make either classes or constraints first-class citizens. That means you cannot pass them as arguments, store them in data structures, manipulate them in runtime, or really do much with them as all outside of their hermetically sealed layer. Thirdly, we are making an assumption that the world is open. And there is also forced global uniqueness.
For example, there can only be one monoid int, even though mathematically there are many monoids on int. That point can be hacked over using new types. For example, sum or product, as is done in the base library, but we start down a slippery slope of increasingly suspicious contrivances. Basically, type classes are now an interminable source of strange complications. The numerical tower set up in base is an example of a rigid compromise.
Otherwise, attempts to encode category theory in Haskell always run into odd problems. Instance resolution needs to decide when two types are not equal to rule out conflicting instances. New type provides a fresh nominal type, so you can write instance monoid age without colliding with monoid int. But, these declarations for types, classes, and instances live at the top level because resolution is a global syntactic lookup over a fixed instance table.
Local instances were actually proposed and rejected because they break coherence, which means there may be multiple competing instances for the same type, and letting both type and scope choose behavior would really be too weird, even by type class standards. So, the theory might have gone to the but maybe that somehow doesn't matter to practical programmers. Well, let me dispel that hope. Issues come up all the time in practice.
Starting with the fact that dodgy systems create dodgy error messages. Haskell could be called type class oriented nowadays, and even if you abandon all libraries and write your own prelude, you cannot easily be rid of them. The global uniqueness is in practice a brutal anti-modular handicap. You can easily create orphan instances when trying to locally specify instances, after which anyone using your module silently inherits that instance.
So, in practice, you probably should give up on locally specifying anything. Undecidable and overlapping instances are sometimes needed when doing type class manipulations, whereupon the program becomes even more of an undebuggable, non-deterministic logic puzzle for the compiler to solve. As for performance, uh type classes can be free if the compiler specializes on the instance, but in practice, you should expect to pay a pricey function table indirection.
When building webs of constraints, there is some potential for subtyping. It's a bit unreliable, since it goes through the Prolog-style logic solver. As a basic example, int to int is functionally a subtype of for all A num A fat arrow eight A. However, it's only a fragile illusion, and not likely to work unless the constraint is quickly instantiated. The extent of the cancerous nature of type classes is best appreciated through related extensions.
These patches are incapable of reconciling global instance resolution with Hindley-Milner inference, so they often feel like tossing increasingly desperate contrivances onto the pile. Multi-parameter type classes are an obvious extension, but immediately introduce multiple ambiguities. First, class methods might not mention all the parameters, leaving some unconstrained at the use site. Even when they do, instance resolution needs a covering condition to uniquely determine each instance.
Functional dependencies is the most inference-friendly extension that allows classes to declare which parameter uniquely determines another. Alternatively, by enabling ambiguous types and type applications, callers can specify intended parameters. Now, some more that loosen restrictions like flexible instances and undecidable instances, which are somewhat routine. Overlapping instances permits overlaps when a more specific instance exists.
Incoherent instances permits overlaps even when none exists. So, basically, it's unchecked and indicates that whatever you're doing is probably way too weird. But, like all weird deviations from solid maths, there is a rabbit hole and an apparently justifiable use case for everything, even incoherent instances. I found one in a GHC pull request where I think the point is that the new type instances are no-op, so it doesn't really matter if a more specific instance was missed.
Now, honorable mention to some other type class extensions. Flexible contexts lifts the Haskell '98 restriction that constraints in signatures must be of the form CA or C open parens A of B. That enables constraints like monad of parens T of M. Constrained class methods allows class methods to mention constraints on the class variable itself. For example, LM of type EQ of A and fat arrow A to S of A to bool inside the class Sec SA.
Pretty weird stuff. Uh there's also default signatures. I'm not sure why not. Quantified constraints allows constraints to quantify over types. So, you can state things like for all A if EQ of A holds, then EQ open brands F of A. That looks like a big expressivity boost. Never used it. Uh type synonym instances that enables instance declarations to use type synonyms. So, you can use aliases basically. It makes sense, but also looks to me more like obfuscation.
If you're defining instances on really complicated types, then I don't want to see it. Undecidable superclasses uh relaxes GHC's recursive superclass cycle check allowing mutually recursive superclass constraints at the risk of non-termination. Finally, honorable mention to implicit params, a parallel mechanism for passing constraints implicitly. Agda has a way better version of this. I've never seen it used except for has call stack.
That one use case is very useful though because GHC handles its own stack. So, if you want a stack trace, which you do because it's useful for debugging, then you have to manually demand one by adding has call stack as deep as you want the call stack to be. It's pretty verbose. All right, type classes conclude here. The most important question, and also the one which seems to be the least asked, do we really need ad hoc polymorphism?
Uh no. OCaml doesn't have it, and being explicit is a little annoying, but not more than that. Okay, but what if we really want ad hoc polymorphism? Well, the real tragedy here is type classes aren't the only solution. Haskell was experimenting and ran with it, but successor languages have no excuse. OCaml, which steadfastly refused to go off the rails, uh has been able to develop a principled alternative, implicit modules.
This gets you ad hoc polymorphism with way simpler machinery. It's not even the only alternative idea. The real lesson here is to extend a system along its existing seams. Type classes are a foreign mechanism that doesn't share a theory with Hindley-Milner. This irreconcilable friction causes extensions to accumulate rather than converge. Although we are done with type classes, as my favorite Chinese saying goes, one step wrong, more steps wrong.
And so, Template Haskell does compile-time staged meta programming. It is another separate layer of hackery that manipulates the compiler's abstract syntax tree directly. Also, it is allowed to do IO and access module private data. So, there is a security risk for packages. Although the output is type-checked, the manipulations are not, and it's very difficult to reason with it since it's a monadic DSL. So, we're dealing with imperative order-dependent generation with very little control over what gets generated at the top level.
It tends to be bloated and a bit slow. It's also easy to generate expressions that don't compile, like referencing a variable that doesn't yet exist, or accidentally capturing names. In which case, good luck debugging what specific conditions trigger the problem. It's awkward to unit test since failures are host-dependent at compilation. You cannot splice values defined in the same module due to stage restrictions, which forces awkward module splits.
Note that simply dependent types provide a much more solid approach to meta programming since you can write derivation functions directly in the core language. But Haskell spent most of its extensions getting as close as possible to dependent types without actually getting there. The practical residue is generics and auto derivation. You can sometimes get decent mileage out of those. The number of Haskell extensions, according to this GHC source extract, is 135.
That betrays its long life and committee-driven management. They are impressively solid since the numerous surface-level complications are stripped down to well-understood core. Even so, every new extension chips away at my sanity. Full disclosure, looking at one of my projects, I've enabled GHC 2024, which flips this list on, and then manually added a dozen more. The first few give extra syntax. Lexical negation, multi-line strings, multi-way if, and block arguments are intuitive and could be default.
Or patterns is a great recent addition that makes pattern matching more algebraic. Apparently, it was a struggle to implement, and they still haven't managed to handle binding new variables in an or pattern. Then, some extensions to do syntax that, to be honest, I don't use anymore cuz I don't like do syntax. Uh strict data is a pretty big change. Sadly, there's no way to fully escape laziness, but this helps a bit. Overloaded strings allow string literals to be byte string or text, etc., rather than forcing string.
Uh partial type signatures allows writing an underscore as the type hole. That's pretty nice while developing. You don't have to type out the full type in that case. You can just leave it empty and allow to be filled in by the compiler. Unboxed tuples and magic hashes for manipulating unboxed primitives. It's basically for performance. Linear types is a fairly big one. I've experimented a fair bit with this in Idris, especially.
And frankly, the Haskell version is a bit too incomplete to be useful. There's no uh quantitative type theory, no linearity polymorphism, and it doesn't work through let bindings. Plus, the proposed solution to extend it to let bindings will use type class linearity constraints. So, um there goes the perfect lightweight solution linearity is supposed to give. Just uh keep digging a deeper hole, I guess. Type families are enabled per file if necessary.
They allow functions at the type level, which is a good step towards types. And it would be a great tool but for being a bit limited. If you had full dependent types, it would just be way better. GHC uh 2024 enables a bunch of mainly benign extensions. Like, this is the stuff that they've decided to switch on by default, essentially. Binary literals, hex float literals, explicit foralls are free syntax improvements. There are a few uh deriving extensions for that internal mechanism.
That is pretty great when it works, but it's a bit limited. Like, it works great for functors especially and a few others. Ideally, we'd have library control over that with dependent types. Instead that would usually be covered by template Haskell or perhaps GHC.Generics. As discussed, a bit ugly. Empty case alleviates a syntactic restriction. Of course, an empty case matches on nothing, which indicates that you are uh in a provably unreachable part of the program.
You're matching on a void value. That stuff is very useful in a proper calculus of constructions. So, uh GADTs, or a generalized algebraic data types, are another of those approximations of dependent types. They allow a type parameter refinements based on the constructor being matched, which allows more precise types and thus more safety, although weak compared to dependent types. Since you can only instantiate Haskell types in there.
Now, sadly, there are no co-GATs. Oh, there isn't even co-data. That is a massive hole in the type system. In category theory, our first instinct is to construct a dual by flipping arrows and inverting categories, as is free. So, data and co-data are two sides of the same coin, and co-data turns out to be great for interfaces, among other things. Are the sort of thing that is normally tossed into the ugly pit of type classes.
That is it for the language. For me, though, Haskell's least forgivable misstep is performance. And the reason is sadly structural. Haskell committed to laziness as the default execution model, which forces funk allocation, dynamic calling conventions, and a garbage collector that cannot be removed due to the graph reduction execution model. All these decisions make sense one after the other, but the resulting compounding errors have left a mess.
Consider Haskell's linguistic ingredients. Pure functions as a central principle gives the sort of guarantees, equations, and multi-threaded maintainability that a C compiler can only dream of. So, it pains me severely that a GHC failed to leverage any of this. Instead, it has spent some 30 years making laziness cheaper with the likes of funk recycling and pointer tagging. But every optimization is a local fix for a global cost.
Fast Haskell and idiomatic Haskell are different, and you are pushed towards the slow one. It's not that Haskell executables are particularly terrible, don't get me wrong. It's still a compiled language, and the performance can match C programs if you avoid the GC and work on unboxed data. It's just disappointing. Laziness is probably the root of these issues. It's now unanimously recognized as a mistake. Apparently, the initial reasoning was to eternally force purity.
Uh that part at least worked out brilliantly. Although laziness can encode infinite data structures and generator type patterns, co-data is a much better solution. And even ignoring that and insisting that some constructors be lazy, not everything has to be. The hope that unused values can be skipped is mostly wishful thinking. Programmers already use if statements pretty intelligently for that. If you really want to automatically detect dead data, then there are ways to do it without poisoning the whole system.
Lazy values are thunks, meaning the program branches on whether the value is already computed or not. That is intolerably expensive. Plus, reasoning about resource usage, side effects, and even termination become difficult or impossible, since computations can be delayed for a long time. Space leaks are possible due to unevaluated thunks not being forced in time. Seq is a compiler built in to partially force values to weak normal head form, but it comes with a bunch of tricky gotchas in the presence of rewrite rules.
Optimizations are also affected, and mixing eager and lazy code can kill performance in non-obvious ways. Unfortunately, laziness has cascading detrimental effects and may have single-handedly killed Haskell from the inside. Looking at this 500-page book on GHC implementation, I searched for justifications for Haskell's graph reduction execution model. I found this quote, "Constructing an instance of the body of the abstraction with substitutions for occurrences of the formal parameter.
Unfortunately, this involved an inefficient traversal of the tree representing the body of the abstraction, and the presence of free variables made a more efficient implementation difficult." What a staggeringly convoluted and insane sounding statement. In case you ever wondered why Haskell has a reputation for impractical academic nonsense, then that would be this sort of thing. That sentence only makes sense to me if you want every parameter to be lazy and have decided to encode the program itself as a graph.
This idea I would never have allowed to leave the drawing board. Look, the tricky parts of evaluating lambda calculus are partial applications and higher order functions. But even that is still pretty close to the hardware and the way assembly code is written. Remember, an assembly function is an address containing instructions and finishing with a ret. To evaluate it, you prepare the input registers and call into it.
That pushes your current instruction pointer onto the stack and the ret instruction will read the stack to come back with a result. This is strikingly similar to evaluating a fully applied function in the lambda calculus. So, the basic strategy is free. Just don't do anything weird. Then it remains to find a way to deal with higher order partial applications. Long story short, uh GHC's strategy has aged badly, probably due to committing early to some severe handicaps.
And I think it never cared enough about performance. To highlight the historical insanity, GHC used to have a compilation pass called the evil mangler. That was a Perl script that did regular expression substitutions on the raw binary assembly. Yeah, it it rearranged closure info tables, optimized certain assembly patterns, and deleted function prologs and epilogs because GHC manages a custom stack. Now, it goes without saying that this is perhaps the most horrifying thing I have ever heard of in an industrial compiler.
Uh Uh it is obsolete now, but apparently still exists as an unregistered mode when porting GHC to new exotic platforms. As for the garbage collector, it is closely tied to the graph reduction stuff, so it cannot be disabled even when it shouldn't be needed. It is a generational copying collector optimized for laziness and allocation throughput. Last time I ran Cache Grind on a GHC program, it was spending some 99% of its time in the evacuate GC function.
Basically copying data around. Sometimes GC time is insignificant, and sometimes it dominates the profile. In all cases, I consider it suboptimal and ugly. At least though, it does handle memory for you and is certainly far more convenient than C, which basically punts off all problems to the programmer. But the GC is not good enough that I can happily forget about it. Speaking of low-level, functional languages should reign supreme.
You could write kernels, drivers, and memory manipulation safely and even optimally. Life down there is terrifying. It's one area that would benefit immensely from more type safety. I mean, there's no theoretical obstacle, but implementations so far have been either interpreted or forced a fat runtime with an obtuse execution model and no option to opt out. In Haskell, you can call malloc yourself and perform low-level operations like in C, but GHC will still drag in its undesired runtime.
Even when you avoid the tricky parts like recursive data structures or dynamically sized partial applications with weird lifetimes. Those don't appear in low-level manipulations anyway. So, the compiled quality is not great. Does it at least compile fast? No. GHC compilation times are almost as bad as Rust, which puts it in the trash tier of compiler speed. Most of the time is spent in the optimizer. However, GHC is saved by having an interpreter.
That thing is a fantastically useful tool for experimenting in development. It is responsive since it only type checks and reloads bytecode and thus nullifies the painful iteration time you'd always get with slow compilation. There is also a language server. Sadly, uh GHCi still can't interpret SIMD as of 2026, so I write my SIMD in C and load that via FFI. Although it is janky and you lose a bit of that precious maintainability.
So, to conclude, Haskell has the seeds of greatness. As a pioneer, dodgy experiments are par for the course, but the failure to recover and charter path for the future has been an expensive blunder and may have tainted functional programming generally. I'm particularly disappointed that functional languages never seem to take low-level performance seriously, which amounts to forfeiting their trump card. Idris 2 promised to be systems level, but the betrayal is now evident and the performance is worse than Haskell.
Next time, I will discuss the more flexible parts of Haskell, like lenses, monads, libraries, and standard practice. We are not done with this thing yet.
The words are the caption track's own and nothing is reworded or re-transcribed. Paragraph breaks are placed between sentences so the text reads as prose.
Free tools for your own script. No signup, no login.
Paste your draft and see where viewers are likely to drop off, with a rewrite for each weak line.
Paste the first 30 seconds of your own draft for a hook score and rewrites.
Check your draft against YouTube's advertiser-friendly guidelines before you record it.
Read this channel's public videos and transcripts, and download a writing brief for it.