6 ms·
Pedagogical Downsides of Haskell
- tome 3y agoI find the go pattern absurd. Which of these is easier to read: foldr k z = go where go [] = z go (y:ys) = y `k` go ys or foldr k z = foldr_k_z where foldr_k_z [] = z foldr_k_z (y:ys) = y `k` foldr_k_z ys
- gpderetta 3y agoThe first.
- tome 3y agoDo you have insight you can share into why you find it that way?
- chowells 3y agoI do. The name foldr_k_z doesn't say what the function is doing. It's just syntactic punning on a function call with two additional arguments. That's actually negative for comprehensibility. Names should be semantic, not syntactic. And that name doesn't say a thing about its meaning. The most it tells you is that it's related to foldr and its k and z parameters. But the details? Well you have to look for those. When you look at the definition, you discover that the it's the foldr worker that closes over k and z. You could name it foldrWorkerThatClosesOverKandZ, I suppose. But does that name contain any information that isn't present in the context? Does it help you actually understand anything? I'd argue "of course not". You already know that it's the foldr worker because it's a local recursive definition inside foldr. And you already know it closes over k and z because it uses them without defining them locally. Nothing in that name provides additional semantic value. You could still use it anyway, on the argument that a little redundancy can help aid reading. But the more Haskell code you read and write, the less that redundancy helps you with anything. On the other hand, the proliferation of names that contain almost no semantic content starts to drag on you. And so an idiom was developed for naming recursive workers that do the core job of what the parent's name promises: just name it "go". Nothing to think about. It's reduced down to a level that communicates exactly that it's not clever. It's just doing the thing it has to do. And it's standardized. If you see it, you know exactly what it's doing. There's no need to waste time mapping a new name into your existing set of well-known patterns. So... As to the original argument's point? I think it probably is awkward for pedagogy. But it's absolutely better for actively using the language.
- ghusbands 3y agoI think I prefer this: foldr _ z [] = z foldr k z (x:xs) = k x $ foldr k z xs
- tome 3y agoI suspect we all prefer that, but the point of abstracting out a closure that captures k and z is for performance.
- chowells 3y agoEh, the performance isn't from abstracting out a closure. It's from making the definition non-recursive so that it can be inlined. Then the compiler can see and inline the k and z parameters into the "go" block to eliminate indirect references. It's really all about inlining.
- tome 3y agoIf it was just about making it non recursive so it could be inlined then the following would be sufficient: foldr k z = foldr' k z where foldr' k z [] = z foldr' k z (y:ys) = y `k` foldr' k z ys That's obviously not sufficient, so it must have something to do with the nature of the closure. In this case I presume that it's because the closure captures k and z, although if you have any evidence to the contrary that would be interesting to see.
- chowells 3y agoThat's a reasonable question. It comes down to being transparent with the compiler. Not redefining k and z at every step is what allows their values to be inlined. You could make an argument about a sufficiently advanced compiler and partial evaluation, but the fact is that partial evaluation is far too slow to rely on for things you could just make explicit in the code instead. When the definition closes over the names, they trivially refer back to the same thing every time. So when the definition of go is in the same scope as what k and z refer to (which is usually the case after inlining foldr), k and z can be inlined into go. When this happens, note that it's actually no longer constructing a closure at runtime. It has essentially closed over the values at compile time, using some very trivial transformations. If you use a definition that is too complex for those trivial transformations, you're getting in the way of the compiler doing its job. I always prefer to write my code with sympathy for the compiler. The less magic it needs to do, the better it does its job.
- abecedarius 3y agoHow about 'folding'? I've settled on that kind of name for looping/recursing helper functions. Scheme has a bit of syntactic sugar called "named let" which makes this internal-helper pattern more concise/direct.
- mrkeen 3y agoBrilliant write up. > There is also a school of thought that you should start Haskell by teaching the IO monad first, but I am not convinced: in my experience, if someone gets exposed to IO early on, they will contaminate all their functions with IO. They will essentially end up writing Java in Haskell. I don't think this is such a bad starting place. Crawling before walking. Purifying an (unnecessarily-) IO function into an ordinary function is a good exercise. Trying to enforce non-IO from the start would be like enforcing 'no new keyword & factories only' in another language.
- kqr 3y agoI agree. Haskell is a really good imperative language if that's what you want to use it for. And allowing beginners to write actual meaningful programs is a huge pedagogical benefit.
- kirbyfan64sos 3y agoI feel like the hard part is that, if you dive in early on with imperative-style code, it's really easy to try and do everything else the imperative "style" too...until you can't, or you run into some weird behavior stemming from how IO works, at which point you just end up super confused. Starting without IO makes sure that you actually start to "get" how the language functions, so that once you jump into IO, the weird parts and how to mix it in with the logic written elsewhere makes a lot more sense.
- kitd 3y agoOne of the reasons I liked the Haskell Wikibook [1] when trying to learn Haskell was that it didn't concern the reader with the IO monad until much later. It just presented 2 forms of using the language, a) normal functional style, b) an "imperative" "do" style, and then showed how they could be used together and when. That was enough to do most basic tasks and only later was it explained why they can't be mixed directly. [1] https://en.wikibooks.org/wiki/Haskell https://en.wikibooks.org/wiki/Haskell
- danidiaz 3y ago> Purifying an (unnecessarily-) IO function into an ordinary function is a good exercise. Agree! And I would add that you can "purify" a monadic function without having to rewrite it in non-monadic style. You can make it polymorphic over all monads and relegate the "impurity" to monadic functions that you pass as arguments/dependencies. A trivial example: twice :: IO () twice = do putStrLn "foo" putStrLn "foo" twice' :: forall m. m () -> m () twice' action = do action action This is not that different to having a Spring bean that doesn't perform any effect directly—say, a direct invocation to "Instant.now()"—but instead receives a "Clock" object through dependency injection. Haskell lets you express the idea of "program logic that only has effects through its dependencies" by being polymorphic over all monads.
- jy14898 3y agoPureScript might be worth considering, a few of the downsides listed here aren't in PS, for example: Int/Number primitives aren't overloaded, strict evaluation, the various tools like package management are easy, explicit Prelude means you are free to import foldl from Array for example. Of course PureScript has it's own downsides not apparent in GHC
- Laaas 3y agoPureScript also has the huge advantage that it's trivial to build "something". When teaching Haskell, I'm never sure what to build as an example. CLI tools aren't attractive, making a webserver is complex, and so is making a native UI. Of course you can use GHCJS, but at that point, why not just teach PureScript in the first place?
- tome 3y agoOr use gloss and make "something" arguably more easily than in PureScript? https://hackage.haskell.org/package/gloss https://hackage.haskell.org/package/gloss
- asplake 3y agoPoint 11 surprised me. Not the “go” thing but the “where” syntax – I wish more languages had it!
- deleted 3y ago[deleted]
- wnoise 3y agoYes. Where is often lovely -- I want to delegate details, and not think about them yet, but keep that delegation scoped to the function that needs the relevant details. But calling auxiliary functions "go" is almost always bad naming.
- chowells 3y ago"go" is a fantastic name for communicating that all you're doing is exactly what the containing named definition promises. It's a lot better than adding "Worker" or "Impl" as a suffix of the same name as the parent. It contains no additional information because there's no additional information to contain - the parent name already says it all. So you might as well make it short and a standard idiom.
- wnoise 3y agoYou're not doing what the parent definition promises though -- if you were you'd leave out the parent definition and the where, and just write the go definition with the true name. Go is instead doing something similar to the parent that is easily transformed to the right thing (i.e. accumulated in reverse or something), or more general that does the right thing when called with specific arguments. Communicating how and why the function does what it does and works in conjunction with the top level wrapper actually matters.
- chowells 3y agoYes, you're doing what the parent promises. You're setting up some initial values for internal accumulators and closing over values that don't change in preparation for the loop. Then maybe you do a bit of cleanup after the loop. But it's no more interesting than a "for" or "while" loop that takes up most of the body of a function in C or Java. People don't demand descriptive names for those, because they realize such a name would contain no useful information. That's equally true in functional programming.
- 0zemp1c 3y ago[flagged]
- tome 3y agoCan you give an example of a Haskell way that is not Right?
- TylerE 3y agoLazy by default, to name just one. Ocaml's immediate by default but opt-in lazy is so much better.
- tome 3y agoYup, will definitely accept that one.
- 0zemp1c 3y ago[flagged]
- consilient 3y agoTypeclasses. They're a huge improvement over OO-style overloading, but using them as your default way of talking about structures is a Faustian bargain: - Implementing two instances of one member each is much more work than one instance with two members, which incentivizes overly coarse typeclass hierarchies: hence Num and friends, which are a disaster. - They don't provide a good way to talk about one structure uniquely determining another (e.g. a given semigroup can be made a monoid in at most one way), which leads to boilerplate overly pessimistic constraint resolution. - The way constraint resolution works forces them to take type constructors, not types - which makes aliases, type families, etc. second class citizens.
- javajosh 3y agoThe pedagogical downside of Haskell is that it ignores the physical reality of the machine. Physically, a computer is imperative, has mutating state, and is filled with all kinds of possible race conditions. Even after you apply the operating system, allowing processes to live together (and giving you space to define new ones), very few constraints are placed on your program and process space. Instead of building on this reality, Haskell asserts that the starting point is not physical reality, but rather a mathematical formalism called "The Lambda Calculus", the physical machine is looked at with disdain and pity, its limitations to be worked around to provide the one true abstraction. This is the original sin of Haskell, because it is an attitude that isn't driven by a need to make a thing, but aesthetics and a peculiar intellectual dogma around building that ultimately becomes a stumbling block. In my view, you have to respect the machine. Abstractions can be beautiful, but they are ephemeral, changeable, unreal. The danger is that these illusions become a siren song to makers who are always looking for better tools, and to these makers the abstractions become realer than the machine. Haskell's power users famously don't actually make anything with it (modulo pandoc and jekyll), and my guess is because either they find that 90% of real-world things you want to do are "ugly" from Haskell's point of view, and so are left as distasteful "exercises for the reader", or they get so distracted by the beauty of their tools they never finish. In any event, Haskell is a road less traveled for good reason.
- turboponyy 3y agoScrew the machine. As long as you can transform one formalism to another, why encumber the human mind with needlessly complicated ones?
- kqr 3y agoI find so many things about this line of reasoning wrong that I don't know where to start. So let's just pick one thing: Haskell does not ignore the physical reality of the machine. It's one of few languages that explicitly recognise it. There are more facilities in Haskell to deal with this reality than in almost any other language you can think of.
- javajosh 3y ago
- iamnotsure 3y agoHad good experience at https://exercism.org/tracks/haskell https://exercism.org/tracks/haskell I don't think this article is helpful for beginners.
- burnished 3y agoI think this article's audience is teachers of beginners, not beginners themselves. At least the author is writing about their experience as a teacher. Don't know why you thought it would be an article for beginners, but good on you for linking a resource regardless.
- deleted 3y ago[deleted]
- iamnotsure 3y agoThe article is an introduction to the basic concepts of Haskell, thus beginners may be considered a target audience. However, the style and the content brings to my mind the dreaded monad tutorials. I'm not convinced the article is about pedagogical downsides of specifically Haskell. It mostly reads like a collection of random purported gotchas/differences from someone with experience with other languages.
- burnished 3y agoI'm taking my cue from the title of the article and the intro - seems pretty certain
- yamtaddle 3y agoI find its syntax & idiomatic style incredibly difficult to follow, in a way nearly no other languages have been for me, including some functional languages (OCaml doesn't seem nearly as bad to me, for instance). It's sometimes implied that those who trip over Haskell just aren't big-brained enough to understand various important concepts related to it, but I've found they're usually very easy to grasp, provided the explanation's not using Haskell examples. If all programming were Haskell, I probably never would have become a programmer in the first place. Would have taken me too long to figure any of it out, probably would have concluded I wasn't smart enough to be a programmer. I do wonder if there are some shared experiences or common patterns to who tends to love Haskell, and those who don't. I also feel nigh-dyslexic trying to read math formulas. Human language and broadly C-family programming languages, on the other hand, seemed easy and natural to me, almost effortless to pick up. Wonder if there's a "mathy"-person versus "languagey"-person divide on finding Haskell legible. I'm not sure it's the whole thing, but I think I've also figured out that I find algorithm-type reasoning far easier to follow and work with than equations or proofs. Like, the only way I can begin to get traction with an unfamiliar equation is to break down what each term and operation "does" to something "moving through" it—it's tedious as hell. Might be something there.
- alpaca128 3y agoI'm in the exact same boat. Haskell code feels more like abstract maths and I feel more at home when I can just easily track the data flow. The language and community uses relatively abstract terminology due to its roots and it's just a bit too cryptic to me. Though I'm glad newer languages are starting to adopt more features from the functional territory for the situations where it just makes more sense.
- Hirrolot 3y agoI find the terminology that Haskell uses quite misleading for software engineering. It borrows concepts from category theory with quaint names such as a "monad", "endofunctor", "catamorphism", etc. The problem is that, instead of a "monad", we can say "brrrdogcogfog" and nothing will change -- the name is absolutely irrelevant to the problem being solved. Given that a monad is an interface for sequential computation, a much better name would be something like "Seq", "SeqComp", or something like that.
- agentultra 3y agoI wonder if there could be a (or already is) a "teaching" Prelude designed for this purpose. One of the reasons the standard Prelude includes partial functions and specialize versions of `map` and `filter` is to support the pedagogical use-case (as far as I understand the situation). Most production applications will use a custom Prelude of some kind in order to prevent programmers from using foot-guns like `head` or make things more general in the case of `map` and `filter`. Turns out using linked-lists for everything isn't the best idea but a lot of Haskell applications will use them because it's in Prelude. Bit of a balancing act supporting both use cases.
- jerf 3y agoI don't know if there is one already, because the Haskell community generally heads in the other direction with its alternate Preludes. But the effort to fix up the fixable issues mentioned in the post is about the same as writing the post was. Getting it distributed to the students may be a bit harder, depending on the local setup. But it's definitely fixable with Haskell as it is today. Linked lists are particularly tricky in Haskell, because as a data structure manifested in memory, they really stink. But as a lazy data structure traversed exactly once and thus just serving as a mechanism for providing "the next thunk", they're fine. Haskell and its laziness completely conflates the two of these, so it ends up being easy to think you have one and end up with the other.
- agentultra 3y agoDefinitely. Linked lists are great for pedagogy and useful in many applications. I think it’s a bit of a sign that the struggle between pedagogy and practice can lead to suboptimal outcomes for both parties.
- tikhonj 3y agoCode World[1] is a great project that addresses a number of the problems from the article, with an eye towards using Haskell to teach children basic math and programming simultaneously. Code World directly addresses a number of the obstacles outlined in this article: 1. Using an online editor with a rich built-in library removes any toolchain problems. 2. A custom standard library simplifies pedagogically unnecessary details like Foldable 3. The custom standard library also avoids currying (f(a, b) for functions rather than f a b) 4. Custom error messages improve the feedback students get from the compiler I would highly recommend Code World to anybody looking to teach programming with Haskell. If you want to teach Haskell in a way that fits the existing ecosystem, it's also possible to run Code World without the custom standard library[2]. [1]: https://code.world/# https://code.world/# [2]: https://code.world/haskell# https://code.world/haskell#
- FpUser 3y agoI generally trying to avoid single paradigm languages that are trying to show me the one and only "true" way. I see no business benefits coming of of their use.
- yakshaving_jgt 3y agoI think Elm is second to none as a tool for learning FP. It compiles quickly, the guidance offered in error messages are best in class, it's small, and the mental model is consistent. In fact I think it's far easier to learn Elm (and also perhaps web UI development wouldn't be such a shitshow if programmers earlier in their career used Elm to build their mental model) than it is to learn: - React - Redux - Immutable.js - Lodash/Ramda - ES${CURRENT_YEAR} - Webpack/Parcel/Grunt/Groan/Whatever - etc… I've seen so many early programmers go through some React course thinking they've learned FP, and yet struggle to solve basic problems by applying functions to values.
- mprovost 3y agoThis is great and a lot of it rings true to my experience writing a book to teach Rust. It's basically a giant topological sorting exercise to find the optimal order to introduce syntax so that you steer clear of rabbit holes. Or you just end up drawing the owl. For example, to implement a simple "hello world" program in Rust you have to use a macro (println!), so you can't even look for a function signature in the standard library docs to help. So you can either just say "don't worry about this for now, just trust me" or spend a whole chapter diving into macro syntax. The number of concepts you need to implement a basic program is pretty large and you could easily spend a chapter going into any of them. Personally I'm not a fan of the approach in this post to just "lie" to people but I do find myself showing a non-optimal implementation because that's all the syntax I've introduced up to that point. Then later I show how to do it better. I know some readers just want the final answer up front though.
- glynnormington 3y agoI provide a dependency diagram so students can work out where to apply most effort and how to catch up if they miss something. I also show likely dependencies from the course assessment to the various topics. For instance, there is a strong dependency on the IO monad, but a weaker/optional dependency on (general) monads. In terms of presentation order, I tend to over-simplify early in the course and circle back and make things more precise later. (I'm teaching a 2nd year university course on Functional Programming with Haskell for the first time, so I found the OP fascinating. Thanks!)
- justincredible 3y ago[dead]
- lincpa 3y ago[dead]
- deafpolygon 3y agoFor someone not familiar with functional programming (but familiar with OOP/procedural), this was not easy or intuitive for me to follow.