What Is Good About Haskell?
doisinkidney.com
doisinkidney.com
Most of my daily job goes into gluing services (API endpoints to databases or other services, some business logic in the middle). I don't need to see yet another exposition of how to do algorithmic tasks. Haven't seen one of those since doing my BSc. Show me the tools available to write a daemon, an http server, API endpoints, ORM-type things and you will have provided me with tools to tackle what I do. I'll never write a binary tree or search or a linked list at work.
If you want to convince me, show me what I need to know to do what I do.
My company does everything in Haskell, but it's almost all just boring plumbing code. APIs, JSON, databases, HTTP stuff, HTML templating, etc. It works great.
https://news.ycombinator.com/item?id=21024494
As for libraries we use heavily (in no particular order): Yesod, Persistent, Esqueleto, Lens, Lens-Aeson, Hedis, STM, Wreq, Shakespeare, Hspec.
And also to point to this excellent article: https://pchiusano.github.io/2017-01-20/why-not-haskell.html
Until you’ve done Windows programming for a while, you may think that Win32 is just a library, like any other library, you’ll read the book and learn it and call it when you need to. You might think that basic programming, say, your expert C++ skills, are the 90% and all the APIs are the 10% fluff you can catch up on in a few weeks. To these people I humbly suggest: times have changed. The ratio has reversed.
Read the whole article, it's pretty amazing. Parts of it are just as relevant to mobile development today as to desktop development back then.
I use
http://hackage.haskell.org/package/optparse-applicative for small cli apps
http://hackage.haskell.org/package/envparse For any Docker microservice
http://hackage.haskell.org/package/aeson-1.4.5.0/docs/Data-A... For all JSON work. Sometimes I’ll use it with lenses which is massively powerful but a rabbit hole
I’ll use http://hackage.haskell.org/package/stm when dealing with parallel execution
https://github.com/brendanhay/amazonka For anything dealing with AWS
https://github.com/haskell-works?tab=repositories Projects for Kafka and avro
http://hackage.haskell.org/package/warp For trivial micro services or Scotty if more than a few endpoints
http://hackage.haskell.org/package/persistent For dealing with Postgres
http://hackage.haskell.org/package/parsec For dealing with any text parsing.
The tools are available, they can make things like cli apps and micro services trivial. However if you have never used a ML language before you will have a steep learning curve as very different to C style/based languages.
I was once of the opinion Haskell is academic, what can you use it for in the real world. Then I studied with it, played with it admittedly on and off over 1-2 years, hit hurdles where I had to think as so different to what I’ve learnt before. Eventually it clicked, it’s very hard and frustrating now in my day job using typical enterprise or popular languages. It’s not about convincing, it’s about having a open mind and wanting to learn something different
I'm fairly convinced that Haskell is good at preventing the kinds of bugs which you might run into writing, say, a parser or other kinds of very complex logical code, but I'm less convinced that the nature of the language helps with the kinds of issues you get hooking together APIs, databases, etc.
That's the only one occurrence I can find. I think the GGP made a good job of answering that.
I'm not going to walk over the differences as each of those would be a many hours task just for explaining.
Instead, the next high-profile HN Haskell link will be the thousandth demo/intro/tutorial that yet again implements some core data structure.
Rather than showing how Haskell is great for working with databases.
Or working with Kafka.
Or integrating with AWS.
Or parsing text.
Or running microservices.
Or any of the other hundred things that I'm going to do a dozen times before I need to reimplement binary trees.
data Value
= Object (HashMap Text Value)
| Array (Vector Value)
| String Text
| Number Scientific
| Bool Bool
| Null
[0] http://hackage.haskell.org/package/text-1.2.3.1/docs/Data-Te...[1] http://hackage.haskell.org/package/scientific-0.3.6.2/docs/D...
All of which tend to introduce monads.
When you have multiple moands on the go you need some combining them.
For example, pattern matching against static types is cool, but, compared to pattern matching directly against data, Clojure-style, is even cooler. One makes the code a bit more concise and readable, but not necessarily a whole lot more maintainable. The other takes one of the more annoying and error-prone portions of my (say) Java code and renders it far more manageable.
There's a recent LispCast that talks about this a bit: https://lispcast.com/what-is-data-orientation/
If you want to use clojure, then go for it. Use what you want to use.
Here's where I come down on it:
There are some kinds of projects where you can cut off most potential problems at the pass with compile time checks. In those cases, yes, you absolutely want to statically render as many errors as possible impossible. Compilers come to mind as a shining example here.
There are others where the nastiest bits invariably happen at run time, though. And, for a significant number of those, the grottiest bits fall under the general category of "type checking" - not checking types in the structure of the code itself, per se, but checking types in the actual data you're being fed. And, since you don't get fed data until run time, that means all that type checking has to be done at run time. There's no sooner time at which it's possible. There's some tipping point where that becomes such a large portion of your data integrity concerns that it's just easier to delay all your type checking until run time, so that you are dealing with these things in a single, clear, consistent way. If you try to handle it in two places, there's always going to be a crack between them for things to slip through.
I think Haskell is an excellent language for servers and API's. It really excels as a backend language. So, I'm sorry you think Haskell is only good for compilers, but I think the range of use cases it's good at is much broader than "Compilers".
Haskell is best thought of as a better Java. I wouldn't select it for every problem, but server API's and backend work is a really good fit.
Also I think Clojure is great. We can both co-exist in this world though. It is possible.
It's unfortunate the OP picked on python - it's not the style of post that I would write.
But I also believe another very important thing: There is no silver bullet.
Because I believe that, I am able to recognize that even the things I love have some limitations. And I don't believe that this should be a fight, and that is why I think I should be able to articulate what I have found to be the limitations of a tool, and acknowledge that some other tool that other people like might have something to offer in this area. Without being perceived as a hater for doing so.
> Brooks distinguishes between two different types of complexity: accidental complexity and essential complexity. Accidental complexity relates to problems which engineers create and can fix; for example, the details of writing and optimizing assembly code or the delays caused by batch processing. Essential complexity is caused by the problem to be solved, and nothing can remove it; if users want a program to do 30 different things, then those 30 things are essential and the program must do those 30 different things.
If I'm solving a problem for myself, if it breaks in 'prod' then "Ooops", I try and avoid that, if I'm expecting other people to use it I will document public interfaces and write unit tests, but my focus is on scratching my own itch, not getting paid.
If I'm writing code that 1000s of peoples lively hoods, or millions of users buying decisions are being made on the stakes are higher. I might decide to use a more rigid language like Java, because the chances that I'm going to be given the freedom to replace rather than repair classes is slim. Similarly, if I persuade a client that microservices are the way forward, I'm going to spend significant time making sure we have a monorepo so each service has the same time line, an automatic deployment pipeline and I'm going to want to be able to defend my technical choices with economically sound data... and thats where Hickey kicks in. Many of the economically sound data sets are actually vapour.
* Bugs are typically caused by misunderstandings of requirements or something odd about the interactions between different systems, and rarely about internal logic.
* Where quick is often better than proven correct.
* Where requirements are in constant flux and where a lot of code is tossed out because it was the result of a failed experiment or an unwanted feature.
The part of the business applications I work on that's a problem is dealing with the outside world. The data is messy. It's inconsistent. The protocols I'm using to communicate are invariably something horrendously loosey-goosey like JSON or XML. Stuff like that. And so, an inordinate amount of time I spend doing business applications in static languages ends up being spent on taking the messy, messy outside world and trying to create a clean, well-typed, rigorous façade for it so that I can operate on it inside my blissfully statically typed fantasy world. And all that static typing never seems to save me in practice, because the software quality problems I run into almost never crop up in the bits that I can operate on in a Haskell-friendly way. It's invariably in some mismatch between the outside world and my domain model that I failed to deal with accurately, which means that it's in my mapping code. Worse, oftentimes it's because of my mapping code, because my statically typed domain model ends up accidentally placing requirements on the input that aren't strictly needed by the business logic; I just unwittingly introduced them in the course of my efforts to get the types to line up.
In most the systems I work in, null permeates both the input and the output, and can often even have its own semantic value. i.e., "there is a value for this key, and that value is null" might actually be semantically distinct from "there is no value for this key". . . it's gross, but it happens, and whether or not it happens is often outside my control.
And I am beginning to suspect that it's actually safer, and even simpler, to fully live in and be fully aware of the reality of the business domain I'm working in, than it is to try and live in a bubble and pretend that life isn't complicated. And I'm also beginning to think that it might be wise to mind my own business. . . which includes not worrying about whether or not a value is null unless and until I find that I need to care whether or not it is null. Rejecting input because a value wasn't set when I had no intention of even looking at it is just such a grave violation of Postel's law. If I find that I'm only doing it because I need to satisfy the type checker. . . seems like a foolish consistency to me.
Perhaps if I could live in an alternate reality where things like JSON and MongoDB hadn't happened, and we instead decided that clean and consistent data is every bit as important when sitting on magnetic disks or traveling through fiber optic cables as it is when bouncing around in silicon. Oh, that would be wonderful. I dream to live in that world. But that doesn't seem to be the reality I occupy.
The systems I write also have to deal with JSON with nullable fields, and with fields I ignore while parsing. Aeson for instance gives you complete control over how strict or lenient you wish to be when trying to parse data.
The idea I was trying to convey was that if you care about marshalling data into types a la Haskell, then you can code less defensively when writing code for the data you actually care about. You do that defensive validation in just one place, as opposed to sprinkling nil checks all throughout your system.
If I'm understanding your system correctly, it sounds like you have unreliable input, and you want the same output, only with some of the fields updated if they're there. Haskell readily lets you do this too. The lens-aeson library is perfect for this.
Lots of examples for that here: https://www.snoyman.com/blog/2017/05/playing-with-lens-aeson
Also, I am not sure how you differentiate between a null value and no value, but whatever mechanism you're using you could also use to model those two different types of null as actual types in Haskell.
> Oh, that would be wonderful. I dream to live in that world. But that doesn't seem to be the reality I occupy.
It's hard[er] to discern tone through the medium of text, but it sounds like you're suggesting Haskellers live in some fantasy world where all the data is perfect and everything is pure.
I don't really understand that perspective. I make 100% of my income from writing online business software in Haskell. I employ other programmers to write Haskell for me too. We live in the same world you live in, but we might just approach it differently.
That happens. Surely, if you know about this in advance, you can use a type along the lines of
data Field = Field Int | Null | Empty
And if you don’t have this kind of knowledge, well... That’s just a problem waiting for the right time to surface, whether you are using Haskell, Clojure or whatever else.
> And I am beginning to suspect that it's actually safer, and even simpler, to fully live in and be fully aware of the reality of the business domain I'm working in, than it is to try and live in a bubble and pretend that life isn't complicated.
I would’t say Haskell forces you to live in the bubble. Haskell forces you to think, in advance, about the relations between fields and types, sure. It doesn’t force you to use only simple, bubble-y types, though; the types can be something general, or some abomination (like the one above). I’m not aware of a use case where I wouldn’t be able to say “this will always be something”.
The only major distinction is, I would say, the place in code where you deal with the types. Clojure: In the functions all over the place (-), and for some fields, never (+). Haskell: Always in the topmost layer of your app (+), but you have to deal with all of them (-).
That’s the basic tradeoff between those two languages. Which pros and cons are more important depends heavily on your use case.
Is agree.
Of course, if you can't describe the values in a business domain in types, I'd argue you aren't fully aware of it's reality. And when you've done that modelling, you aren't only aware of the reality, but you've encoded it so that the knowledge is preserved not only for operational use in the program, but also for anyone who reads the program.
Anecdotal evidence: I recently had to turbo-build a tool to generate statistics over five-digit numbers of very complex business objects (including dates, strings, IDs, boolean as whatever string, ill-named columns) scattered in a number of systems (some web apis plus some CSV extracts from you don't really-know where). Using 'raw structures' like Aeson's JSON AST with lenses was more than good enough. Lenses have typically solved the "dynamic / cumbersone shape". Then I had to create a CSV extratc with like 50 boolean fields, reaching to intermediate techniques like type-level symbols allowed to really cheaply ensure I was not mixing two lines when adding a new column. I could even reach to Haxl from Facebook to write a layer of "querying" over my many heterogeneous source to automatically make concurrent and cached fetches for it.
The main difficulty in this setup is to keep the RAM usage under control because of two things. On the one hand, AST representations are costly. On the other hand, concurrency and caching means more work done in the same memory space.
Overall, got the data on time at relatively low effort (really low compared to previous attempt - to a point that some people with mileage at my company thought it would be impossible to build timely). Pretty good experience, would recommend to a friend.
In one of his recent talks he made the claim that `Either a b` is not associative which.. well it is since it's provably isomorphic to logical disjunction which _is_ associative.
I thought what he might be looking for are _variant_ types which are possible to implement in Haskell but are a bit complicated for reasons. There are libraries for it or you can try languages like Purescript or dependently typed languages like Idris, Agda, or Lean.
Regardless I don't find his particular brand of vitriol appealing. If he doesn't really have a lot of experience working with Haskell like type systems, why does he feel the need to have an opinion about them?
To be fair I used to have a lot of the opinions pointed out in the article and reflected in many of the comments here. An old blog post of mine [0] muses on the utility of static types. I was seriously into Common Lisp at the time.
The problem with past me then was that I hadn't taken the time to learn and understand Haskell to form an opinion. I had learned Common Lisp out of frustration to win an argument that it as an old, crusty language that nobody used or needed anymore... and lost. I hadn't done the same yet for Haskell and would join the chorus of people repeating things like, "Haskell is an academic language but is not pragmatic for real-world use." It's embarrassing looking back on it.
I've learned enough Haskell in recent years to ship a feature into a production environment and teach a small group of people to hack on it. It's pretty great and I much prefer working with it than I do with weakly-typed or dynamically typed languages. The amount of work I can do with the amount of effort I have to put in has a great ratio in Haskell. The initial learning curve to get there is hard but it's worth it in the end.
Maybe it's cool, but I think static types can be "cool" too. When you take the fashion statement out of the question, what are you left with? One is polymorphism at runtime and the other is at compile time. I'll take the compile time one.
Here's an article I read a while back (from the same author it turns out!) which nearly converted me to the dynamic-types camp: https://lispcast.com/clojure-and-types/
That observation on `Maybe` really hit home in a big way. Not at first. Eventually. I used to think that banning null and using `Maybe` instead was the best idea ever. I still love the basic idea, and wish I could always work that way. . . but nowadays I'm so frequently working in the limit case, where everying is either optional, or used to be optional, or will be optional in the future, or is officially required but somebody didn't get the memo. And it's like in some Zen parable where the student keeps getting hit with a stick until they're enlightened. Bruised, bloodied, and enlightened. You either have it or you don't. む.
Cycorp? Do you need more Lisp developers? I'm trying to switch jobs.
And every non-trivial program I've worked on is 90% of a compiler. (You could describe compilers as just "some business logic in the middle", too, if you were in Architecture Astronaut mode.) You don't think HTTP servers use "pattern matching"? You don't think API endpoints would benefit from "ridiculously easy" testing? This is your bread and butter.
This article is showing how to implement if-statements and null-checks in Haskell, and reduce your code size by half. I bet you have some of those in your software. I'm not sure how this could be much more relevant, without reducing its usefulness by being overly specific.
In the real world, software has a CS "core", plus 95% boring data copying and gluing APIs together, where availability of libs and tools is far more important than theoretical correctness properties and the most general abstraction possible. This is why Haskell looks so nice in blog posts but terrible in a production system.
In Haskell, updating fields in a records is still an active area research.
In Haskell, everything is still an active area of research. Mutable data already exists in Haskell and has sensible semantics.
But the goal of Haskell (so far as I can see, at least) is good, reliable, maintainable code which produces good, reliable, maintainable programs. If this is not what you look for in a programming language then it might not be for you.
https://hackage.haskell.org/package/DataVersion-0.1.0.0/docs...
Isn't "correctness" a useful property, even for a web service? I think that being able to eliminate entire categories of bugs is terrific, in any situation.
Sure, it's always nicer to have good libs/tools than to have to write your own (though that distinction is much less important when your language has great abstraction capabilities). Are there any libs/tools you're missing in Haskell? The way your comment is phrased makes it sound like good old fashioned FUD.
I'm not sure what you mean by the last sentence. It seems to still be an active area of research all over the place. Look at Swift/Rust/Go/Java, or Postgres/Mongo/Datomic, or ext4/zfs/btrfs/ntfs. Everybody updates records in very different ways. It's not like all Algol-derived languages have the same data model.
If you want to learn statically typed functional programming learn Elm (which takes only a few days), then one of F# or OCaml.
If you want to learn dynamically typed Functional Programming, learn Clojure or Racket/Scheme.
They amount of investment it takes to see return on learning Haskell makes it terrible for an introductory language. And every proponent of it glosses over this part. It has some advanced concepts but its not an introductory language.
There is so much benefit you can get to your coding from learning FP that you should pick a language that allows you to see and judge that value prop on your own quickly, not have to invest so much time to be haskell proficient to try to get the return on your learning.
The only thing worse in my book is FP evangelists. FP has some cool ideas and is an interestingly different way to do things compared with imperative programming, but enough exposure to the “FP is obviously the best paradigm and anybody who isn’t a fanatic is clearly just unenlightened” crowd will sour anybody on it.
In general terms, almost everything you and I do is either a CRUD app, or something overcomplicated which would be better-implemented as a CRUD app. There's not technological advancement happening here. And usually you're not even doing a new CRUD app, you're just reimplementing an earlier CRUD app with better CSS and JS and a different marketing team to tout it. IF there's an innovation in your company it's not the CRUD app developer who's innovating. We're just reimplementing the wheel over and over, because the other wheel implementations are closed source and owned by a competitor.
If you want to innovate, you have to take on harder problems that aren't CRUD apps. That's where languages like Haskell shine. Haskell doesn't shine because it's better, it shines because it's different, and suited for different tasks. The tasks for which Haskell is suited haven't been saturated yet, so there's still room for innovating on the technical side of things.
So yeah, I can't show you how to do what you do with Haskell--the reason you'd want to use Haskell in the first place is to do something different from what you (and most other developers, myself included) are doing. The reason you'd want to write Haskell is to solve technical problems which haven't already been solved.
You're right to bring up binary search trees and linked lists as criticisms of the Haskell community, because those are also pretty solved areas: touting binary search and linked lists as the powers of Haskell completely undersells Haskell. Haskell learning materials fall into two basic categories: complete beginner stuff, and Ph.D. theses written in an alien language. This is, unfortunately, part of why there's any innovation to be had here: having very little mid-level learning material creates a barrier for entry that keeps people from reaching a level where they could innovate. The same is true for the communities of many less-widely-used languages.
This has been a rather cynical post, I realize. I'm not sure there's any recommendations here: I'm certainly not going all-in on learning Haskell and using it to innovate, myself. CRUD apps pay the bills and innovation is risky. I find interest and novelty in other areas of my life.
I wrote the article a while ago after being frustrated using a bunch of Go and Python at an internship. Often I really wanted simple algebraic data types and pattern-matching, but when I looked up why Go didn't have them I saw a lot of justifications that amounted to "functional features are too complex and we're making a simple language. Haskell is notoriously complex". In my opinion, the `res, err := fun(); if err != nil` (for example) pattern was much more complex than the alternative with pattern-matching. So I wanted to write an article demonstrating that, while Haskell has a lot of out-there stuff in it, there's a bunch of simple ideas which really shouldn't be missing from any modern general-purpose language.
As to why I used a binary tree as the example, I thought it was pretty self-contained, and I find skew heaps quite interesting.
This is a true statement. (Opinion yada objective yada experience yada)
> In my opinion, the `res, err := fun(); if err != nil` (for example) pattern was much more complex than the alternative with pattern-matching.
This is also a true statement. (yada yada)
The insight I think you're missing is this piece right here: `we're making a simple language`. Their goal is not necessarily to make simple application code. That's your job, and you start that process by selecting your tools.
For certain tasks, pattern matching is a godsend. I'm usually very happy to have it available to me when it is. And I do often curse not having it available in other languages to be honest.
But Go users typically have different criteria for what makes simple/reliable/maintainable/debugable/"good" code than Haskell users have. Which is why the two languages are selected by different groups of people handling different tasks. You're making a tradeoff between features and limitations of various languages.
And the language designers have an even different criteria for those things. In this case, adding pattern matching would absolutely make the language itself more complex, and they apparently don't believe that language complexity is worth the benefits of pattern matching. I think that's a perfectly reasonable stance to take.
I get that there's a tradeoff with including certain features, I suppose I disagree that the tradeoff is a negative one when it comes to things as simple as pattern-matching, and I think it should be included in languages like Go.
They're not saying that if err != nil is better or worse, simpler or more complex, etc... than pattern matching for application code.
They're saying that supporting pattern matching makes the language itself more complex, and they're not in favor of that tradeoff. You're focusing on application complexity, and that's a very different thing.
Both the go language authors as well as the kind of developers that choose to use go think of the relative simplicity of the language itself as a feature. Even if it causes the application code to be slightly more complex. It's just another dimension that can be used when comparing programming languages, and one that group tends to value more than other groups.
For instance, and amusingly enough written in golang, one of the most respected recent books on this topic is `Writing an Interpreter in Go` and its sequel `Writing a Compiler in Go`. https://interpreterbook.com/ and https://compilerbook.com/ Both of these books are reasonably short, and have the reader make meaningful progress within a weekend.
Going through the motions of actually making your own programming language (or reimplementing an existing one) teaches you a lot of things you wouldn't otherwise expect about how to write general code, how to use existing languages effectively, and how things work under the hood. It's also one of the best ways to really get a practical feel for how to approach unit testing.
It's an exercise I'd recommend if you haven't gone through it already. It might make it really click for you why some features that seem like a no brainer and should be in every language aren't, and why some undesirable "features" are so prevalent.
Is 'recent' the key word here? ;) cause that is a very bold claim to make.
Another exercise, perhaps less demanding in this regard, is to explore using Free Monads[0] to implement an EDSL[1] for a problem domain. Of course, the approachability of this varies based on the person involved.
> For instance, and amusingly enough written in golang, one of the most respected recent books on this topic is `Writing an Interpreter in Go` and its sequel `Writing a Compiler in Go`.
Queue obligatory reference to "the dragon book":
Compilers: Principles, Techniques, and Tools[2]
0 - https://softwareengineering.stackexchange.com/questions/2427...1 - https://www.quora.com/What-is-an-embedded-domain-specific-la...
But I know there's a recency bias when people are evaluating tech books, so if there's a good book from the last five years I'll recommend that over a great book from the last 15, just so there's a higher chance of the recommendation actually being used.
I mentioned the dragon book by obligation, not in comparison to the works you referenced.
I hate this kind of "I have secret knowledge, why don't you spend T amount of your time on some big project to maybe come to the same secret insights I mean". If you have an opinion on why pattern matching is so complex and undesirable, just come out and say it please. Otherwise I'll just call you out as not really having an argument.
Alternate interpretation, I learned something valuable from doing this thing, perhaps you'd be interested in doing so as well since the book that took months or years to write will do a better job teaching it than I will in a five minute break while typing on HN.
It's always impressive when freely sharing knowledge and tips is somehow taken as being insular and exclusive.
> If you have an opinion on why pattern matching is so complex and undesirable
Where did I say pattern matching is undesirable? It sounds more like you just want a fight here.
Remember the HN guidelines:
> Please respond to the strongest plausible interpretation of what someone says, not a weaker one that's easier to criticize. Assume good faith.
But you didn't share knowledge. You suggested that you had knowledge that was pertinent to the topic at hand. But you didn't share it. You did share tips for resources where one can learn more, and that's great. But you didn't add something like "... and that's where I learned that pattern matching is undesirable because <technical reason>".
> Where did I say pattern matching is undesirable?
This whole thread was about you saying that pattern matching was undesirable from the point of view of Go's designers or implementors due to their design goal of simplicity. Then you mentioned those compiler resources. The only reasonable interperetation for me is that you wanted to say that you did indeed know concrete technical reasons why pattern matching in Go would be complicated and therefore undesirable.
The only use of "undesirable" in any of my comments was in regard to features that are prevalent across languages today. If you must know I was thinking of inheritance and exceptions specifically.
As far as pattern matching goes, I was making no arguments except to say that I like it, adding a feature like pattern matching adds some non-zero amount of complexity, and that the go authors are apparently uncomfortable with that complexity. As I am not a go author, I am unsure of their exact reasoning and would not think to say why they believe that. My implication was not that I have an exact concrete reason for why the go authors feel the way they do. It was merely that I don't inherently disbelieve them when they say they have a reason.
In fact my exact wording was "I think that's a perfectly reasonable stance to take", which does not imply agreement, only a lack of strong disagreement. In other words I don't think they're ignorant of the matter or misrepresenting the situation.
> But you didn't share knowledge. You suggested that you had knowledge that was pertinent to the topic at hand. But you didn't share it. You did share tips for resources where one can learn more, and that's great. But you didn't add something like "... and that's where I learned that pattern matching is undesirable because <technical reason>".
The comment that appears to have gotten you riled up was after the person I was talking to saying they understood. After a discussion about language complexity I thought that it would be appropriate to suggest some resources on a "quick" project that can help build an intuition on that topic. And to be honest, it's a project I like to find excuses to suggest. I find people tend to be surprised at how easy and fun it can be to make some meaningful progress.
I understand that you would like for me to somehow short circuit that process, but I don't believe I am capable of building someone else's intuition by posting a throwaway comment on HN. Intuitions are typically built on experience and tinkering, not reading someone else's experiences.
That you view that project suggestion as a continued argument is unfortunate, I can assure you that was not my intent. Again referencing the HN guidelines, I encourage you in future to try to read people's posts first with the assumption that they are being genuine and only fall back to an assumption of malice when you absolutely have to. Long drawn out arguments over semantics don't help anyone.
I do understand, though, that the purpose of Go is not necessarily to push the boundaries of language design. I also understand that it's important the language is easy to pick up, compiles quickly, etc.
I think that some of Go's design decisions are bad, even with those stated goals in mind. Again, I don't want to overstate my experience or knowledge of language design (although I do know a little about Google's attitude towards Go, since that's where I spent my internship learning it), but some features (like "multiple return values" instead of tuples) seem to me to be just bad. Tuples are more familiar to a broader range of programmers, aren't a strange special case, are extremely useful, and have a straightforward implementation. Also, I don't want a bunch of fancy features added to Go: ideally several features would be removed, in favour of simpler, more straightforward ones.
Perhaps they find it easier to teach to users coming from languages with less or no type inference? Java and C++ programmers in my experience don't tend to be familiar with tuples, despite there being a tuple in the C++ stdlib. My purely uninformed guess is that it's because of how verbose declarations can get in Java, or in C++ without auto/decltype from C++11.
I remember having some bugs in Python due to one element tuples, I don't think I would have had the same issue if Python had multiple return value instead..
If you like math/category theory, go deep on the math itself. Your knowledge will be transferable to more than just some man-made story (like a programming language).
Hm interestingly I actually felt like learning Haskell had a huge benefit to my day job, probably much more than I imagine learning x86 assembly would. (Though admittedly I have learned some assembly in the past and that was also helpful).
I feel like Haskell forced me to write better code by forcing me to think about side effects. I don't know that I would actually use it in a production project because unfortunately real projects often rely a lot on state, even if constrained to a subsystem, and I still find it difficult to reason about the performance of a Haskell program.
Not trying to invalidate your point; perhaps you were already very good at what I learned via Haskell =] I admit I also find Haskell much more enjoyable to program in for the most part.
I see, but wouldn't that be possible by "forcing" yourself to think about side effects in the language you were already using?
I mean, at the end of the day, it's learning the functional programming concepts that is supposedly going to make you a better programmer. Why not just start learning those in Java, Python, JS, etc.?
One thing I hear a lot from people who've learnt Haskell is they admit they're probably never going to use it in a real-world project (for so many reasons). Then, isn't it inefficient and a waste of time to learn parts of Haskell that are only found in Haskell?!
If I were to learn FP (which I will soon), I'd choose to learn it in the language I'm using now. It's not only more efficient, but also I'd enjoy being able to put what I've learnt in practical use.
That could totally be possible for many, but for me, I need something more concrete. I had read about Haskell and the benefits of immutability and agreed from a high level, but until I actually used it, I didn't feel like I understood it.
Because every OOP/imperative programmer I've ever known for 18 years has the easy way out in not thinking about side effects or immutability and they never proactively reach for them.
Granted my bubble is not representative of the world but this trend is nevertheless quite telling. I also never proactively reached for FP techniques in OOP/imperative language until I learned my first FP language.
> Pure functions by definition adhere to dependency injection and single responsibility principle.
Dependency injection and SRP are other man-made constructs with dubious utility in the same vein as functional programming.
> There's a reason that lambda have become table stakes for new programming languages, and that's because composing functionality is a generally useful feature
Lambda in functional programming is supposed to be a primitive you use to do everything ahem y combinators ahem. In Java 8/C++11/Swift, the lambdas you speak of are used only as embedded subroutines.
Functional programming selects for better than average programmers to begin with--the "programmer's programmer" that writes code for fun and probably visits this site. You're unlikely to convince to learn Haskell the person who writes enterprise .NET, never used anything but Windows, and who never opened a text editor after work. The functional programmers were already good before they became functional programmers. Then you get the cargo cult type of intrigue. "X writes really good code. X also uses $FP_LANG! Be more like X!"
You might think that this isn't worth the mathematical rigamarole that comes with it and that it's grown up with, but as people have seen in a number of other HN threads, formal methods are having a renaissance now and the tools we have that engage with them can get us a lot further than they could in the 1960s.
I have written many bugs, of many different kinds, in other languages that could have been detected automatically by a type system like Haskell's. I'm not suggesting that that makes the other languages inherently bad, or that other programmers (or I) couldn't adopt other methods that would also help avoid these errors, but I think the ease with which Haskell's approach can do it is something interesting to consider.
Program verification and functional programming are separate things. Ada predates Haskell by at least a decade, and it's not functional at all. Rust is kind of a revival of that in proving memory safety via borrowing; not functional either.
But the set of programs which can be formally proven is smaller than the set of all programs, so I'd rather not miss out by only making formally proven software. (The entire field of deep learning is a good example of useful code which can't be formally proven.)
Now everything has an ui, and data innies and outies, but gosh! That's just connectors to the diamond!
Once you've wired everything together, now you need to track version, build artifacts, manage release processes, test them and qualify them, etc, so you do a lot of QA and DevOps work.
QA, DevOps and sanitation engineering is so product specific that it can't be packaged up, it's a craftsmanship position, and that's why all of us spend most of our time doing this kind of work.
In your career, you will go through new and excited, then sort of bored, then you will find a niche that's both interesting to you, and that you're good at - you become an expert in something. At that point, you do as much of that as you can, and the sanitation engineering doesn't seem so bad when its directed at something interesting to you, especially when you work with colleagues who are better than you at something and challenge you professionally.
Much less than 30 years :-)
Thanks for the insight/encouragement!
For example, we have a client at the moment who makes a type of device with a lot of user-configurable behaviour. An embedded web server allows access to its UI from a browser, and we were originally brought in to build that web UI for them. On the face of it, this is a substantial but straightforward SPA development, just one where the back end happens to talk to APIs that communicate with various physical components in the device rather than a traditional database.
However, it turns out that the way users view that device and how they want it to behave is very different to the way you have to program the various physical components to make useful things happen. That means even in this superficially simple project, we have some interesting algorithms in the front-end to present application-specific visualisations of the current state of the system, and we have a much more involved algorithm sitting behind that UI that converts the user’s tidy, intuitive view of the world into the very untidy and often counter-intuitive data formats required internally to program the hardware components.
To make this comment slightly more topical, I’ll also mention that the behind-the-scenes algorithm is essentially a form of compiler, taking a serialised version of the user’s world as input and running through a pipeline that systematically converts the data into the required internal formats. The first generation was written in Python, and has proven to be reasonably successful, but we are always a bit nervous about maintaining it just because of the number of edge cases and interactions inherent in the world we’re operating in. For the second generation, we made a big decision to go with Haskell instead, and for this sort of work, there were very welcome benefits including greater expressive power when writing data-transforming code and a strong type system that prevents mistakes like accidentally forwarding data from one pipeline step to the next without applying an appropriate transformation.
I agree with Beltiras’s point in the GP comment, and I probably wouldn’t choose Haskell to implement the kinds of software mentioned in that comment for much the same reasons. However, it definitely has real value in the sort of situation I described above, where we have both integrations with other systems but also substantial data crunching to do.
The pure FP Scala community understands your complaints more than the Haskell community.
https://typelevel.org/ is chock full of
a) useful utilities for actually writing applications
b) decent documentation. Not just pages of type signatures, but demonstrations of the libs usage.
Everything you need is here: JSON, config, units of measure, streams, testing, validation, a really nice JDBC wrapper (https://tpolecat.github.io/doobie/)
Throw in https://http4s.org/ and you've got yourself a rock-solid, purely functional stack with sensible, documented APIs, a more readable syntax and better tooling support.
I urge anyone who learnt some Haskell, thought "man this shit sucks, I'm never going to write something useful in this" to at least give FP Scala a chance. Here's a useful service template to start hacking with: https://github.com/http4s/http4s.g8.
0 - https://www.manning.com/books/functional-programming-in-scal...
On the other hand Scala has its own share of infelicities when it comes to pure FP. I've mentioned this elsewhere, but the core language of Scala, that is the language that is left after you desugar everything, is OO and the FP part is really just a lot of syntax sugar mixed in with standard library data structure choices. That means if you're coming from a pure FP background a lot of things will seem like hacks (methods vs functions/automatic-eta-expansion, comparatively weak type inference, the final yield of a for-comprehension, subtyping to influence prioritization of implicits for typeclasses, monomorphic-only values vs. polymorphic classes, etc.). Treating Scala as an OO language side steps a lot of these warts.
And then there's the social factors; the Scala community is split on how much it embraces pure FP and pure FP is a (significant) minority within the community. This carries over to the library ecosystem where things are basically split into Typelevel/Typelevel-using and non-Typelevel libraries. Many workplaces have a fear (well-placed and not so well-placed) of the Typelevel ecosystem. Years ago Scalaz was something akin to the bogeyman in some places. Cats has a bit of a softer image, but still comes off as an "advanced" library in the Scala ecosystem.
Most of the social weight I feel is behind the non-pure-FP parts of Scala. Sure some of the libraries in pure FP Scala have good documentation (but I don't actually think the situation here is far better than Haskell's once you leave the core Typelevel libraries). The ones with excellent documentation though live outside the land of pure FP (Akka, Play, Spark, Slick, etc.).
Whether this is right/correct or wrong/incorrect is left as an exercise for the reader ;-).
True. Most multi-paradigm languages are not equal in each paradigm with which they support.
> All of Scala's FP parts can be desugared into OO. The reverse is not true.
This is a bit of a strawman argument, as any functional programming environment can be implemented by, or "desugared into", an object system. Contrast this with the fact that mutable OOP systems cannot guarantee Referential Transparency[0] and your second assertion is proven.
However, this simply proves that Scala supports more than one programming style. Whether a given code base employs FP, OOP, imperative programming, or some mixture therein, is a decision left for the system authors and not the language. It is left as an exercise for the reader to determine if that is good or bad.
> This has persisted in Dotty ...
AFAIK, Dotty is intended to be a new experimental language. I do not follow its development nor progress.
> That's the annoying part when doing pure FP in Scala. You can sometimes feel like you're fighting against the grain in a way that isn't true when on the OO side.
I respect what you identify but do not agree with your annoyance. But that's just me.
0 - https://softwareengineering.stackexchange.com/questions/2543...
https://programming.tobiasdammers.nl/blog/2017-10-17-object-... is an example from first principles in Haskell.
A more fleshed out version is O'Haskell (which unfortunately died out in the early 2000s).
Mutability can be built (or more accurately faked) on top of referential transparency as well through syntax sugar in a very similar fashion to how Scala builds FP on top of OO. Indeed this was the original impetus behind do-notation in Haskell, but it stopped short of trying to make do-notation look like ordinary Haskell code. If you had syntax sugar that elided the difference between do notation and ordinary equality and unified IO with normal types then you'd have mutability in your language implemented through syntax sugar on top of an immutable core language (you could call it automatic IO expansion to be cheeky that automatically inserts a call to pure in front of any non-IO code used in an IO context). In fact I could see a rather reasonable case to be made for such a construct.
Scala similarly "fakes" (not necessarily a bad thing!) a lot of stuff. This is how automatic eta expansion and special function instantiation syntax (the arrow as opposed to new Function1(...)) elide the difference between methods and functions in Scala and let the language pretend e.g. that methods are first-class entities that you can pass to another method (which is not true, they must be wrapped in a class first just like Java) and let you pretend that methods have the same type signatures as functions (when in fact methods have a special method type that can be polymorphic whereas function are always monomorphic. In fact you cannot write the true type of a method in a first-class way in Scala; it is a special type that exists outside of the normal type hierarchy that is referred to as a "method type" in the Scala spec). This is leaving aside the encodings of typeclasses, ADTs, fully polymorphic functions (FunctionK in cats), etc.
In all these examples it is the FP concept that is "faked" (higher-order methods and polymorphic functions respectively in the two examples) and the OO concept (method taking an ordinary instance of a class and generics in methods) which is fundamental.
Dotty is explicitly blessed as Scala 3. I would highly recommend keeping high-level tabs on it if you're a Scala programmer. You don't need to know the specific details of it, but note that Scala 2.14 will be built specifically with Dotty in mind. It is the future of Scala (https://www.scala-lang.org/blog/2018/04/19/scala-3.html). And it comes with a lot of goodies! I'm really excited about it. More importantly for this discussion the current encoding of typeclasses in Scala 2 still desugars to implicits + OO classes instead of the other way around (which is perfectly possible where typeclasses are the core abstraction and implicits and OO classes are built using typeclasses).
But I think if we're only talking about pure FP, or at least something close to very pure, I think you're still getting a better deal than Haskell, even in spite of all those quite legitimate downsides you mentioned. My own personal biases mean that I will always prefer pure FP to anything else (I personally didn't love Akka and Play when I used them briefly), but that's an argument for another day.
I feel like the F# community tends to be more grounded in reality. Or at least I'm more exposed to the side of it that is trying to popularize it in Microsoft, as a useful tool for Domain Driven Design and the like.
Plus:
- The type system. It can make your life a huge pain, but in 99% of the cases, if the code compiles, it works. I find writing tests in Haskell somewhat pointless - the only places where it still has value is in gnarly business logic. But the vast majority of production code is just gluing stuff together
- Building DSLs is extremely quick and efficient. This makes it easy to define the business problem as a language and work with that. If you get it right, the code will be WAY more readable than most other languages, and safer as well
- It's pretty efficient
Minus
- The tooling is extremely bad. Compile times are horrendous. Don't even get me started on Stack/Cabal or whatever the new hotness might be
- Sometimes people get overly excited about avoiding do notation and the code looks very messy as a result
- There are so many ways of doing something that a lot of the time it becomes unclear how the code should look. But this true in a lot of languages
This is how limited type systems are still very much better than no types.
This is not what I was arguing against. I was arguing that I would be surprised if people were happy to go from strong typing to dynamic typing, not that 'limited' type systems are better than 'no types'.
For a dynamically typed language, a REPL ends up being essential. In a lot of ways, REPLs can be a superior form of programming. Those are often much harder to get to work with static type languages (often too much ceremony around the types).
The other thing that comes up is sometimes those compile time errors are somewhat pointless. For example, in many cases the difference between an int and a long are completely inconsequential. But further, whether or not your type is a Foo with a name field or a Bar with a name field or an Named interface simply does not matter, you just want something with a name field. While static typing would catch the case of passing in something without the name field, it unnecessarily complicates things when you want to talk about "all things with a name field" (Think, in the case of wanting to move Foo, rename Bar, etc).
Then there is the new concepts you need to learn. With dynamic typing, you can write "add(a, b) { return a + b; }". But how do you do that with Static typing? Well, now you need to talk about generics. But what if you want to catch instances where things are strictly "addable?" now you are looking at constrained generics. But what if you want a specialized implementation? Now you are potentially talking about doing method overloading. What if you want a different style of adding? Now you might be talking about plugging in traits. Typing and type theory have a tendency to add a requirement that you learn a whole bunch of concepts, but also that you learn how to correctly use those concepts.
It is no wonder dynamic typing has it's appeal. Dynamic languages are generally low on ceremony and cognitive burden.
I say all this being someone that likes static typing. Just want to point out that dynamic typing has it's appeal. Obviously, the big drawback is when you come back to a dynamically typed language and you want to fix things. It can be insidiously hard to figure out how things are tied together and you get no aid from the language.
You lost me here. All of those "what if's" seem to apply equally to dynamically typed languages. If I want "a + b" to work with two Python classes that I just wrote, I'm probably going to have to implement __add__ methods on both classes, and possibly with non-trivial implementations. It's not like dynamic typing makes everything magically addable, with no burden on the developer. Wouldn't you agree?
Not to mention that languages such as Haskell and OCaml have REPLs too. They are not at robust as, say, Common Lisp's -- but REPL-driven development is hardly a stranger in the statically typed camp.
I agree that both camps have their appeal, though!
Haskell has probably one of the best and most useful REPLs around.
> But further, whether or not your type is a Foo with a name field or a Bar with a name field or an Named interface simply does not matter, you just want something with a name field.
This is perhaps an argument for a structural type system, IMO. Though I completely disagree with it.
> Well, now you need to talk about generics. But what if you want to catch instances where things are strictly "addable?" now you are looking at constrained generics. But what if you want a specialized implementation? Now you are potentially talking about doing method overloading
Those same invariants are still in your dynamic code, it's just now they are invisible to everyone and will crash at runtime if broken.
> It is no wonder dynamic typing has it's appeal. Dynamic languages are generally low on ceremony and cognitive burden.
There is a low cognitive burden on the writer, but for every refactor afterwards, and anyone who wants to change your code later, the cognitive burden is higher.
Dynamic typing does have an appeal, but it seems to be shrinking these days while people wake up to the benefits of static types. And it's no wonder, they just make sense from a pragmatic perspective.
Thanks for your post. I mean, I disagree with almost everything you said, but it's interesting to hear the perspective.
Is Stack not adequate? I thought the consensus was that it was ok. Is it really more horrible than say Maven or SBT or whatever in other languages?
I'm not familiar with Cargo, but NPM gives me nightmares!
For example: I haven't read or written a lot of Python in a while, but would Python programmers really want to implement mutation in such a class by copying dicts around? The hand-wringing about "oh no, I wrote the condition as not is_node" is silly since one could just define an is_leaf method that can be used without negation. And "changing heapToList to return a lazy sequence makes it no longer return a list, oh no!" is just as silly, since one would of course not do that but define a separate heapToSequence (and probably base heapToList on that).
Also: "pattern matching and algebraic data types which have massive, clear benefits and few (if any) downsides". They have downsides whenever your data is not tree-shaped. Yes, a lot of data is tree, but then a lot isn't. I work in compilers, which is often touted as a prime application of ML-family languages, and this is very true, but not 100%. If you can have sharing in your abstract syntax tree (like a goto node that might refer to a target node somewhere else in the tree), you start to have difficulties. And you get even more difficulties when you try to model control flow graphs and the like. Nothing insurmountable, but still things where it's suddenly good to have other tools than only algebraic datatypes. OCaml is good in this regard.
I wouldn't recommend Hsakell for a desktop GUI, but it's excellent in the API world. It's more of a backend language.
I would never bring it to the production though, reasons being:
1) Production code should be understandable by an on-call person at 4 am. If business logic is buried under layers of lenses, monad transformers and arrows, good luck troubleshooting it under stress. And real systems do break, no matter type safety.
2) It's a research language, and a big part of the research is about control flow. And therefore haskell has _way too many_ ways to combine things: monad transformers of different flavors, applicative functors, arrows, iteratees, you name it. And libraries you find on hackage choose _different_ ways to combine things. In the business code you probably want to combine multiple libraries, and you inevitably end up with unholy mess of all these approaches. Dealing with it takes more time than writing business logic.
3) Developers look at these fancy research papers and try to reproduce them. As a result, very basic things become extremely hard and brittle. I saw a real-live example when applying a transform to all fields of a record took a team 2 days of discussion in slack, because "writing this manually?! it won't scale to record with 100 fields".
4) Architecture is extremely sensitive to the initial choices due to isolation of side effects. Because if you suddenly need to do something as simple as logging or reading a config in a place where it wasn't originally anticipated, you're in for a bumpy ride.
This is fundamentally why it's useless in practice. It requires you to anticipate/predict everything ahead of implementation.
It requires you to be perfect at Waterfall ( https://en.wikipedia.org/wiki/Waterfall_model ).
Real-world problems laugh at such idealism.
Only if you choose to isolate all side effects from each other. Actually Haskell makes refactoring really easy, so you can adapt your implemention to changing requirements easily.
I wanted to make point that that #4 is not true.
Amortizing the up-front cost of design, into a continuous cost of refactoring every time the requirements change doesn’t make the original claim false.
You are still paying the price of rigidity.
This is simply bs. Haskell is actually a language (that I know of) where, IMO, your initial choices matters least. And that's because it's gives you great refactoring experience.
Yeah right! I don't have to refactor extensible code.
Being 'good at refactoring' is only a desirable feature if you are spending a lot of your time refactoring code.
Why are you spending so much of your time refactoring code? Because your language isn't extensible!
Not a fan of TDD, eh?
Not a fan of silver bullets.
But your comment is still odd. Refactoring seems to be a core tenet of modern programming (regardless of TDD). In TDD in particular it's a central part of how you work, and so your remark sounds odd unless you also claim you don't believe in TDD :)
Don’t know what the state of Haskell refactoring is now but it looks like until very recently “great refactoring” was “change one thing and then manually fix compiler errors”.
Point me to something like that, for, lets say, Python. For example to error when I misuse return value of a function. Or when I decide that this string is no longer a string but a customer name. Or when my function will call IO operation, but it's not allowed to do so. Or when I add new field to a record and its not fully constructed somewhere.
- We are a statically typed language with significantly better support for refactoring than anything else out there
- But...
- Let's say Python
Let's say Java, why don't we? A language with significantly better support than "change one thing and manually fix compiler errors".
See IntelliJ's capabilities: https://www.jetbrains.com/help/idea/refactoring-source-code.... (there are 23 subsections)
And even Python's capabilities are significantly better than manually hunting through compiler errors (though obviously not as extensive): https://www.jetbrains.com/help/pycharm/refactoring-source-co...
On topic: Oh, these are great and I wish Haskell had more of that.
But these are "dumb" actions, really. How about:
* writing terrible code to get something working, then breaking it down and putting it back up as something production ready
* changing core data structure and updating all the code that uses it
* solving some problems in separate projects and combining them together
* copy-pasting code from another project and adjusting it to a current project
All of the above of course without running the program once in between, without changing the desired program's behavior etc.
And no, I don't think you can't do that in Java, I just think that Haskell is better at it.
* writing terrible code to get something working, then breaking it down and putting it back up as something production ready
Automated: Extract method, move code, pull members up/down, rename etc.
* changing core data structure and updating all the code that uses it
Depending on what exactly you're changing. From as simple as automated "update type signatures of all affected functions and methods" to automated "rename" to yes, hunting down compiler errors
* solving some problems in separate projects and combining them together and
* copy-pasting code from another project and adjusting it to a current project
Once again, depending on what and how you combine, using the things from above.
There are more powerful tools than "oh, let's run the compiler for each change and manually fix all the errors". The flow should be "let's run all the automated refactorings and then run the compiler to see if those missed something". And these automated refactorings are often reversible with a simple Cmd/Ctrl-Z.
Ah, yes. And many of these are available even for dynamically typed languages (but not as powerful and sometimes with caveats, for obvious reasons).
Java and Python are simply not languages that encourage (or even make possible) writing code that is easy under those changes. In Haskell you have immutability (if something is declared, that's its final value, so I can simply move it around), purity (code that is basically context independent), dumb data structures (no "enterprise boolean" with nontrivial internal state and ceremony to initialize), functional (no code attached to mutable data that may implicitly require data to be in special state), no nulls in flow control (that are invisible to a compiler, so yea good luck moving function around that expects no nulls), ergonomic creation of data/types (so you don't have strings/integers everywhere that are opaque to a compiler).
There is no contest, really. Maybe with some modern languages like Rust or TypeScript, but I haven't tried those.
However, most changes/refactorings I usually encounter are due to trivial problems that can be automated. And I'd love to have those tools for FP where a lot of changes are still just that: rename a function or a field in a data structure, extract a function, rename/move a module, update type signatures etc.
However, it looks like the stronger the static typing, the worse are the tools :) Not enough manpower to take care of them, perhaps?
As for Haskell, I think, it's a combination of manpower (so popularity, really) and the fact that the compiler wasn't developed with tooling in mind.
Thanks for conversation, stranger, and have a nice day!
This is interesting because another popular argument against Haskell is "Haskell's type checker can only check trivial properties that can be automated".
Either automating trivial things is valuable or it's not, and the anti-Haskell camp seems not to agree on which!
In Ruby it's changing the attr_accessor line.
In Haskell objects are immutable, so you need lenses.
In my experience working in the largest Haskell team in the world, dealing with various applications and services in production: none of what you said is close to reality.
You've recounted your experience and detailed that you have seen issues with things in Hackage, and I won't claim your experience is invalid. I just won't let you claim your experience is that much more valid than someone else's without proof.
Especially point 4 sounds very painful. I frequently have to add logging or special case handling at different places in our code base.
It's all about how much do you isolate sideeffects from each other. If you go nuts with isolation, you loose flexibility. If you write all your code in IO monad (that means any code can have sideeffect), you are as in other languages where you can do sideeffect anywhere you want.
I'm still curious as to how to introduce variations though.
Frequently we find that we have to introduce "if (settings['special_x']) then DoSpecial else DoCommon" deep in the business logic to handle special cases that arise.
Assuming the existing function did not depend on the settings, how does one best introduce this dependency on the settings? In our code base we can cheat by effectively having the "settings" dictionary as a global variable, though we try to minimize this obviously. But in a pinch at 4am, that might be enough.
How about Haskell, is there a way to "inject" such settings without refactoring all the way to the top?
Source: 3 years of pure FP Scala in prod
If anything, it's the inverse: "isolation of side effects" means the architecture is more flexible and lego-like -- as opposed to a spaghetti mess.
And if you need to change interfaces, you are in for one of the smoothest rides, as the compiler will guide you every step of the way to implementing the change wherever it's needed.
This is the same in any statically typed language. Actually the quality of Haskell/GHC's error messages are limited by constraint-based type inference and languages that have flow-based type inference do a better job IMO.
Surely not in any statically typed language. Languages like Java cannot encode the same useful information (or rather, you cannot force them to) as languages like Haskell. Specifically, you cannot make them enforce lack/presence of IO in their types. Most mainstream statically typed languages cannot do that, in fact.
Even then, in Haskell you can say "this doesn't do IO" which you cannot in most languages!
It's also just an example of the expressiveness of the type system, which "most statically typed" cannot enforce or sometimes even express.
Lets say you are making an RPG where you can equip items. You write a lot of pure functions and behavior for stat adjustments etc. Then the designer comes to you and say that items are no longer constants, every item needs a durability which goes down on use. In Java this would be a few line change, just add a new field in items and add a line to subtract it in relevant places. In Haskell you would need to bubble up the new item change to the global state and properly set it in each part of the code where you use items. If you aren't careful you can easily miss to properly bubble it up somewhere and the item doesn't get updated, or you might have the same item referenced in several places and now you'd have to refactor it to have a global item pool to since tracking down all places that should be updated is not feasible.
I don't think that's how you'd handle a state change in idiomatic Haskell. What you propose sounds very error-prone.
But with assumption that code was working before and you had for example function: use :: Item -> Item, and you change duration in this function, what else do you need to change to "bubble" new state? I don't get this.
BTW What's the problem with global store and passing only IDs ? I feel like this is probably valid approach and anyway ECS are implemented in similar manner AFAIK - https://en.m.wikipedia.org/wiki/Entity_component_system
In practice this is not actually a problem. It just takes a little getting used to to get yourself out of the 'memory-address as identity' mindset that procedural languages have.
And then find out that since items used to be constants all instances of a given type of item are actually the same item object, so modifying one unexpectedly affects them all. Or worse, sometimes items are copied and sometimes they're shared by reference, so whether they're affected depends on each item's individual history.
One of the benefits of Haskell is that it forces you to think through these aspects of the API in advance. A mutable object with its own distinct identity and state is a very different concept from an immutable constant and the design should reflect this. Changing one into the other is not an action to be taken lightly.
Applications and building large systems are explicitly goals of Haskell. Haskell was never intended to be a research language.
>Haskell was never intended to be a research language.
Intended and actively tried not to be are two different things...
Rather than “avoid success” at all costs.
It’s a joke in the Haskell community that relates to the language’s early history and its ability to break things without much fuss. Things have settled down a lot more in the last few years, hence the shift in emphasis.
If only weren't web only and run as a private project. An elm like language with llvm backend would be amazing
> If only weren't web only and run as a private project.
Yeah, the way the project is run is troubling.
1) Many of us have also seen the C++ library or application that busts out the template metaprograms at every opportunity. Haskell is the same. Use restraint when bringing in new libraries and programming techniques. (as an aside, I have yet to encounter a single problem that I thought merited the use of arrows)
2) You also see this in other languages, though to a lesser degree. The same techniques we use with other languages work just as well in Haskell: As leadership, form a strong opinion on the best way to write control flow and ensure that everything uses a consistent style.
3) Are your engineers running around implementing random research papers? This has nothing to do with Haskell.
4) In practice, this is almost never a big problem. Refactoring a pure function to an effectful function is easy and safe. In a production setting, almost all of your functions run in some monad already, so it's pretty rare to run into a situation where you suddenly need to percolate effects through many functions.
I've never been lucky enough to get a full-time Haskell job (nor did I try), so my experience is based on single-man development & prototyping using the existing stuff found on Hackage. I presume that it may be different for a huge organization that can afford to build the entire ecosystem from the ground up while enforcing design rules (and these rules must be created by an exceptionally brilliant architect!). But in other languages you don't have this situation.
> Many of us have also seen the C++ library or application that busts out the template metaprograms at every opportunity.
C++ and boost set standards a bit low, to be honest.
> Are your engineers running around implementing random research papers? This has nothing to do with Haskell.
Not research papers, just the usual business logic. The problem is that research papers set the code style. Also they are not "my engineers".
> In practice, this is almost never a big problem.
It typically is, because changing internal implementation is reflected in the interface, and for each invocation of the function change has to propagate through _the entire_ call stack to some place running in IO monad or whatever.
> almost all of your functions run in some monad already
You defy your own argument. If that's how you design application, what's the point of bothering with side effect isolation then?
For Example, Yesod gives you the Handler monad, which is based in IO, and really just exists to provide access to runtime/request information, so at the API handler level you can do anything you need to. But what's nice is that not everything has to live in IO, and so in places where it makes sense, such as a parser, we can say that parser doesn't need IO, because that wouldn't make sense.
And my point here is that just having the ability to separate between IO and non-IO is very useful; we also do not have to split every single effect down into it's own special, separate type; in many cases that would just be overkill.
I don't understand this.
If you have a monad like
do
...
_ <- doSomething arg1
let result :: Result = pureFun arg2 arg3
...
How is that not simpler and better than having that pureFun invoke a bunch of side effects and perhaps even mutate something that you have to be aware of? How does this take away the benefits of side effect isolation?Of course, I suppose there is always unsafePerformIO, at least for those late-night debug sessions.
However, you can just log the output if you like without putting IO into the function. I see zero reasons why anybody would push such logging into production. It behaves exactly the same way on your own computer as it would in production - it's just a pure function.
However, disregarding the implementation details, here is the abstract problem: I have a program which is reading some complex data structure from IO, applying a complex, pure pipeline to it, and sending the reply back through IO.
Now, say the output is not correct in some way. Your only chance of debugging the issue is to somehow visualize some of the intermediate values, right?
This being a pure pipeline, you don't need to have logging in production. You could just log the input value, and when a bug occurs, run the same input value from the logs through a modified version of the original pipeline with some logging sprinkled throughout - you're right on this side.
Now, I expect that there are many situations where adding the logging in production is still preferable - maybe the pure pipeline is in development and some kind sof issues are frequently occurring, maybe the pipeline takes too long to re-run, so it's just easier to have the logs from the first run etc.
What I'd do is simply break down the larger pure function into smaller pure functions and sequencing them in the monad.
This way you would still retain purity with your functions but you could log every intermediate step by simply logging their output (and its corresponding input). Would probably make the code more maintainable as well.
From what I understand Haskell has great tooling for function profiling. Unfortunately, I got no experience with these yet so I can't say if they'd help you solve such problems.
Edit: Btw sounds like property based testing might work well as a solution to this kind of problem. It works really well with pure functions.
For temporary debug output you should probably consider the Debug.Trace module before unsafePerformIO. It has a simpler interface and doesn't introduce the possibility of arbitrary I/O, just text output (and event logging if that's enabled in the RTS).
If you're able to run the code locally, there is a function called Debug.Trace.trace which writes messages to stdout without requiring IO.
If it's a production issue, all you need to do is figure out what is being passed to your function. It's a pure function, so you can always debug it locally once you've got that.
At my job, software is only have the engineering, we need to save half our brains for product/business issues.
I can't tell I had less problems understanding magic container IoC stuff than most Haskell concepts.
Not even that. Java's optionals are almost useless, needlessly verbose, you still don't have pattern matching, and there's also still null to contend with.
That's a huge red flag.
As for the rest.. I don't think the language was the problem at that team.
And after a few refactorings.
Most errors have nothing to do with “unreadable code”.
Look, you've already fucked yourself by needing a 4am custom fix. That isn't a language problem.
It can be a language problem if the person who needs to do a 4am custom fix can't understand what the code is supposed to do.
Or to put it another way: I'd much rather be debugging some BASIC code at 4am than some Brainfuck code.
As I pointed out in my previous post, I do feel the language can be a non-trivial factor in allowing a dev like myself to be confident in developing a fix for my coworkers code at 4am.
However I don't know Haskell, so I have no idea how it fares in this regard.
Where I work now, on-call was long ago relegated to nothing more than a routing task. The on-call person takes the initial call, figures out who best can solve it, and contacts the person. The guy on my team who writes only in Javascript and Python would NEVER be in the position to make a critical fix overnight in a Java module. For those of us who work in Java, reading the language is trivial but understanding what the module is supposed to do from a high level is non-trivial. People who work in Haskell daily would be the same.
This is what I tried to call out. A Haskell developer called at 4am would have no more problem reading Haskell code than I would have reading Java code. While a Javascript developer should NOT try to issue a critical patch in a language (s)he doesn't work in, regardless if the language is Haskell, Python, etc. Any organization requiring you to do so is poorly managed.
Do you agree there's a spectrum here (with stuff like Brainfuck being on one end)? If so, I think it's a valid question to ask about a language.
Maybe Haskell is just as easy as Java, maybe it is slightly more difficult, maybe it is slightly easier.
For example, you can't really do embedded DSL's in Java, but you can in Haskell. If my coworker had used that and I had not yet had time to study it, could it be I would struggle a bit more with grokking his code? I dunno, at least to me it seems possible, but since I've not really used Haskell it might be entirely different.
I'm in complete agreement with you. My contention is that a person who works with a certain language, even Brainfuck, all day every day is going to be able to read it as easily as a person who works in a more "popular" language like Java: even if it's at 4am. Anyone who doesn't work in the language shouldn't be reading it with the goal of implementing a critical fix at 4am. That person should contact a team member who does work in the language.
I'm contending that the 4am scenario, as posed by the parent commenter, is a process issue not a language issue. If a manager requires that whoever answers the phone at 4am fixes the problem, even if the person has no knowledge of the language or requirements of the module, then his/her employees should be running to another job because that manager is bad for your health and livelihood.
For example, const helps me a lot when trying to understand C++ code. If I see a const reference, I know this bit of code I have in front of me can't modify whatever it is referencing. My language at work doesn't have const references, so I'm never quite sure if a call does modification as a side effect or not. This is the kind of stuff that I think can matter when trying to fix something at 4am.
Now, Haskell as I understand it is rather pure, so side effects like that are not a big issue. But on the other hand it allows for embedded DSL's from what I understand, which perhaps could be an issue if my coworker were to get creative. Though as I said I have no background to judge Haskell on this, I just thought it was a valid thing to be concerned about.
Give yourself time the next day to actually think about the problem with a clear head. At 4am trying to debug code, you're far more likely to cause more problems than fix things.
To me, I read this question the same as "Haskell's not useful when you're stuck in a well with a bomb ticking down." The language isn't the problem. It's the situation that's the problem.
All commenters who said "It's your own fault for writing crappy code, duh!" perhaps never worked with anything sufficiently complex and expensive.
In reality, it would be a sysadmin confirming dependencies for this application to run (in the system, network, storage, ...) are functioning as required. If there turns out to be a problem with the application itself, there's no time for development at that point: you roll back. I don't see how the paging developers thing would be able to provide any stability. It's too late for that when you're in production.
I am also the guy who puts metrics in place to monitor the health of the system and if any metrics breach alarming thresholds the owner of the service (me!) is automatically paged.
How does a sysadmin know that something is broken anyway, and how does a sysadmin know who needs to get paged? If a sysadmin can make this decision - so can an algorithm.
Much of this is in Google's SRE handbook[1]. The entire notion behind the DevOps concept was to make sure that you don't segregate development and operations.
The people who write crappy code must be the people who wake up at 3am when the crappy code breaks.
>It's too late for that when you're in production.
Bugs will always slip through testing, and things will break even in production. Nobody is perfect - not even Google.
So what do you do when rollback doesn't work, you've tried everything in the playbook but the system/service is still down? Whose job is it to understand how to recover your service? Surely you don't expect the sysadmins to be doing that? They don't understand the system. How could they? They didn't build it.
The problem had to be fixed right then as further delays would cause heavy disruptions of the operation for that day, which would have been very bad for our customer.
While support can deal with most issues, it's not entirely uncommon that us devs have to step up and either help fix the issue by analyzing the relevant code, or actually push a fix in the middle of the night.
I don't like the "simple language means understandable code" argument being thrown around like it was some obvious fact. I'm currently working on a project whose major component is a byzantine home-grown message passing framework written in pretty much Java-in-C++ (old school OO with liberal use of shared pointers. Not that I would call C++ in any form "simple", but I do think that C++ without template magic is simpler than Haskell with GHC magic, or, say, any Lisp with a lot of macros). Maybe it's a case of grass being greener on the other side, but I'd gladly take a monad transformer stack or a DSL written in Lisp macros over doing the same song and a dance over and over again just to implent a tiny new functionality, or trying to deduce the logic from someone else's sea of boilerplate.
I mean, taking this logic to extreme, one could say that code written in assembly is easy to understand: it's adding a value to EAX now, then it jumps over there...
This being said, Haskell does have something in it that encourages one to design a ballistic algebra that runs in the type system before even pointing a gun at one's foot.
Haskell is not even a total language, so type system doesn't magically divert gun from your feet. You can easily prove False in Haskell (there's a Prelude function for doing this!), enough said. And if you want types keep your feet safe, it's possible of course, at a _great_ cost, and not in Haskell.
(1) "Haskell is unreadable because the business logic gets buried under layers of technobabble." Not observed in practice, and not a property of the language anyway.
(2) List of many things that are supposedly about control flow. They aren't. Also implies that these things are incompatible with each other, which isn't true.
(3) Indeed, some developers try to reproduce research papers out of curiosity some of the time. That's a good thing.
(4) Mostly true, but true in any language, and not due to isolation of side effects.
These are textbook examples of straw manning: representing a caricature of the other side in order to easily argue against it. (But why? Who hurt the poor guy?)
You can say the same thing about production Java code buried under layers of frameworks, factories, facades, proxies, aspects, annotations, etc. I don't see why readable, maintainable code can't be written as easily in Haskell as Java, C++, Python - you just have to make writing clean code a priority.
If you are stuck on a plane are you going to watch citizen kane or something from the last couple of years. What about an Akira Kurosawa movie? They top film critics' lists of the 'best' movies, but really they are some of the most influential movies of all time.
Lisp and Haskell aren't the 'best' languages to use the vast majority of the time, but they heavily influential and have contributed a lot to computer science.
Let's assume that your on call person is a developer. (If the person is not a developer, (s)he is not going to comprehend any language.)
1) If your application is written in Haskell, the on call developer would more than likely have been involved in the code base and reads Haskell. Your assertion about not comprehending it seems suspect.
2) You have a serious process issue if a developer is woken from a sound sleep to track down, correct, thoroughly test, and promote a fix for the bug in the middle of the night. The number of cases in which this happens should be close enough to zero to call it zero.
Your comment sounds like an attempt to cast Haskell as a purely academic language because it fails a (poorly) made up real world scenario. I'm NOT a Haskell-er so I don't feel a need to defend it. I do find the reasoning behind the criticism poorly thought out.
2) You’ve never worked in a company where a bug brought down prod and an on-call has to fix it?
2) Early on and even then rarely. After working in a hospital writing and supporting an electronic patient chart system, I found it hard pretending that anything short of life and death was serious enough to be woken over.
1) For a team of Haskell programmers reading Haskell code is just the norm. It's the same as for a team of Python programmers reading Python code. Also as a side-note: I never in my 15 years of professional life had a 4am call. Maybe I'm lucky, I don't know. In my experience automated tests, code reviews, sane deployment policies, etc... all minimise the chance of this ever happening. Haskell's powerful type system helps a lot here as it saves you from a whole array of problems upfront. I feel much more confident deploying Haskell code to production than code in any other language.
2) In commercial projects engineers tend to do things in sane and simple ways. This is not surprising really. Just cause you can does not mean you should. It's not Haskell specific but a general principle ie. KISS.
3) "Fancy research papers" at times can actually be really helpful. If you happen to have a problem that someone wrote a paper about that saves you time having to rediscover the solution. Having lots of research papers is imho a great strength of Haskell. That being said KISS from above applies here. If your problem is simple then do it simply.
4) Architecture in general is much less sensitive to change in Haskell than in other languages in my experience as the type checker guides you when you want to change your code. Refactoring in Haskell is pure joy. Re logging/configuration/etc... there are well-known patterns to deal with these as you would imagine, it's not like no one had to solve these problems in Haskell before. There are also tools (eg. Debug.Trace) in case you need some quick ad-hoc printf-style debugging. The same really as in any other language.
I don't want to write about all the pros of the language, others have done so already, just wanted to add my own professional experience to the mix.
Not a serious Haskeller at all, so not sure if this is a terrible practice, but for logging, `unsafePerformIO` could work in a pinch without changing types.
I know Haskell and Go about equally. Meaning I know all the language features & common libs & have a grip on their runtimes. Go on day 1 was way easier to write and understand than Haskell on day 1. Now that I've normalized their learning curves, Go didn't get much easier to work with. Haskell did - I use my brain way less when writing Haskell than when writing Go.
But even then, Haskell or Go. It's all just the same stuff.
I've pretty much given up talking on the Internet to people about Haskell. Arguing against some of the pts in this thread about how Haskell isn't good for production.
I'll continue to write Haskell for pay for the foreseeable future. If I'm lucky, I'll do it the rest of my career. I don't see any reason why not.
I would say that's a software company's biggest problem instead. I really don't understand how someone with little or no coding experience can be in a leadership position with other programmers who are supposed to be problem solvers and have all the details of how to build something. The bigger the gap in knowledge, the more communication breaks down and the hierarchical structure becomes useless.
I had one where the leader in question had coding experience but didn't know Haskell at all.
Despite this [1], they tried to read some code (that the Haskellers has no issues with) and couldn't, so he deemed the codebase unreadable and eventually called for a rewrite. Worse, during the debate about the rewrite, there was constant discussion about how the code was unreadable and bad, but it was never sourced to this leader. Instead it was asserted with weasel words only.
[1] Maybe it was because of this..this leader was driven by confidence in their experience and seniority.
leaf = object()
Huh, that's funny, it's shorter than the Haskell? Why is that? Let's keep going. def merge(lhs, rhs):
if lhs is leaf: return rhs
if rhs is leaf: return lhs
if lhs[0] <= rhs[0]: return lhs[0], merge(rhs, lhs[2]), lhs[1]
return rhs[0], merge(lhs, rhs[2]), rhs[1]
Ugh. My mouth tastes funny. Livecoding on this site is always disorienting. I need to sit down for a bit. Exercise for the reader: Continue on in this style and figure out whether the Haskell really deserves its reputation for terseness and directness.Edit: I kept reading and was immediately sick. __dict__ abuse is a real problem in our society, folks. It's not okay.
def insert(tree, elt): return merge(tree, (elt, leaf, leaf))
The Haskell memes are growing stronger as I delve deeper into this jungle. The pop-minimum function here is a true abomination, breaking all SOLID principles at once. I can only imagine what it might look like in a less eldritch setting: def popMin(tree):
if tree is leaf: raise IndexError("popMin from leaf")
return tree[0], merge(tree[1], tree[2])
We continue to clean up the monstrous camp. def listToHeap(elts):
rv = leaf
for elt in elts:
rv = insert(rv, elt)
return rv
The monster...they knew! They could have done something better and chose not to. They left notes suggesting an alternative implementation: def listToHeap(elts): return reduce(insert, elts, leaf)
Similarly, if we look before we leap: def heapToList(tree):
rv = []
while tree is not leaf:
datum, tree = popMin(tree)
rv.append(datum)
return rv
And again, the monster left plans, using one of the forbidden tools. We will shun the forbidden tools even here and now. We will instead remind folks that Hypothesis [0] is a thing.Haskell's an alright language. Python's an alright language. They're about the same age. If one is going to write good Haskell, one might as well write good Python, too.
Here's my version of skew heap in Python 3, edited down to just the essential operations. (Link to slightly fuller version: https://gist.github.com/olooney/97643d07d69d22015ae5bb70c121...). It seems about as clean as the Haskell version presented, mainly because I used None to represent leaves (which is quite Pythonic.) Relative to the code presented in the article, it also benefits from a clear partition of trees of immutable nodes on the one hand, and a class to represent the mutable heap interface on the other.
from typing import NamedTuple, Any, Optional
class Node(NamedTuple):
"""A single Node in a binary tree."""
value: Any
left: Node
right: Node
def merge(p: Optional[Node], q: Optional[Node]) -> Node:
"""
Implements the critical "merge" operation which is
used by all operations on a SkewHeap. The merge operation
does not mutate either tree but returns a new tree which
contains the least item at the root and is in heap order.
The resulting tree is not necessarily balanced.
"""
if p is None: return q
if q is None: return p
if q.value < p.value:
p, q = q, p
return Node(p.value, merge(p.right, q), p.left)
class SkewHeap:
"""
A SkewHeap is a heap data structure which uses an unbalanced binary tree to
store items. Although no attempt is made to balance the tree, it can be
shown to have amortized O(log n) time complexity for all operations under
the assumption that the items inserted are in random order.
"""
def __init__(self, items=tuple()):
"""
SkewHeap() -> new, empty, skew heap.
SkewHeap(iterable) -> new skew heap initialized from the iterable.
"""
self.root = None
for item in items:
self.push(item)
def push(self, value: Any):
"""Add an item to this heap."""
node = Node(value, None, None)
self.root = merge(self.root, node)
def pop(self):
"""Remove the least item in this heap and return it."""
if self.root is None:
raise ValueError("Cannot pop empty SkewHeap")
else:
value = self.root.value
self.root = merge(self.root.left, self.root.right)
return value
def union(self, other: 'SkewHeap') -> 'SkewHeap':
"""Return a new heap which contains all the items of this and another heap combined."""
ret = SkewHeap()
ret.root = merge(self.root, other.root)
return ret
def __bool__(self) -> bool:
"""Return true iff the heap is non-empty."""
return self.root is not None
def test():
h1 = SkewHeap()
for item in [42, 13, 50, 11, 14, 50, 91, 72, 91]:
h1.push(item)
h2 = SkewHeap([63, 15, 1, 22, 91, 11, 92, 99, 93])
h = h1.union(h2)
while h:
print(h.pop())
Maybe someday I'll read an article comparing two languages where the author's greater familiarity with one language over the other isn't the dominating factor, but it won't be this day.The point of the post was really to argue that simple features like pattern matching, ADTs, and so on, should be in languages like Python and Go. Also I wanted to make the point that functional non-mutating APIs could be simple and tend to compose well: the `unfoldr` example was all about that. In that vein, it was important that I compare the Haskell code to an imperative version.
For instance, with your `reduce` improvement: I agree that the `reduce` version is better! It's simpler, cleaner, and easier to read. But Python these days is moving away from that sort of thing: `reduce` has been removed from the top-level available functions, and you're discouraged from using it as much as possible. The point I was making is that I think that move is a bad one.
Finally, while the Python code here is shorter, you still don't get any of the benefits of pattern-matching and ADTs.
* You can only deal with 2 cases cleanly (what if you wanted a separate case for the singleton tree?).
* You are not prevented from accessing unavailable fields.
* You don't get any exhaustiveness checking.
Python is untyped. This fundamental separation between Python and Haskell is non-trivial, and can't be papered over. Your complaints about exhaustiveness, field existence, and case analysis are all ultimately about the fact that Python's type system is open for modification, while Haskell's is closed; in Haskell, we can put our foot down and insist that whatever we see is an instance of something that we've heard of, but in Python, this is simply not possible.
I agree, when it comes to Python's moves. I am about ready to leave Python 2, but I'm not going to Python 3.
In my mind, the syntax would be something like this:
sum_class Tree:
case Leaf:
pass
case Node:
data: Any
left: Tree
right: Tree
def size(tree):
case(Tree) tree of:
Leaf:
return 0
Node(_, left, right):
return 1 + size(left) + size(right)
A combination of data classes and pattern matching.Don't get me wrong, I'm all for algebraic data types and think every statically typed language should have them, but it's nonsensical to talk about them in the context of a dynamically typed language.
The author commented elsewhere on this page that the article was mostly a response to missing these features in golang - I think that would've been a much clearer comparison that showed what you were really missing.
Most of the article is spent explaining this sideways; with Haskell you need to hold less aspects in the head because they are either eliminated entirely (purity) or can be deferred to the compiler (types), ... What we learn is that Haskell is easier to implement tree-like structures.
I think this is what most language proselytes are trying to convey in their articles. But don't talk about explicitly. Inevitably they will pick a task that is easy to achieve in the language because the developer environment aligns well for that use-case, and then let the reader infer that this applies to all programming tasks.
Basically we are trying to benchmark humans, without building a proper model of how humans work. The next evolution in programming will be done by properly understanding how we work and interact with the computer.
The tree data type in Rust could be:
enum Tree<A> {
Leaf(A),
Node(A, Box<Tree<A>>, Box<Tree<A>>),
} enum Tree<A> {
Leaf(A),
Node(A, Box<Tree<A>>, Box<Tree<A>>),
}
be more "familiar" syntax than data Tree a
= Leaf
| Node a (Tree a) (Tree a)
?They are tagged unions, but sometimes, the tag doesn’t exist. Or rather, invalid parts of values can be used so that the tag isn’t an extra bit of data, but instead is built into the same space. “Tagged union” gets too deep into only-mostly-accurate implementation details to be a good name.
Sure, tagged union implies a particular implementation that may not always be required, but it's conceptually easy to understand and doesn't have the historical baggage. (Maybe a better description would be strongly typed union? I want to make clear I'm unfamiliar with Rust and just guessing based on the syntax presented.) I think the biggest problem with "tagged union" (or the even longer "strongly typed union") is that it just isn't a good keyword name — it's two (or three) words and fairly long. No one wants to type out 'tagged_union' and from that sense, 'enum' is better. I don't have a better suggestion for you, and IIRC Rust 1.0 has now frozen the language to some extent.
Thanks for trying to explain, I appreciate it.
I think also, likewise, “union” sounds strange unless you have C experience. Many of our users do not know C, and so that name doesn’t help them either.
In the end names are hard.
Happy to, you’re very welcome :)
Indeed! Thanks again.
Are we assigning here with the =? What does the pipe symbol mean? Why pipe instead of another =? Why the weird formatting?
To me Haskell's look is too off-putting. With the Rust example I have a good guess what will the resulting object will look like.
But I know it's just a learning experience. Once I would know Haskell your example looks more elegant to me. I just don't get it from looking at it :)
> Are we assigning here with the =?
Kindof you are assigning what the type `Tree a` is.
> What does the pipe symbol mean?
The pipe is symbolizing or/either here. A tree is either a Leaf or it is a Node with two subtrees.
> Why pipe instead of another =?
You are building up a single type with the pipes, having an extra = wouldn't really make sense when you are thinking about building a type algebraically.
> Why the weird formatting?
The formatting is optional. It is free to be all in one line if you want it that way. For the given example, I would probably make it a single line, but I'm not a Haskell veteran.
In the Rust syntax ',' means both OR and AND.
A Tree {"IS" a Leaf ,"OR" a Node
A Node ("IS" an A ,"AND" a Box ,"AND" a Box.
You could ask many of the same questions of Python's non-C/non-Java like syntax:
- What is "def"?
- Why the weird formatting? (And unlike Haskell, Python's tends to be stricter!)
- Why do I need to write ":" after some lines but not others? It doesn't work like the semicolon in C-like languages!
- What's with the "if __name__ == '__main__'" weirdness I see in some Python programs?
- What's this weird [f(x) for x in ...] syntax? It doesn't look like anything in C. What's with the brackets anyway?
Etc.
Yet Python with its "weird" syntax and constructs is a hugely popular language...
https://www.google.com/search?q=haskell+pipe+operator&oq=has...
https://www.google.com/search?q=__name__+%3D%3D+%27__main__&...
Do note Haskell tutorials and communities abound, and you have excellent online tools such as Hoogle (in which you write the type of what you think you want and it responds with "these are functions with a similar type signature, with their documentation"). It's easy to google Haskell things, just not as easy as googling Python things :)
Do note the type definitions from the example are Haskell 101 and will be covered very early in almost every tutorial, for example Learn you a Haskell.
PS: it's not a "pipe operator" you're looking for. This isn't an operator at all! The "|" you're looking for it's in a definition, and it means a union of alternatives (this type can be "this" or "that" or "this other thing"). If you think about it, this "union-or" is written the same as the bitwise-or from more popular languages :)
GP should note that C++’s unique_ptr isn’t an obscurity.
That would be very misleading since Box represents a heap-allocated owned value
It doesn't matter if some people are confused, because you can just explain what it is in 3 seconds. What's important for such a ubiquitous type is that the name is short.
I'd be interested to know if anyone has done interesting conceptual and/or empirical work on readability. It seems like a very slippery and difficult concept to me. Readable to whom? Readable in the small or the large?
I think there is work on "readability", just in another context: Its called typography and orthography. And I think the gist of it is: Do it like everybody else does, first and foremost, strange and unfamiliar equals unreadable.
I think that's a very different case. Maybe some analogies might be drawn between some of that work and some of the lower-level aspects of reading code (related to syntax noise etc), but code readability, if it's a defensible concept at all, is a far more complex and layered phenomenon than letter & word recognition.
The first thing a researcher would need to establish is whether or not readability even exists as a natural kind apart from familiarity. I don't know the field, so this might already have been pursued somewhere.
Agreed. Note the same applies to Haskell's syntax :)
People confuse "readable" with "based in my knowledge of Java and C, I can't make head or tails of this notation without reading a tutorial first", which in my opinion is not a sensible conclusion.
Looking at the second definition it's not immediately apparent what 'a' is and "Node a (Tree a) (Tree a)" seems just like a bunch of words concatenated by spaces, it has no apparent structure or meaning, unless you're used to writing Haskell/ML/Lisp/etc.
That's false. A programmer coming from Python or Go won't know this. Nobody who hasn't been exposed to the extremely arbitrary generics syntax in Java-like languages will know about <A>.
Node(A, Box<Tree<A>>, Box<Tree<A>>),
is obviously nested (a Node contains an A and two Trees) and the trees are optional (they are contained in a Box, whose purpose is obvious without even knowing the language) | Node a (Tree a) (Tree a)
might or might not be nested, given the cheerful taste for currying and juxtaposition without punctuation that prevails in Haskell syntax, and it isn't obvious what purpose the parentheses serve (just grouping ?) and whether the trees are optional.You need to know that “enum” means that commas in the next section are different than usual, but only one layer deep—check nesting carefully. You need to know about type parameters in either version.
I had a similar experience learning my first ML: I couldn’t even tell how many words each word would gobble up, because I didn’t know the reserved words get. Syntax highlighting helped, and it’s not a problem after a week or two. It’s no worse than figuring out what’s a binary vs unwary operator in C and its descendants.
Also: it’s not obvious to me that Box means optional/nullable. I’d expect it to mean a required non-null pointer to a heap element.
Currying makes no sense in type definitions. It's like saying that in Java you aren't sure if "String name" will run something, "given Java's cheerful taste for running things".
To me, it's obvious the parentheses in "(Tree a)" are grouping things, which is the most immediate (and correct) interpretation, but I'll agree this is more debatable.
(x + 1) / 2
do you assume part of it is optional?The rust example to me isn't immediately obvious that it's an either / or situation other than that must be how an enum works (I know enums from other languages)
This is actually one of my main two beefs with Haskell:
. The language is really interesting from a feature point of view, but the syntax, oh man. Looking at Haskell code when you switch back and forth from a more conventional language (C/C++/JS/Java/C#/Python/etc...) is a major headache. I can switch from C++ to Python without a second thought. Not so with Haskell.
. You can't really predict how something will actually execute, that's left to the language to decide. While in some situations this is a desirable feature, in many, it isn't at all, especially if you're writing performance critical code.2. You can't really predict how something will actually execute, that's left to the language to decide. While in some situations this is a desirable feature, in many, it isn't at all, especially if you're writing performance critical code.
> You can't really predict how something will actually execute, that's left to the language to decide.
If this is referring to order of evaluation, it's better to think that it's up to the code to decide, not the language. In a function application/call like:
f a b c
it is f, not Haskell, that will decide in what order a, b, and c will evaluate by the way in which it uses them. Haskell, the language, merely doesn't force the evaluation of arguments before evaluating a function.With data in the leaves you could easily do something like:
@dataclass
def Node:
data: "Any"
@dataclass
def Tree(Node):
left: Node
right: Node
using the new dataclasses module. You could also add a type parameter to the above if you really wanted to.The pattern matching will need to be done manually though, following python's philosophy of duck-typing.
Edit: An alternative involves abusing the pattern matching that python does have to write things like:
data,*subnodes = myTree
for node in subnodes:
# etc
but whether that's really a good idea is debatable.You need something to represent a completely empty tree. You also need something to represent in a Tree node that "there is no child here". It makes sense to use the same thing for both. In Python and other languages with nullable references you can just use None (or null, or ...) for this. But ML-family languages have no nullable references. You could use option types instead, but that would look pretty complex, something like (my Haskell is rusty):
data Tree a = Maybe (TreeStructure a)
data TreeStructure a = TreeNode (Maybe (TreeStructure a)) (Maybe (TreeStructure a))
(You should be able to use Tree a in the definition of TreeNode, but I wanted to show the "real" structure, which you would also have to care about when pattern matching.)It's easier to have an empty leaf instead. Personally I would possibly call it EmptyLeaf or maybe NoTree instead of just Leaf.
But yeah if you want an empty tree I'd recommend just using None in python. You'll just need checks like 'if node' where you'd otherwise have pattern matching; you could improve QoL a little by defining an iterator over the existing children. If you really want a separate object then you run into all kinds of annoying stuff, including the fact that by default python will allocate separate objects for all of them, which is 1) slow and 2) bad for memory usage.
I'm sure in Python you regularly have uses for empty lists, dicts, and strings. (Probably not empty tuples, but what do I know.) Why would empty trees be particularly strange or exotic?
The concept of an empty tree is not too exotic, but I'm struggling a bit to find a use for it (which is not to say that there isn't one). What makes it different in my mind is that with lists/dicts/strings it makes perfect sense to filter them, which is a bit weirder with trees. It's easy to imagine a scenario where you filter a lists and end up with no items, I'm struggling a bit to figure out what happens if you filter a tree, do you just throw out the entire subtree if one of the parents is filtered out? It seems to me that the answer depends strongly on what you want to use the tree for, and if you know that you will likely also know whether you need an empty tree or not.
Just to illustrate why the 'remove the subtree if the parent is filtered away' option is not obviously the correct one, consider that it also makes sense to just remove the node and return a list of disjointed trees. In that case the 'empty' case is just an empty list of trees.
Almost every "collection" data structure needs to have an empty representation. Trees are no different.
>>> x = [1, 2, 3, 4]
>>> y = list(filter(lambda n: n < 3, x))
>>> x, y
([1, 2, 3, 4], [1, 2])
Same with trees: No removal is needed for filtering, you could implement it something like: def filter_tree(f, tree):
filtered_tree = new_empty_tree() # it's useful if this is *not* None!
for element in tree:
if f(element):
filtered_tree.add(element)
return filtered_tree
Regardless, removal of individual, even internal, nodes from trees is of course possible without removing entire subtrees. The details depend very much on the actual kind of tree, but you can start at https://en.wikipedia.org/wiki/Binary_tree#DeletionEmpty tree can be represented with root node that is a leaf and has no data.
So, for a programming language that has so much to offer, why isn't it adopted more?
Could it be that it doesn't actually increase productivity to such a degree to justify the cost of change?
Legit question, I am not trying to be a troll.
Whether you’re a student fresh out of school, or you’ve been unemployed for 10 years as a sysadmin, or you’re an expert programmer already, Common Lisp is accessible to you. Those examples are real; they’re backgrounds of folks I either used to or currently work with. Being paid helps immensely.
So it’s not that the language is unlearnable, unreadable, or out-of-reach. (The pot-shots that random commenters in forums like these take on the language are usually shallow or even outright wrong.) In some cases, it’s even demonstrated to be asymptotically more productive.
So what’s the deal? I personally think it’s just that productivity isn’t in and of itself incentivizing enough. You know Python and C++, you’re relatively proficient at them, you know how to get the job done with them, why learn something new? Haskell/Lisp won’t get you a job (necessarily), it won’t allow you to do something with a computer that you fundamentally couldn’t do before, and it’ll suck up a lot of your time to figure out what’s going on with them. Moreover, there’s no big organization behind it (like Mozilla or Facebook or Microsoft or ...) so where’s the credibility? A bunch of researchers at some university? A bunch of open-source hackers?
I think one has to be personally invested in becoming a broadly more knowledgeable and more skilled programmer, just for the sake of it, and (IME) most people aren’t like that. I think one has to also have a penchant for finding simpler or more fundamental ways of solving problems, and that has to turn into an exploratory drive. Even if one is like that, learning Haskell is one of a hundred possible paths to improve oneself.
My comment shouldn’t be misconstrued as supporting a mindset of it being OK to just know a couple languages well. I think the hallmark of an excellent programmer is precisely this broad knowledge of not just tools, but ways of thinking, in order to solve the gnarly problems that come up in software.
In my experience it's usually some combination of FUD echoed throughout these comments such as:
Haskell is an academic language and cannot be understood be mere mortals
Well everyone is mortal and some people have learned Haskell so this is obviously hyperbole and meaningless. Is it difficult to understand? Yes... but I have a theory that this is largely because we aren't taught to think in the way Haskell asks us to. Most of us in industry invariably learn Haskell as a second or third language and by then certain patterns and expectations are present in our brains that we hold as truths.
The tragedy of this argument is that people think you have to understand everything in Haskell in order to get started. They point to more advanced features like lenses and profunctor optics and all of this jargon as proof. However I don't ever recall having to learn the entirety of template metaprogramming to get started in C++.
There's really a pyramid of features and the amount of Haskell you need to know to get started is small.
It is too hard to hire Haskell programmers
It's as hard as you make it out to be. There are plenty of people out there who would love to program in Haskell for their day job. When I started posting jobs for Haskell programmers my queue was full, constantly.
The problem is that unless your organization is fully invested in Haskell people in your organization will find ways to hijack this process in order to make it come true by moving the goal posts. It might be hard to find Haskell programmers in your area so they'll say you can't hire remote developers. Say you'll train people up into Haskell and they'll say we don't have anyone experienced enough, or not enough time, etc, etc.
It's not hard to hire programmers. It's hard dealing with people who don't want Haskell to be adopted in your org.
The documentation is poor, there aren't enough libraries, etc
This may have been true more than ten years ago but it no longer holds. The documentation is phenomenal. It seems rough if you're not used to reading type signatures. However once you are familiar with rudimentary levels of Haskell this disappears fast. Haskell is documented. Whenever you add a top-level signature you're adding documentation that not only enriches the program but documentation that cannot be wrong, go out of date, etc.
I think this one gets bandied around a lot by developers from big ecosystems that have deep corporate pockets to fund the development of frameworks, libraries, and tooling. Haskell has been getting more of that but it's still nothing compared to Java/.NET/Swift, etc.
Regardless there are libraries for every common task and ones that can do things with Haskell's type system that you simply cannot in other languages (or can only emulate, poorly, with possibly buggy run-time code introspections, templates, macros, etc).
The real world is messy and Haskell's type system just gets in the way
The real world is messy so why would you want to use a tool that makes incorrect programs permissible?
This comes up from folks who like how easy it is to get started with dynamic languages which are permissive about their inputs. I like this property of dynamic languages too.
What I don't like is all of the run time inspection I have to do in order to know what data is safe to use. At first this doesn't seem like a problem with unit tests and a tight feedback loop. However at larger scales in a code base it makes refactoring and reasoning about high-level patterns much harder. And despite the claim that "type errors are rarely ever the source of production issues anyway," I still find TypeError: undefined is not a function in logs more often than they'd like to think.
The point of all this is that we ship this code confident that we probably nailed ~85% of the problem and we tolerate the risk that there will be some number of errors that will be reported after the fact. Ship early and ship often. However in practice this sucks up a lot of time as a project matures.
I rather like the experience of not having to chase down where an errant null check as missed or where some code mistakenly mutated a shallow copy. I don't even have to think about that in Haskell. I can focus on the business domain logic.
First, it doesn't fit everywhere. The best advice I received is that FP fits best when you can think about your program as a pipe - data comes in, data goes out. The more your program doesn't look like that, the less well FP fits.
Now, Haskell can do non-FP things, but you're fighting against the nature of the language to do so. It's probably better to pick a language that you don't have to fight in order to write your program.
Second (though I'm not sure that this is actually a reason that people pay attention to): Compile time. How long does Haskell take to compile a multi-million-line code base? How long does Go take? (Yes, I know that Haskell may take fewer lines. It won't be enough fewer to make the compile time shorter, though, nor anywhere close.) If you've got a multi-million-line code base, you've probably got a reasonably large team, and you've probably had them for years. If they each compile, say, five times per day, and there are twenty of them, and they've been doing it for ten years, the time spent in compiling adds up to real serious money lost.
Third: For a large team, they won't all be A students. Half the programmers will be below average. I'm not sure that Haskell is a great language for them. I wonder if they can't do more damage with Haskell than they could with, say, Go. (Of course, they could do a lot of damage with C++, too...)
As the author of the post, I don't actually think I can make a compelling argument for why someone should switch to using Haskell in their day job. I don't have real experience in the software engineering industry, and from what little I do know language choice doesn't make a huge difference.
That said, I think it's valid to say that a given pattern is bad, or another pattern is better. I was trying to argue for that in the post in a couple cases that I think Haskell does well.
Servant + Aeson beats any api backend in any language, period. If you combine that with Elm on the frontend you’ve got a great onboarding path for new devs to learn enough haskell to work on the backend.
Of course, for production, ignore lenses, monad transformers, free monads, effect systems, etc. They’re awesome, but the complexity is not worth it in practice at this time.
Real world is not perfect, it is immutable with plenty of side effects. Using Haskell for day to day messy work is not trivial and should not be considered IMO, u nless you have Haskell gurus all around.
I would rather take a dumb language like Go or Java over Haskell for work code.
Perhaps we should use a language that is immutable and can reason about side-effects. Like Haskell?
Good:
After working in a variety of organizations using, typed but also dynamic languages I'm now writing all my back-end code in Haskell. I'm becoming more and more convinced that for multi-year, multi-programmer applications (a language like) Haskell is the only way to make it sustainable, while still being able to add features.
Stephen Diehl has a great writeup on "what he wish he knew when he was learning haskell" http://dev.stephendiehl.com/hask/
It's difficult to say to someone "Just go read books for a couple of months because you need to understand purity, laziness, cross compilation, monad transformers (go read The Book of Monads), 20+ language pragmas. etc etc"
It does however feel like I'm learning useful stuff, and it's a lot of fun to get an executable that runs FAST.
I'm constantly sad that Standard ML is so outdated. No good tooling, no real unicode support, etc.
At least we have ocaml and F#.
"While it solves the problem of methods, and the mutation problem, it has a serious bug. We can’t have None as an element in the tree!"
Uhm... what?! Why not? You check that your left and right are None and if they are it is a leaf. And if they are not it is not. What your data value is doesn't matter and you can have as many None values in the tree as you like. Your tree doesn't need leaves to define where it ends, it ends when there are no more branches.
Or the following:
x
/ \
/ \
/ \
y None
/ \ / \
/ \ / \
Leaf Leaf Leaf Leaf x
/ \
/ \
/ \
y None
/ \ / \
/ \ / \
Leaf Leaf Leaf Leaf
Looks like this. x
/ \
/ \
/ \
y None
There is no reason to explicitly define leafsCould you specify what you mean in code maybe? If you don't explicitly define leaves than how can you represent the empty tree?
https://gist.github.com/MadWombat/798c6d993a7d2ac4ac74d6624a...
Anyway, I'm pretty sure what you've said here was addressed in the article. The version you've presented here is exactly the first alternative I showed, which isn't as good as a version written with ADTs because you can't represent an empty tree.
Also, if I really wanted to implement Leaf type, I would probably do it via __new__ and a bit of metaprogramming.
In what language? Did you mean ">=" or "<="? They mean the exact same thing in Haskell.
> "|" means an OR
It also means the same thing in Haskell.
data Maybe a = Nothing | Just a
"data of type `Maybe a` is either `Nothing` OR `Just a`." max a b | a > b = a
| otherwise = b
"max of a and b is either a when a > b OR b otherwise." odd x || x >= 5
"x is odd OR x is greater than or equal to 5"A cool thing about Haskell is that it lets you define new operators. In the parsec package, you get the operator <|>, which keeps the meaning of OR but works with parsers.
csvCell = csvQuotedCell <|> csvNonQuotedCell
"a CSV cell is either a quoted cell OR a non-quoted cell".> I'm cringed to see $s and \s in a program code that makes it look like LaTeX
I don't think they're so common that the comparison is valid. $s implies Template Haskell, which should be used sparingly. \s implies lambdas, which are nowhere near as common as the use of backslashes in LaTex.
> I also prefer the boundary of terms and expressions to be consistently marked up with parenthesis.
The way you wrote this, makes me think you prefer to write 2 + 5 * 2 as 2 + (5 * 2). You can do that, just like in any other language. I don't think it's common to add parenthesis redundantly in any language though.
If you actually meant something like preferring f x y to be written as f(x, y), it's not just a matter of taste. It's so the syntax makes sense with partial application. You can do
f x y = ...
g = f x
h = g y
and it would be more confusing to write f(x, y) = ...
g = f(x)
h = g(y)I think it's more likely to mean function application ...
$logInfo $ pack $ show (a, b, c)
The one without a space is a Template Haskell splice and the ones with a space are using the function application operator. If logInfo didn't need to be expanded by Template Haskell and we deactivate that extension, we could write: logInfo $pack $show (a, b, c)
But I doubt anyone would because the implied parentheses around those operators are: (logInfo) $((pack) $((show) (a, b, c)))
[1] https://downloads.haskell.org/~ghc/7.8.4/docs/html/users_gui...[2] https://github.com/yesodweb/yesod/blob/c8aeb61ace568cdc2bc81...
I’m simply more impressed by array-oriented languages :)
The parts of Haskell that make it a good language aren't the things that you can just write a tutorial for. They're about software engineering, not code snippets.
Purity and immutability remove entire classes of bugs caused by spooky action at a distance. When you assert this, people claim "I don't have those bugs", forgetting about the time someone else changed a function they wrote to mutate one of its arguments, breaking code three steps up the call chain.
Parametric polymorphism documents what information a function cannot use within its definition. If you point this out, people ask what good that serves. There's no way to explain how much easier it is to get things done when you can write a function and know that no matter what values are passed in, there cannot be special cases that trip you up.
I see people try to explain why the `Maybe` type is better than null values, and have their explanations rejected with "You still have to check for it. All you're doing is changing the syntax of the check." I've seen variations on that theme in maybe 10 different HN threads over the last 6 years. When all you talk about is ADTs and pattern matching, why would anyone ever look at the bigger impact of the type system? The relevant detail here is that an `Integer` can never be null, not that you use `Maybe Integer` to talk about potentially missing values.
Further in that same direction, I see people say that the `IO` type just complicated things because your program has to do I/O anyway, so it always needs to be in `IO`. This is exactly the same as the `Maybe` problem, but that similarity is even further from being addressed by articles about pattern matching and ADTs. No, the similarity isn't "monads". Anyone who talks about them here has missed the point entirely. The point is that parametric polymorphism completely prevents distinguishing IO values from non-IO values, so code that's not written to work with IO values cannot do IO accidentally.
There are a lot more cases, especially when you get into more sophisticated things possible in the type system using ghc extensions like generalized algebraic data types or higher-rank types.
But all of the reasons you should be using Haskell in reality come down to practical large-scale software design concerns. The language lacks features that make several common classes of bugs possible. It makes several other common classes of bugs take a lot more work to implement than the non-buggy way to solve the same problem. These aren't things you can just write a short article about. They're things that require years of experience and introspection to see are even problems, and a willingness to accept that a lot of the problem is the ecosystem, not an individual failure to execute. None of that fits in an article.
I think articles about how great pattern matching and ADTs are make the language look worse, because anyone with some experience can say look at what's actually happening and say "I can do that in <other-language>, Haskell clearly doesn't have anything to offer." In other words - stop writing these articles. They drive people away from Haskell, not encourage them to look at the good parts.
IMHO, people brag about type-classes too much with respect to the particular type-classes that embody category-theoretic constructions, and not enough about their original application to ad-hoc polymorphic overloading. If I can think of a Task X which I have to do for a variety of somewhat different types in somewhat different ways, but which is used in a polymorphic way, then I can absolutely make a type-class out of that.
It's like how people think the big secret to object-oriented programming is inheritance, but actual OOP experts tell you to prefer interfaces (which are almost-but-not-quite just like type-classes!) and abstain from building large inheritance hierarchies.
People shouldn't downvote you: the sarcasm bit is funny, and it does actually point out to one of the major reason Haskell is not gaining more acceptance in spite of its many great features:
. the syntax is completely alien (to the point of turning quasi APL-ish in some cases) and scares away potential users.
. the community is so focused on esoteric stuff that the "how do I do X that's super easy to do in traditional languages" is completely missing from the conversation.the community is so focused on esoteric stuff that the "how do I do X that's super easy to do in traditional languages" is completely missing from the conversation.
Such is life.
In the same way, Haskell is a good language, it's a moral language. Of course this causes some pain, but it's worth it.