Last year I shared a link here about the blog post that was inspired by this talk. Now that the recording is finally available, I wanted to share it with the community as well. I hope you all don’t mind me sharing another piece of my own content here- I hope you find it interesting!
I enjoyed reading that post. Thanks for sharing it.
You might want to update the blog post with the link to the recording as is mentioned in the Prelude :)
As a non-Haskell user, it’s amazing how poorly it handles JSON parsing. It’s the ugliest and most complicated JSON parsing of any language.
Why do all of the libraries insist on de-serializing into user-defined Haskell objects? IME that’s almost never worth the hassle, and it’s 100x easier to just treat the incoming JSON as its own data structure and objects.
You absolutely can just parse a json object into a map of keys to json values. That’s how a lot of parsers start out.
The problem is that you need to write functions that work on some of the values inside of that json object. So what do you do? You can pass every function the entire json blob and have it try to extract the elements it needs, and possibly return an error if something was missing or invalid. That leads to a lot of duplicated effort though, since a lot of your functions will be looking at the same data. It’s also a pain to write a bunch of functions that might fail.
An alternative would be to write one function that takes the json blob and tries to get all of the fields that it needs, and if one of them is missing then it fails. If everything exists, then it can call all of the functions you need to call. That would work great as long as you know ahead of time what fields you need and what functions you want to call, but it’s also a bit messy.
It would be idea if you could just say “here’s a set of common fields that I want to work with, and I’ll check these fields once ahead of time. If they are all present and valid, then I’m good to go, otherwise I’ll report an error”. That’s exactly what these json libraries are doing. You write a type that says “here’s everything I expect to get and how I expect it to look”. Then you do the check once to make sure your json object has everything you need. Once you’ve done that, the rest of your code can happily assume that it’s getting valid data and you don’t have to worry about errors.
I guess my point is that a runtime error for a missing key is just as useful (and probably more readable) than a runtime error from typing, so why bother with all of the typing and conversion boilerplate?
There’s no reason for readability to be impacted either way, and in general the existence of types aren’t de facto boilerplate. Handling the errors up front simplifies all of the code that comes afterwards because you are assured that you have good inputs. Ad hoc error handling spread throughout your application causes a lot more boilerplate because you end up having to handle errors many more distinct places. It’s also a lot more error prone (you can forget to handle them) and makes testing harder (you have to test every function to make sure it can deal with errors)
I’m not arguing against up-front verification or for dispersing error handling throughout the code, though.
What I’m saying is that it’s a little clunky and awkward to use the type system for that purpose. IMO, of course.
If you don’t like types for that then you don’t really like types. That’s fine I guess, but I think it’s wrong to say the approach is clunky. It’s not, it’s just not aligned with the way you like to write code.
Nothing insists on forcing you to work with your own data types, but not doing so is, frankly, insane. We could pass around the Value
type everywhere, but anywhere I use it, I have to revalidate all my assumptions about the data: is foo.bar[3].baz
a number? and is that number an Integer without any fractional part? Is there a string at home.page.next
that is a valid URL?
Most Haskell developers believe strongly in the parse, don’t validate mantra - if I parse my data and force the data into only valid shapes, then I know statically that I never need to re-check anything. If I have a Value
, I know absolutely nothing other than I received a bytestring which parsed as valid JSON. if I have a CreatePayment
, I know I have a field called createPaymentIdempotencyKey
which is a valid UUID, I have a field called createPaymentAmount
which contains a valid MoneyAmount
, etc. I never need to check those assumptions again - I can’t have got a CreatePayment
unless I had valid data.
This also makes applications faster, I don’t have to do error handling all throughout the app - to look something up by its idempotency key, I already know I have a valid UUID, so I just serialise that in by DB interface, I don’t need to first extract it, hope it exists, deserialise it, ensure it’s the right format of string, then convert that; that was all handled by the parser.
Dealing with JSON is fine for toy projects, but you need to get rid of it as soon as possible in anything doing real work. Applications become much simpler when you build a core application which only operates on valid data, and then wrap it in the code that protects it from the incorrect data, like an onion; bytes -> utf-8 -> json -> my data -> business logic data validation -> business logic.
None of that makes the code not ugly, klunky, and awkward, though.
And at the end of the day, if “foo.bar[3].baz” isn’t a number and the code expected it to be, it’s going to be discovered at runtime, regardless of the programming language.
The difference is how much extra code has to be written to detect and handle that condition, and that’s where Haskell falls down compared to other languages, IMO.
Here’s the valid Haskell version of that:
key "foo" . key "bar" . ix 3 . key "baz" . _Number
I write stuff like this all the time. Really not awkward!
On the contrary, absolutely no extra code is written, but in your preferred style, it’s scattered all throughout the codebase - I can never fully know if all my assumptions have been checked everywhere they need to be. But if I need to add a new constraint to my Haskell code, I know exactly where I need to do, to the parser.
I’m guessing you don’t write a whole lot of commercial or production software?
On the contrary, absolutely no extra code is written, but in your preferred style, it’s scattered all throughout the codebase - I can never fully know if all my assumptions have been checked everywhere they need to be. But if I need to add a new constraint to my Haskell code, I know exactly where I need to do, to the parser.
I’m not sure you know what my “preferred style” is, just that I don’t like the burdensome, type heavy Haskell way of doing it.
I’m guessing you don’t write a whole lot of commercial or production software?
There’s no need to be condescending. I’ve written enough commercial and production software to realize Haskell adds extra up front development cost but doesn’t eliminate the most expensive bugs.
I like seeing examples Haskell in production, as I would like to understand the distinct advantages it offers in real world applications. After having written a decent amount of Haskell and quite a lot more of non-lazy plain ML (SML and OCaml), my naive and biased impression is that:
The Haskell ecosystem is a bit lacking in many areas outside compiler construction: https://github.com/Gabriella439/post-rfc/blob/main/sotu.md. Things have improved in areas such as web applications, though.
The language has too many extensions and feels a bit too big and fragmented, like C++. This also leads to a significant cognitive overload. If I stop writing Haskell for a few weeks, it’s hard to start back. It’s also hard to read code written by others if it uses extensions I’m not used to.
It is not easy to find employers / employees that use Haskell. However, this might be improving rapidly with the popularization of remote work.
I feel robustness is a bit hyped. If I want formal guarantees, something like Agda or Dafny is the way to go. Liquid Haskell does not feel nearly as mature. Using OCaml or Erlang, I feel I can write large programs that are reasonably correct more quickly.
With that said, my experience in Haskell is limited, and I would love to hear someone telling me I am totally wrong. The language is very interesting, and I really want it to succeed. Are there any advantages that offset these disadvantages?
I have been using Haskell professionally for quite a few years now- although not exclusively and I do have experience with other languages so hopefully my perspective here isn’t too skewed.
Overall, I think that Haskell is a good choice for most general development work. There are tradeoffs of course, but the type safety is definitely a win for refactoring and coordination in larger teams. Most Haskell programs aren’t encoding everything at the type level to prove correctness so you do still have some bugs, but the balance between “really good correctness guarantees” and still being fast to develop with is favorable to Haskell over a lot of other languages.
The ecosystem is good enough for a lot of things. There are certainly gaps, but I think people over-estimate the cost of them. In reality I’ve found that the benefits of Haskell outweigh the costs when I need to write a library in most cases.
The number of extensions and different styles of code is a thing. At places I’ve worked the code tends to converge toward a shop style, even when it’s not enforced. I do find the difference in styles throws off less experienced people, but I’m not sure it’s much worse than dealing with different frameworks in other languages. I do think the C++ comparison is apt in a lot of ways- the “never met a feature it didn’t like” was something that always bothered me about C++ and I can see how Haskell could feel the same way, but in practice the set of features feel better integrated in a lot of cases, and the fact that you need to use a language pragma to enable them gives you an easy to search thing to learn about the feature.
For hiring, I’ve found it to be a bit inconsistent. Overall my impression is that there are more people who want to use Haskell than there are Haskell jobs, so it’s not hard for a decent company who is committed to Haskell to find good developers. It’s much harder for a company that is mostly working in other languages to attract developers to a Haskell team, because Haskell developers tend to be very averse to risking bait-and-switch jobs.
In the end, I think Haskell is a great option for anything being built by a team who like Haskell, and it’s a compelling enough language that a curious team would benefit from trying it out. That said, the biggest factor in language success in my experience is matching the language with the team, and if you have people who don’t want to work with Haskell I don’t think you’d be successful in forcing it.
If you go on Github and sort by stars and filter by language, Haskell doesn’t come up very high considering how old it is. I think Haskell has had an outsized influence on other languages, particularly Rust and Swift, but it just isn’t great for general purpose work.
I think Haskell is a great choice for general purpose work, but it’s historically not been a great choice for an open source project because there have been fewer Haskell developers out there who would want to contribute. The result has been that a lot of open source Haskell work has been on libraries and other things internal to the Haskell ecosystem. There are a few notable exceptions (pandoc and xmonad are probably the two most widely recognized examples).
Over the last few years there’s been a shift toward trying to file down the sharp edges on the Haskell user experience, and a lot of things have improved. It took a while, but I think Haskell’s ecosystem is starting to catch up with where Haskell as a language has been for a long time, as a useful way to build general purpose tools.
I remember finding it noticeable that Hackage seemed to have a higher ratii of libraries to programs on it than, say, PyPI.
I assumed this was a sign that most of the things were being used behind closed doors. :)
I have a 2020 Thelio desktop (AMD; thelio-r2 running GNU/Linux). It isn’t terrible, it’s reasonably quiet, it’s a reasonable size, so far customer support has been great, and i’d buy a Thelio again. But i have three complaints:
Any suggestions for my next desktop? I’d like something comparable to the Thelio in terms of power and size and quietness, but with some front USB ports, and a high end graphics card that can run at full power, and a minimum of fuss (ie “it just works”, eg sufficient cooling so that it doesn’t ever overheat and shut down, and my hard drive doesn’t crash after two years). I’d prefer pre-built but i’m willing to build it myself; with the 2020 Thelio i went pre-built because i figured if i did it myself i’d screw it up and buy some component that doesn’t work well with GNU/Linux, or put the thermal paste in the wrong place, or not provide enough cooling, or something. But since I didn’t achieve “it just works” with pre-built anyways, maybe i should just build it myself?
Come to think of it, i should just ask System76 support if it would be feasible for me to replace the case on my 2020 Thelio with an aftermarket case with a side hole for a fan, and front-facing USB ports.
insufficient cooling. I wonder why they didn’t just put an additional fan in the side of the case?
it can’t handle the preinstalled Ryzen 9 3900X graphics card that i purchased with it; when under heavy load for about 20 minutes (for example, when playing a game), heat builds up and causes the rest of the system to shut down. I have to throttle the graphics card by about 30% to prevent this, at which point it’s not that great, and one of the supposed attractions of a desktop over a laptop was a great graphics card. I was hoping that if i bought a prebuilt desktop rather than building one myself, i would avoid this sort of problem.
I’m having this exact same problem and have since I bought the unit. This is SUPER sad since I otherwise love the machine but what the hell is the point of buying a monster desktop that you can’t even push to anything like its full potential.
I kinda gave up gaming on the beast because running No Man’s Sky at anything but low detail/res settings causese the case to get BLAZING hot to the touch, and then the system shuts down.
And now I’m stuck for at least another 5-6 years because my desktop budget needs to refill :)
If I’m buying a desktop in 2022, I’d probably go for off-lease business desktop if I didn’t care much about graphics (as most are SFF). They’re very thick on the ground, fast, cheap, and low-trouble. Whitebox is very tempting, but I’ve had so many miserable and hard-to-debug issues with them.
Of course, desktop Macs also put a wrench into things value wise. Next time it comes down to upgrade, I’m considering a Mac.
I’m sad to hear this. I bought two of their laptops (over the years) and both have been extremely strange and unreliable beasts, but I was hoping this could be chalked up to their reluctance to design the laptops themselves. (Apparently they are re-branded imports.) Given the freedom of designing a whole desktop PC from components, they should have been able to do a much better job.
my preinstalled internal NVMe SSD drive (Sabrent Rocket) crashed in 2022. I noticed it’s mounted right under the GPU, so given the problems with failing to dissipate the heat from the graphics card, i suspect this got too hot over time too.
This is an annoying anti-pattern common in many motherboards I’m afraid. I believe it’s because NVMe connects directly to the PCIe bus, and so the slot for it tends to take the space that would otherwise be occupied by a PCIe card. A double-width GPU in an adjacent slot will then happily sit right over it. It worked just fine a few years ago, but NVMe drives and GPU’s both now tend to run hotter than they used to.
it crashes from time to time (the system freezes and the fan starts running at full speed; probably not their fault; but my previous computers (laptops running GNU/Linux) didn’t have this problem – therefore I suspect it’s some problem with the GNU/Linux drivers for the AMD GPU)
Oh my god. I have this exact problem for the entire lifetime of my AMD card. It’s not a Linux problem, I’ve hit (and can deterministically reproduce) this problem on Windows too. The only thing that kinda worked was tuning the fan curves really aggressively to the point that the fans spin up at the slightest 3d rendering. I’ve tried a lot of stuff up to re-pasting the card and nothing helped.
Not buying an AMD card again.
A good method of guessing how probable cooling problems are with a given computer is look at how much ventilation the case has. Small windows and/or grilles in corners? Trouble. This is just a fact and all case manufacturers create cases like this for some reason. For example, I love the aesthetics of Fractal Design Define cases, but they run hotter and louder than their Meshify cases that have a full mesh front panel.
I think the best move is to use a customizable gaming-focused company like iBuyPower, where you can pretty much spec out whatever you want and they put it together for you, to have them source roughly the same hardware as System76 uses and put it in a chassis with better air vents + fans + front ports; then you install Pop!_OS (System76’s distro, and coincidentally by far my favorite consumer-focused Linux distribution!) on it yourself when the fully built rig arrives in the mail.
As long as the underlying CPU/GPU combination is the same, and you’re using a motherboard that has compatible WiFi and Bluetooth, I think you’ll end up with very similar Linux/Pop!_OS compatibility, but better thermals, performance, and longevity. System76 seems to optimize for having an aesthetically-pleasing chassis over thermals, and if you don’t care about the former (or enjoy gaming-style aesthetics, where thermals are an important design consideration) you can get a lot better of the latter. You can probably even control any RGB lighting you’ve had them set up for you, if you’re into that sort of thing, via OpenRGB!
One thing I’d stress though: specifically for the motherboard, make sure you’re checking for Ubuntu compat, not “Linux.” WiFi/Bluetooth drivers ship in the kernel, so while the latest kernel may have support for the drivers, that kernel may not yet be used in the latest version of Ubuntu/Pop!_OS. Since Ubuntu is extremely common amongst Linux distros, checking for Ubuntu compat should be fairly easy via Google, and if it’s Ubuntu-compatible it should be Pop-compatible since they use the same kernel.
And by using something like iBuyPower you have roughly the convenience of a prebuilt, minus having to install the OS yourself and having to do an upfront check to make sure you’re using a motherboard with WiFi and Bluetooth that work with Ubuntu.
You could also just build a desktop yourself! It’s not thaaaat time-consuming. But if you’d rather not spend a day clicking and screwing parts together, and managing a panoply of hardware orders from various websites, that’s valid and there are other options.
I just took a look at iBuyPower on your suggestion, and it seems like they don’t really address the biggest problem of doing a custom build: the research required to pick all of the components out. Snapping the parts together is easy enough, the benefit of a pre-built is not having to select all of the components individually. It does look like iBuyPower has some pre-built machines, but if then you are back to the “might not work with Linux” problem.
A lot of gaming focused companies also, frustratingly, seem to top out at 32gb of ram these days. That’s fine for gaming still, but increasingly not fine for a lot of other workloads. I know ram is upgradable later, but you often end up paying for ram you need to throw out (or deal with reselling) because they do 2x16 or 4x8 configurations.
…I keep forgetting that people actually buy desktops instead of building them from parts. It’s like I’ve been getting furniture from nowhere except IKEA for 20 years. It makes perfect sense but the cognitive dissonance is real.
I’m planning to buy my first desktop system in a very long time later this year, after 3 iterations of using a laptop as my primary computer. I was looking into building myself and it seems like the process of assembling them has gotten immensely easier over the years (I remember having to set jumpers and know about interrupts to get my hardware working, so I guess my baseline standard is pretty low), but I’m going to go with a pre-built mostly because of the choice paralysis involved in picking all the components. Sites like pc part picker are great for ensuring you get compatible hardware, but it’s just a pain to have to go through a ton of reviews and read the specs for dozens of different options for every single part. When I realized I was on day two of reading about the details of different cooling fan options I just decided the I’d much rather throw an extra $1k in vendor markup to buy a pre-built.
Now, of course, I’m having the same problem of trying to select a decent vendor. I was planning to go with a System76 Thelio, but the comments here about thermals have me a bit worried so I might go back to the search. As a Linux user I still find buying brand new machines a bit stressful because you never know when someone will ship a machine with a critical component that doesn’t have drivers, and it typically takes a while for the forums to fill up with enough comments to give you reasonable confidence about a particular machine.
May I offer https://www.logicalincrements.com/ as at least a basis for comparison? I don’t know if it will actually help the analysis paralysis, but for me it gives me at least a reasonable selection to start from? You can compare it against what vendors offer, at least, if you want a 3rd party comparison.
I use linux as a daily driver too and fortunately, if you buy current-0.5 generation hardware there’s basically always drivers for it these days. Desktops have it much better than laptops in that regard, though laptops these days are still actually mostly reasonable. The most driver incompatibility I’ve had with Linux in the last few years was a Dell XPS 15 that needed me to build display drivers from source to be able to adjust display brightness. Ubuntu 20.04 fixed it for me.
I’m being a troll, but this just looks like: “These are the 21 projects written in Haskell used by more than 1 person,” which is just unfortunate. I wish there was larger adoption for Haskell (but I am as guilty as everyone else).
EDIT: whoosh
I and a group of people actually use a few of these. The most famous one being pandoc, cursed by the arch maintainers for its huge collection of dependencies which they ship as separate packages. I wrote my master thesis using pandoc. I believe Hakyll is supported by many static-site hosting services. xmonad is a highly loved alternative to wms like i3 and sway.
I and a group of people actually use a few of these. The most famous one being pandoc
I’d describe both pandoc and shellcheck as Haskell success stories. Pandoc is widely used in my circles.
There was a great command-line podcast downloader written in Haskell called hpodder, which had a bit of uptake in the blind Linux community years ago. I think it has suffered software rot. I couldn’t get it to build when I last tried. A lovely, reliable program though.
Do you think there’s still demand for that? I’d love to see more Haskell projects that are serving a particular userbase, and if it just needs to have some bitrot issues addressed and be updated to work with a newer compiler and libraries I could probably fork it and get it updated.
Do you think there’s still demand for that?
I honestly don’t know. Probably most people who were using it at the time have moved on, but it’s impossible to say.
I’m still using hpodder - I tried to fix some utf-8 issues a decade or so ago, but couldn’t figure out how, so I just wrapped it in a couple off shellscripts :-)
I’m still using hpodder - I tried to fix some utf-8 issues a decade or so ago, but couldn’t figure out how,
Have you built it from source in a while, or are you just using an old binary that still works?
I think it’s just a pretty old binary I have lying around. The timestamp is from January 2022. I also have an hpodder.old from 2019, so perhaps it is newer than I thought.
The newest local commits I have in my clone of the original hpodder repo are from 2017:
* 6429e6a 2017-10-18 Use FlexibleContexts
* 7ffaaf1 2017-10-18 Add --compressed to curl options.
* 782effb 2015-06-06 Revert "Fix stripping of unicode byte order mark."
* 737c69e 2015-06-06 Add network-uri to Build-Depends.
* f78f053 2015-06-02 Apply hpodder-1.1.6-unix-2.7.patch from the gentoo-haskell repository.
* dd32cb8 2015-06-02 Apply hpodder-1.1.6-haxml-1.22.patch from the gentoo-haskell repository.
* 13364a3 2015-06-02 Apply hpodder-1.1.6-base-4.patch from the gentoo-haskell repository.
Let me see if it builds (I’m on Debian 11 (stable, bullseye), GHC 8.8.4). Yes, it does build, with a couple of warnings.
I can share the repo if anyone wants to clone it.
Here it is: https://koldfront.dk/git/hpodder/
Here it is: https://koldfront.dk/git/hpodder/
Thank you.
As said below, there seems to be a high correlation of “successful / useful software project” to “written in Haskell.”
I use xmonad on linux since more than a decade and I def. use shellcheck whenever I write production level scripts.
Yes! The point I was making is that there’s a high correlation of “successful / useful open source project” and “written in Haskell.” The problem is that there is a fractional number of projects written in Haskell compared to other languages.
I’m really interested to see how generics turn out for Go. I used Go quite heavily from 2015 up until 2019, but I’ve not kept up with the language much over the last few years. My impression is that a lot of people who might have cared about generics have moved on to other languages and most of the go community now are either indifferent or actively hostile to the idea.
I still have a bit of a soft spot for go though, even though it’s not thought of very highly among most of my peers, and if I can find the time I’d like to refactor some of my old open source code to see how it feels with generics.
Sometimes I feel like I’m the only Haskeller who doesn’t dislike Go; glad to know I’m not actually alone :)
As someone who has his foot a bit more on both sides still, I will say I don’t think it’s true that there aren’t folks that are excited about them (even besides just me). I’m also optimistic that most folks who are indifferent it’s due to lack of experience, and I suspect most of the haters will come around; my memory from the mailing list discussions is that most of the pushback came from memories of C++ templates; I think if the devs can keep compile times under control, people will chill out when they start to grok how universal types differ from that.
I do a lot of work that articles about hiring claim nobody ever does. The most recent project I shipped at work involved writing a highly domain optimized dynamic programming algorithm, along with a few data structures that would probably be considered at least somewhat “leetcode” like (domain optimized variants of suffix trees and BK trees, and a couple of linked list variants designed to exploit caching better for our access patterns).
Even so, I’m not a fan of leetcode questions and asking these sorts of things during interviews. In a world before leetcode, I wouldn’t expect an average developer to go into a question like this well prepared even if they were strong with CS fundamentals, because so much of this naturally arises from spending time with the problem. CS is big enough that if you’ve been working in a different class of problem for a while, you’re just not going to immediately make the right connections and insights. At best you end up testing how similar the most recent work someone was doing is to the question you asked- rather than understanding their general competence.
Now that we have leetcode the problem is different: the “just grind leetcode” culture removes whatever small signal there might have been in talking through CS problems and replaces them with rote memorization in a form that most people will fail to generalize to real world problems.
I think CS is important for developers to know (whether they have a CS degree or not), but leetcode just isn’t the way to evaluate that.
Partly it’s that the sort of algorithm-challenge stuff involved in typical interviews is genuinely really rare to need to do as a constant day-to-day thing, so making that the default skill set to test for is not great. If you do need to do it regularly, OK, but that doesn’t make it true for the rest of us.
Partly it’s that even places like Google have long acknowledged that doing well on these types of timed/competitive algorithm challenges negatively correlates with on-the-job performance, so it’s not even providing a useful signal.
But mostly it’s that the conditions under which such things are posed are horrific and unrealistic. When you need to roll out a dynamic programming solution, for example, I highly doubt that your employer locks you in a room with no access to a proper editor/IDE, documentation, or other resources, and demands you do it syntactically perfectly, on the first try, on a whiteboard, within 20 minutes, while explaining every step of it to a hostile interlocutor who keeps butting in with questions, or else you’ll be fired on the spot.
But that’s the way the interview is run. Can you at least grant that maybe this is not good?
(and to be clear, that’s without getting into the other massive issue, which is whether these algorithm challenges really are synonymous with or usefully test “CS” or “CS knowledge” or “CS fundamentals” of any sort)
Just to be clear, I agree with you. I was saying that I do a lot of this kind of algorithmic development in my day to day work and I still think leetcode is a bad idea. I think general CS knowledge is good, but quizzing people on implementing algorithms in an interview isn’t. I’d much rather talk through concepts at a high level and learn about their interests and how they’ve been able to apply ideas to their work, and maybe have them teach me about something they think is interesting or useful.
I’m wary of “general CS knowledge” or anything really that emphasizes “CS”, simply because a lot of the current horror started with FizzBuzz, which was invented because of how many people had strong “CS” but couldn’t actually write practical code.
To some extent that’s the holy war about CS versus software engineering and whether to just teach a bunch of theory or try to do more applied stuff and practical techniques. But also as someone who came late to any sort of proper “CS” knowledge — mostly because I knew someone who was TA’ing university courses and became their study buddy to make sure they knew the material — I have to say that across a long career I can’t really think of a time when that knowledge and only that knowledge helped or would have helped. It’s a bit like the people who insist that you have to take a bunch of math and generally certain very specific courses like linear algebra, because what it boils down to is that math was how they learned abstract reasoning, so they decide it’s the only way anyone can ever learn abstract reasoning.
It’s easy to “blame” (or “credit” depending on your disposition) modern tooling and development ecosystems to for making it easy to pull in dependencies (and their dependencies) for every problem that we might run across, but I think that’s only part of the cause. Another factor that I think isn’t discussed much is how modern development processes lead us toward accumulating a lot of dependencies.
Code review in particular seems to bias people toward bringing in dependencies for everything, largely because adding dependencies is rarely subject to scrutiny during the review process. I’ve never in my life had a reviewer comment on the code inside of a dependency I added, and I could count on one hand the number of times I’ve even been asked about the choice of a particular dependency that I added to a project. On the other hand, if you implement some data structures or any sort of marginally complex algorithm yourself, it’s almost guaranteed that you will be under fire from reviewers. In most cases you need to defend the choice to implement the code in the first place, rather than adding a dependency. Even if the reviewer lets that slide, you still have much more code that must be shepherded through the review process. Even if the cumulative weight of many dependencies is a burden, even if avoiding those dependencies is easy enough, the process by which we ship software seems to strongly encourage external dependencies to remove friction.
It’s funny how that might differ from project to project and between ecosystems.
On my last few jobs, the amount of dependencies has been no greater than five or six (I think it’s four on my current one) — half of which are mandated by business (basically analytics and integration with CRM). Any new dependency would be preceded by a serious discussion if it can’t be implemented trivially in a day’s work, and we make sure to wrap said dependency in an interface so it can be mocked or swapped out with minimal code changes.
largely because adding dependencies is rarely subject to scrutiny during the review process
And you’re just talking about the decision to add the dependency.
What if we included reviewing the library code as well? “Does this library we’re adding clear our own, in-house, bars for quality?”
I really wanted to keep the article focused on debugging rather than spending more time on unicode terminology than was necessary to follow the example, but I guess I could have added that term in. I noticed a couple of typos in the article as well, so if I have an opportunity to make edits I can try to add an aside if you think the article is worse off for the terminology I used.
I find that any use of the word “character” can be dangerous in most contexts, but especially since you have to basically explain grapheme clusters anyway (talking about ZWJ etc) it seemed odd to not at least name the thing being explained.
Hey everyone, I hope you enjoy this article. I wanted to disclose that I wrote this article in part to promote my book Effective Haskell. I also wrote the article because a lot of people find debugging in Haskell to be tricky, or don’t expect that you can do normal print style debugging in a pure language. I thought having a tutorial for how to use common debugging techniques would be helpful.
printf "%c (0x%x): %s: Count: %d -> %d" char (ord char) (show $ classifyCharacter char)
count count
If we run this code, we get exactly what we’d hoped: we’re adding and subtracting when we should be, and we correctly count 4 characters in the file:
I’m pretty sure what you actually wanted here was:
printf "%c (0x%x): %s: Count: %d -> %d" char (ord char) (show $ classifyCharacter char)
count newCount
Nice article though, I think that Debug.Trace is very underrated when it comes to debugging Haskell code, particularly recursive code. I’ve used this many times in the decade plus I’ve been using Haskell:
foo x y
| traceShow ("foo",x,y) False = undefined
| otherwise = foo y (x+y)
which gives an output like:
("foo",1,1)
("foo",1,2)
("foo",2,3)
("foo",3,5)
...
I wish the project success- there’s a lot of innovation still to happen in programming languages and I’m always glad to see new ones getting built.
That said, I’m a little skeptical of the recent trends in optional typing. I don’t dismiss the idea that we might be able to make it work, but my experience so far has been that optional typing ends up being a worst-of-both-worlds scenario. You don’t really get the benefits of a strong static type system, since you have to deal with potentially untyped values, but you still spend some amount of time appeasing the type checker for the code that you did end up trying to make well typed. In the best case scenario you end up with an un-typed language, but more often than not they turn out to have all of the safety and reliability of untyped languages with the quick development time and short iteration cycles of a typed language.
Agreed! The following struck me as a pretty typical “pitch” for optional / gradual typing, but it doesn’t make sense (to me):
When prototyping, don’t specify any types, and Stanza will behave like a dynamically-typed scripting language. But when confident in your design, gradually add in types to make your code bulletproof.
I tend to think about programs as transformations applied to data, so the literal first thing I think about is the types (the data). I don’t write the transformations until I’ve got the types mostly worked out anyway, so there’s no point at which I want to omit the types.
I agree. I see optional typing as a way of adding some benefits of static typing to dynamic languages when you can’t start over from scratch. I don’t see the point of starting a language from scratch with optional typing, unless there is some high-value externality that induces it (Typescript wanting compatibility with Javascript, for instance).
my experience so far has been that optional typing ends up being a worst-of-both-worlds scenario. You don’t really get the benefits of a strong static type system, since you have to deal with potentially untyped values, but you still spend some amount of time appeasing the type checker for the code that you did end up trying to make well typed
As far as I can tell, every optionally typed language is simply going about it wrong: The default is that the programmer has to prove something to the type system, otherwise you get a static type error. A better default would be type annotations that cooperate with a runtime contract system for which only guaranteed runtime failures are converted to static errors. For cases where you do in fact want to force static checking, you could signal some assertion as static.
Another way to think about it is that type systems produce three-valued logics: proven, unproven, and disproven.
What happens when something is unproven is the key design point: Most type systems error. Unsound type systems “assume” the unproven thing is true. A contract system “checks” the unproven things at runtime. We should flip the default form “assume” to “check”, which requires runtime support. Explicit assume, runtime-check, and static-assert syntaxes could provide escape hatches for unsafe eliding of runtime checking, pushing out the boundaries of statically verified code, and requiring static proof respectively.
You’ll like Pytype, which only errors on disproven code: https://github.com/google/pytype#how-is-pytype-different-from-other-type-checkers
Neat!
The other big one I’ve heard of (but have not tried) is Erlang’s “Dialyzer” based on “success types”: https://easychair.org/publications/paper/FzsM
Unfortunately, I don’t think either of these perform have a runtime checking/contract system.
we even quote and link to stanza in our typing faq :) never actually used stanza beyond a few toy programs, but i’m a big fan of the design and ideas behind it.
A better default would be type annotations that cooperate with a runtime contract system for which only guaranteed runtime failures are converted to static errors.
This is how SBCL handles Common Lisp’s optional type annotations by default, “Declarations as Assertions”: “all type declarations that have not been proven to always hold are asserted at runtime.”
Sadly, as far as I can tell from the CL implementations I’ve seen, they only support the declaration of primitive types and arrays thereof. Things get really interesting once you get to user-defined type constructors, higher-kinded types, first-class functions, mutable objects, etc. Especially true if you want to preserve non-functional properties, such as constant space usage or predictable runtime. For example, there are trivial cases where contracts can cause behavior to become accidentally quadratic with naive implementations.
It doesn’t include your entire wishlist, but CL’s type system does support user-defined type constructors, arbitrary unions and intersections of types, etc., which is enough to do algebraic data types and some mildly fancy types like “nonempty list” or “integer outside the range [-500,500]”. There’s an overview here: https://alhassy.github.io/TypedLisp
I think most people who reach for optional typing, based on the pitch they use, really want whole program type inference. Haskell and a few others have this. Most languages with “inference” just have local inference and so you still have to “think about type annotations” but there is no reason to require that.
Speaking as someone who prefers dynamic typing (for live coding, creative coding, generative design, and hobby projects), the benefits I see in using type annotations in a dynamic language are: speeding up performance sensitive code, and getting high quality error messages when I misuse a library function. For the latter, I want error messages to tell me which argument was bad in a call to a library function – I don’t want a stack trace from the interior of a library function that requires me to understand the code.
Neither of these benefits requires that all type errors should be reported at compile time. If a statically typed library function runs fast and gives good error messages at runtime, then it meets the requirements.
The only time I expect to “fight the type checker” is when I have descended into the language’s statically typed low-level subset to write some high performance code.
I think that type annotations and compile time type errors are both excellent additions to a dynamically typed language. By the latter, I mean the compiler is able to deduce and report some run-time errors at compile time. This doesn’t lead to “fighting the type checker” if the static checker doesn’t report false positives (which static type systems will invariably do).
My reading of the Stanza docs is that Stanza is not a pure dynamic language. It switches to static type checking rules (inaccurately reporting false-positive type errors on good code) if all of your variables have type annotations. It’s a compromise that attempts to appeal to both static- and dynamic-typing users, but I suspect doesn’t give either side all that they want. So if, by “optional typing”, you mean this kind of compromise, then I may agree.
Typescript’s an interesting one, in that the strictness is adjustable and can be ramped up as you migrate your JS code over. For new projects you just start with super-strict settings.
That’s just not possible if there’s any IO. Well… maybe it’s not technically not possible but it’s culturally not possible. Maybe if every library in the world used strict TypeScript and didn’t do casts then there wouldn’t need to be runtime checks.
That is the theoretical aim, but in practice it winds up just being a half-typed codebase IME. Kind of a shame tbh. But, it’s turned me off of believing this language direction is trustworthy, as opposed to better linter.
Y’all do realize this is basically marketing spam for the author’s book Effective Haskell, right?
Like, complete with a play on the title of the book in this article’s title and the massive CTA at the end, right?
I really am sorry that it came across this way. You’re right that the article is something I wrote because my publisher asked me to write something to help people get excited about Haskell to coincide with the launch of the book into beta. That said, I wrote the article, and shared it here, because I legitimately love Haskell and I think it’s a great choice and a lot more people would benefit from giving it another earnest chance as a useful language. A lot of things that pitch Haskell as a solution are deeply technical, and I thought that it might be more accessible to speak to people about Haskell in terms of the every day problems that they have and why I think Haskell is a useful choice for solving them.
I give it leeway because Rebecca has been on Lobsters for a while and contributed as a good-faith community member. Though I would have appreciated it if she included a top-level comment on the submission saying it was for her new book.
Thanks for calling out that I should have written a top-level comment. It didn’t really occur to me to do so, but I’ll make it a point to do that in the future if anything I submit is related to the book. I apologize if it came off as spammy-, I really don’t want to do that.
Better Resource Management
Certainly better than in many other languages but things like the bracket function (the “default” version of which is broken due to async exceptions lol oops) are rather “meh” compared to RAII-style ownership. Because nothing forces you to avoid resource leaks… well, now Linear Haskell can do that, but being a newly retrofitted extension it’s not gonna be instantly pervasive.
TBH, Haskell is in kind of an awkward spot these days:
I love Rust, but I don’t think it’s a clear winner over Haskell personally.
In Rust, the affine types and lack of garbage collection are really great when I’m working on low-level code. As someone who has written a lot of C and a lot of Haskell, Rust undeniably hits a lot of my requirements. For a lot of day-to-day work though, I still find that I’m much more likely to pick up Haskell. Little things like higher kinded types and GADTs end up being a big force multiplier for me being able to build the sorts of APIs that work best for me. I also really value laziness and the syntactic niceties like having universal currying when I’m working in Haskell.
None of that is anything negative about Rust. I really admire what the Rust community has done. If anything, I think rustaceans are in a great position to leverage all of the things they’ve learned from Rust so that they can more quickly and easily dip a toe into Haskell and see if it might be useful to them sometimes. In the end I don’t think we have to view each other as competitors, so much as two languages that sit in somewhat different spots of the ecosystem that can learn and benefit one another.
I think rustaceans are in a great position to leverage all of the things they’ve learned from Rust so that they can more quickly and easily dip a toe into Haskell and see if it might be useful to them sometimes.
This is exactly where I am in my PL journey (outside of work). I’ve been writing Rust for 5 years and it’s a great language for all the reasons you mentioned and more. I also found Rust a nice intro to writing real world code that is more FP than OO (i.e: structs/record and traits/type-classes instead of classes and interfaces) while still having static types (I love lisps but I tend to work better with types). Now I’m getting into Haskell and so far the process have been fairly smooth and very enlightening. The type system is far more expressive and I can see myself being highly productive in Haskell (far more than Rust) not having to worry about memory management and some of the more restrictive aspects of the Rust type system. If the learning process continues I wouldn’t be surprised if Haskell becomes my “go-to” for most problems, but Rust is still there for when I care more about performance and resource usage.
It will be interesting to see how attitudes towards resource collection shift with the advent of linear types in Haskell.
Yes, my opinion is that Rust has successfully stolen the best ideas from Haskell and made them more palatable to a mass audience.
Haskell is great, except for all the monad transformer stuff. That’s all an absolute nightmare. At this point, I just don’t really see a reason to use it over Rust for writing practical (i.e. non-research) software. Rust has the most important pieces from Haskell.
My experience with monad transformers is that they can offer a lot of practical value. There’s a little bit of a learning curve, but I think that in practice it’s a one-time cost. Once you understand how they work, they don’t tend to add a lot of cognitive burden to understanding the code, and can often make it easier to work with.
I do like some of the work people are doing with effects systems to address the down sides of monad transformers, and eventually we might move on, but for a lot of day to day work it’s just very common to end up doing a lot of related things that all need to, e.g. share some common information, might fail in the same way, and need to be able to do some particular set of side effects. A canonical example would be something like accessing a database, where you might have many functions that all need to access a connection pool, talk to the database, and report the same sorts of database related errors. Monad transformers give you a really practically effective way to describe those kinds of things and build tooling to work with them.
Monads are mostly complexity for the sake of being able to imagine that your functions are “pure”. I have not found any benefits for such an ability, besides purely philosophical, at least in the way most functional programming languages are built. There are better ways, that can forgo the need for imagination, but the functional programming crowd doesn’t seem to find them.
I have not found them any good for that use case either. The code I’ve seen usually ends up as a recursive monad soup, that you need to write even more code to untangle. They can work well in some limited contexts, but those contexts can often work just as well using other programming constructs in my opinion. Limited code reuse in general is a problem with many half-assed solutions that only work in limited contexts, for example inheritance, DSLs, composition(the OOP kind), etc. Monads are just another one of them, and honestly, they are just as, if not more easy to overuse as the other solutions.
I do not understand this perspective at all. traverse
alone saves me an astonishing amount of work compared to reimplementing it for every data structure/applicative pair.
The reason you need traverse
at all is monads. It’s all complexity for the sake of complexity in my eyes.
Not at all. traverse
works for a far wider class of things than just monads. And even if a language didn’t have the notion of monad it would still benefit from a general interface to iterate over a collection. That’s traverse
.
general interface to iterate over a collection
So, a for loop? A map()
in basically any language with first-class functions?
Anyways, my comment about needing traverse
at all is in response of needing to reimplement it for many different data structures. The problem I see in that, is that the reason you get all of those data structures is because of Monads. There a lot less of a need to have such a function when you don’t have monads.
The reason you need traverse at all is monads. It’s all complexity for the sake of complexity in my eyes.
How would you write, say,
traverseMaybeList :: (a -> Maybe b) -> [a] -> Maybe [b]
traverseEitherBoolSet :: (a -> Either Bool b) -> Set a -> Either Bool (Set b)
in a unified way in your language of choice?
On a good day, I’d avoid the Maybe
and Either
types that are used for error handling, and just have good old exceptions and no need any traversal. On a bad day, I’d probably have to use traverse
, because Maybe
and Either
, are monads, and create this problem in the first place.
I think if you prefer exceptions to Maybe/Either then you’re sort of fundamentally at odds with Haskell. Not saying this in a judgmental way, just that “exceptions are better than optional/error types” is not how Haskell thinks about things. Same with Rust.
Though, even in Python I typically write functions that may return None over functions that throw an exception.
I think if you prefer exceptions to Maybe/Either then you’re sort of fundamentally at odds with Haskell.
I’m pretty sure by just disliking monads I’m at odds with Haskell as it currently is. But do note, that not all exceptions are crafted equally. Take Zig for example, where errors functionally behave like traditional exceptions, but are really more similar to error types in implementation. A lot nicer than both usual exceptions, and optional/error types in my opinion.
Though, even in Python I typically write functions that may return None over functions that throw an exception.
It really depends if the function makes sense if it returns a none. If you’re trying to get a key from cache, and the network fails, returning a None is fine. If you are trying to check if a nonce has been already used, and network fails, returning None is probably the wrong thing to do. Exceptions are a great way to force corrective behavior from the caller. Optional types have none of that.
I don’t understand why you say Zig error types “behave like traditional exceptions”. My understanding is that if I have a function that returns a !u32, I can’t pass that value into a function that takes a u32.
Similarly, I don’t understand the idea that exceptions force correctional behavior. If I have a function that throws an exception, then I can just… not handle it. If I have a function that returns an error type, then I have to specify how to handle the error to get the value.
Yes, but essentially, you are either handling each error at the call site, or, more often, you bubble the error upwards like an exception. You end up with what I would call forcibly handled exceptions.
Not correcting some behavior leads to your program dying outright with exceptions. If you handle the exception, I’d say you are immediately encouraged to write code that corrects it, just because of how the handling is written. With functions that return an error type, it’s very easy to just endlessly bubble the error value upwards, without handling it.
With functions that return an error type, it’s very easy to just endlessly bubble the error value upwards, without handling it.
If I have an Optional Int, and I want to put it in a function that takes an int, I have to handle it then and there. If I have an optional int and my function signature says I return an int, I must handle it within that function. The optional type can’t escape out, versus exceptions which can and do.
I’d argue that these specific types are actually not very useful. If any error occurs, you don’t get _any _ results? In my experrience it’s more likely that we need to partition the successful results and log warnings for the failures. The problem with these rigidly-defined functions is that they don’t account for real-world scenarios and you just end up writing something by hand.
Haskell’s standard library is anything but rigid in my opinion. Take the specific case of “something that contains a bunch of (Maybe item).
toList <$> traverse l
.fold $ toList <$> l
.getFirst $ fold $ First <$> l
getLast $ fold $ Last <$> l
These are specific to Maybe, especially the First and Last, I’ll give you that. But functions from the stdlib can be snapped together in a huge number of ways to achieve a LOT of things succinctly and generally.
OK, this doesn’t actually answer my question. Say I have a stream of incoming data. What I really want to do is validate the data, log warnings for the ones that fail, and stream out the ones that succeed.
Then use an API that’s designed for streaming processing of data, for example https://hackage.haskell.org/package/streamly
I wrote up a few different operations on “collections of Maybe” or “collections of Either” in Haskell. The total number of lines of code required to express these operations using the standard Haskell library was around 12, including some superfluous type signatures. They cover all the cases in my other comment, as well as the “partitioning” you mention in your post. Here’s a gist:
https://gist.github.com/DanilaFe/71677af85b8d0b712ba2d418259f31dd
Monads are mostly complexity for the sake of being able to imagine that your functions are “pure”.
That’s not what they’re for. Monad transformers (well, the transformers in the mtl with the nice typeclasses) in particular let you clearly define what effects each piece of code has. This ends up being pretty useful: if you have some sort of monad for, say, SQL server access, you can then see from a given function’s type if it does any SQL transactions. If you attempt to do SQL where you’re not supposed to, you get a type error warning you about it. I think that’s pretty convenient. There’s lots of examples of this. If you’re using the typeclasses, you can even change the effect! Instead of reading from an actual db, you could hand off mocked up data if you use one monad and real db info with the other. This is pretty neat stuff, and it’s one of my favorite features of Haskell.
I agree that they might not always be super clear (and monad transformers start to have pretty shit perf), but they’re not just for intellectual pleasure.
Monad transformers (well, the transformers in the mtl with the nice typeclasses) in particular let you clearly define what effects each piece of code has.
Slight correction: they usually let you define what classes of effects a piece of code has. This of course can range in abstraction, from a very specific SQLSelect
to an overly broad, and not at all descriptive IO
. One problem often seen with this, is that methods often combine several different effects to achieve the result, which leads to either having an obnoxiously large function signature, or having to merge all the effects under the more generic one, whether that be the more useful SQL
if you’re lucky and the method only touches the SQL, or the frankly useless IO
, in both cases loosing a big part of the usefulness of it.
But the thing is, that you don’t need monads to achieve any of that anyways. If you represent external state (which the effects are meant to move away from you) as an input to a function, and the function outputs the same external state back, just with the commands it wants to do, a runtime can perform the IO, and bring you back the information on second function call. This of course might be somewhat counter-intuitive, as people are used for their main()
function to be run only once, but it leads to another way of thinking, a one where you are more aware of what state you carry, and what external systems each function can interface with, as it lives straight in the function signature, only with an opportunity to hide inside a type to group several of them. This style would also naturally promote IO pipelining, since you easily can (and probably want to) submit more than one IO request at once. You can build the IO runtime on anything you want as well, be it io_uring
, or a weird concoction of cloud technologies, if you provide your program with the same interface. It also brings the same testing possibilities, even slightly more, as making a golden data tests becomes ridiculously easy. More impressively, it brings the possibility of relatively easy time-travel debugging, as you only need to capture the inputs to the main function every function call to accurately replay the whole computation, and in part, enable to check some fixes without even re-doing the IO. I think this is a better way to move towards in functional programming, but I myself don’t have the time, or motivation in functional programming to push it that way.
Classes of effects instead of effects is a distinction without a difference, right? I can define a monad typeclass that only puts things in a state and a monad typeclass that only takes things out instead of using StateT (in fact they exist and are called Reader and Writer), and I can get as granular as I’d like with it. The amount of specificity you want is entirely up to you.
I agree that the IO monad is pretty frustratingly broad. You’re also correct that you don’t need monads to do this sort of thing. I’m having a little bit of trouble understanding your replacement. You mean a function with external state a
and pure inputs b
with result c
should have the type a -> b -> (c, a)
, right? What would you do when you want to chain this function with another one?
No. Your main
function’s signature looks like a -> a
. And a runtime calls it again and again, taking the actions the function specified in the output type that contains the external state objects, performing them, and putting the results back into the same objects. Your other functions as such grow in a similar manner, for example a function that takes an external resource a
and a pure input b
, to for example submit a write request, it would look like a -> b -> a
. An important thing to note, that it only submits a request, but doesn’t do it yet. It would only be performed once the main
function ends, and the runtime takes over. As such, you couldn’t do reading as trivially as a -> b -> (a, c)
, as you cannot read the data out while “your” code is running. This isn’t great for usability, but that can in large part be solved by using continuations.
As a side note, I don’t particularly enjoy chaining. It’s another solution that is only needed because monads make it appear that the function isn’t performing IO, when it’s more useful for you to think that it does. With continuations, you could just make this look like several function calls in a row, with plain old exceptions to handle errors.
This seems far more complex than using monads to me, but different people think in different ways. I don’t know what you mean by you don’t enjoy chaining— you don’t like sequencing code?
I like sequencing code, but I don’t enjoy sequencing code with monads, since monads force the sequencing of code they touch to be different, just because they are monads.
Can you provide an example of monads changing the way you sequence code? That’s one of the major benefits of do-notation in my mind: you can write code that looks like it is executing sequentially.
The do-notation is the problem. Why would sequencing functions that do IO would need to be different from sequencing functions that don’t? IO is something normal that a program does, and functional programming just makes it weird, because it likes some concept of ‘purity’, and IO is explicitly removed from it when the chosen solution are monads.
Because functions that do IO have to have an order in which they execute. The machinery of a monad lets you represent this. I don’t care which side of (2+2) + (2+2) executes first, but I do care that I read a file before I try to display its content on screen.
In the general case, you don’t care about the order the IO executes as long as you don’t have any dependencies between it. multiply(add(2, 2), 2)
will always perform addition first, multiplication second, just like displayData(readFile(file))
will always read the file first, and display the data second. Compiler will understand this, without needing to distinguish the functions that do IO, from those that don’t. In the few cases where you don’t have any fully direct data dependencies, but still need to perform IO in specific order, you then may use specific barriers. And even with them, it would still feel more natural for me.
In the general case, it’s impossible to determine which code might depend on the other. A contrived counter example would be writing to a socket of program a, that itself writes to the socket of program b, and then writing to the socket of program b. The order here matters, but no compiler would be able to determine that.
In the few cases where you don’t have any fully direct data dependencies, but still need to perform IO in specific order, you then may use specific barriers.
Yes, these specific barriers are exactly what monads provide.
Can you provide an example of a monad restructuring how you want to sequence something? I’m very open to seeing how they fall short, I haven’t written Haskell in a long time (changed jobs) but I miss the structure monads give very often.
Of course no compiler can determine all dependencies between IO. In other languages you don’t need to worry much about it, because in other languages the evaluation order is well defined. Haskell though, forgoes such definition, and with the benefits it brings, it also brings it’s problems, namely, the inability to easily order unrelated function evaluation. There is seq
and pseq
, but they are frowned upon because they break monads :). So the way the IO monad works is by introducing artificial data dependencies between each monad. This feels quite hacky to me. But do note that this is mostly a problem with Haskell, and many other functional programming languages that are full of monads could get rid of them without much change in the language semantics.
Monads don’t change how I sequence something. But they do greatly annoy me, by needing special handling. It’s like mixing async and non-async code in other languages - either you go fully one way, or fully the other. Mixing both does not work well.
Monads don’t change how I sequence something.
Then why did you say:
I like sequencing code, but I don’t enjoy sequencing code with monads, since monads force the sequencing of code they touch to be different, just because they are monads.
They also don’t need special handling. Do-notation is syntax sugar, but there’s nothing in Haskell that privileges monads outside of the standard library deciding to use them for certain things. They are just a typeclass, the same as any other.
As a response to your edit: no, reading is still a class of actions. You can read a single byte, or you can read a million bytes, and those two are very different actions in my mind. Trying to represent such granularity in monads is difficult, and honestly, a waste of time, since you don’t need such granular control anyways. But this is mostly disagreements in definition at this point, so no need to discuss this further I think.
Yeah, linear IO is a major motivator for my work on Dawn.
Monads arise naturally from adjoint functors. Perhaps they are not obvious, but that does not mean that they are artificially complex.
It sounds like you vaguely disapprove of effects-oriented programming, but you need to offer concrete alternatives. Handwaves are not sufficient here, given that most discussion about monads comes from people who do not grok them.
Monads arise naturally from adjoint functors. Perhaps they are not obvious, but that does not mean that they are artificially complex.
Such technobabble explanations are why I try to distance myself from functional programming. While technically correct, they offer no insight for people who do not already understand what monads are.
It sounds like you vaguely disapprove of effects-oriented programming, but you need to offer concrete alternatives. Handwaves are not sufficient here, given that most discussion about monads comes from people who do not grok them.
I do, in this comment. It might not be the most understandable, it might not have the strong mathematical foundations, and it definitely is wildly different as to how people usually think about programs. But I still think that it can offer better understanding of the effects your program does, besides giving a bunch of other advantages.
Also, I don’t disapprove of effects-oriented programming, it’s just that monads are a terrible way of doing it. I feel like there are a lot of better ways of making sure effects are explicit, my suggestion being one of them, effect handlers being the other one about which I learned recently.
I looked this up and it seems that the idea that every monad comes up as an adjunction occurs, if you define a category based on that monad first. isn’t this totally cyclic?
In many cases, the algebras for a monad will be things we already cared about. In fact, that was sort of the original point of monads – a way of abstractly capturing and studying a wide variety of algebraic theories together.
For example, if you’re familiar with the list monad, its algebras are simply monoids, and so its Eilenberg-Moore category is (at least equivalent to) the category of monoids.
There are other monads whose algebras would be groups, or rings, or vector spaces over a field K, or many others.
But I think Corbin was probably not referring to the way in which every monad comes from at least one adjunction (or two, if you also throw in the one involving the Kleisli category), but rather that if you already have adjunctions hanging around, you get a monad (and a comonad) from each of them in a very natural way. If you’re familiar with order theory by any chance, this is a direct generalisation of how you get a closure operator from a Galois connection between partially ordered sets.
This entire discussion is almost completely irrelevant to someone using monads to get programming tasks done though. As an abstraction of a common pattern that has been showing up in combinator libraries since the early days of functional programming, you can fully understand everything you need to know about it without any of this mathematical backstory.
Why we recognise the monad structure in programming is mostly not really to be able to apply mathematical results – maybe occasionally there will be a spark of inspiration from that direction, but largely, it’s just to save writing some common code over and over for many libraries that happen to have the same structure. Maybe monad transformers take that an additional step, letting us build the combinator libraries a bit more quickly by composing together some building blocks, but these would all still be very natural ideas to have if you were just sitting down and writing functional programs and thinking about how to clean up some repetitive patterns. It would still be a good idea even if the mathematicians hadn’t got to it first.
It’s completely opaque to me how to get them to do what I need them to do. I found myself randomly trying things, hoping something would work. And this is for someone who found Rust lifetimes to be quite straightforward, even before NLL.
Name popular OSS software, written in Haskell, not used for Haskell management (e.g. Cabal).
AFAICT, there are only two, pandoc and XMonad.
This does not strike me as being an unreasonably effective language. There are tons of tools written in Rust you can name, and Rust is a significantly younger language.
People say there is a ton of good Haskell locked up in fintech, and that may be true, but a) fintech is weird because it has infinite money and b) there are plenty of other languages used in fintech which are also popular outside of it, eg Python, so it doesn’t strike me as being a good counterexample, even if we grant that it is true.
Here’s a Github search: https://github.com/search?l=&o=desc&q=stars%3A%3E500+language%3AHaskell&s=stars&type=Repositories
I missed a couple of good ones:
Still, compare this to any similarly old and popular language, and it’s no contest.
I think postgrest is a great idea, but it can be applied to very wrong situations. Unless you’re familiar with Postgres, you might be surprised with how much application logic can be modelled purely in the database without turning it into spaghetti. At that point, you can make the strategic choice of modelling a part of your domain purely in the DB and let the clients work directly with it.
To put it differently, postgrest is an architectural tool, it can be useful for giving front-end teams a fast path to maintaining their own CRUD stores and endpoints. You can still have other parts of the database behind your API.
I don’t understand Postgrest. IMO, the entire point of an API is to provide an interface to the database and explicitly decouple the internals of the database from the rest of the world. If you change the schema, all of your Postgrest users break. API is an abstraction layer serving exactly what the application needs and nothing more. It provides a way to maintain backwards compatibility if you need. You might as well just send sql query to a POST endpoint and eliminate the need for Postgrest - not condoning it but saying how silly the idea of postgrest is.
Sometimes you just don’t want to make any backend application, only to have a web frontend talk to a database. There are whole “as-a-Service” products like Firebase that offer this as part of their functionality. Postgrest is self-hosted that. It’s far more convenient than sending bare SQL directly.
with views, one can largely get around the break the schema break the API problem. Even so, as long as the consumers of the API are internal, you control both ends, so it’s pretty easy to just schedule your cutovers.
But I think the best use-case for Postgrest is old stable databases that aren’t really changing stuff much anymore but need to add a fancy web UI.
The database people spend 10 minutes turning up Postgrest and leave the UI people to do their thing and otherwise ignore them.
Hah, I don’t get views either. My philosophy is that the database is there to store the data. It is the last thing that scales. Don’t put logic and abstraction layers in the database. There is plenty of compute available outside of it and APIs can do precise data abstraction needed for the apps. Materialized views, may be, but still feels wrong. SQL is a pain to write tests for.
Your perspective is certainly a reasonable one, but not one I or many people necessarily agree with.
The more data you have to mess with, the closer you want the messing with next to the data. i.e. in the same process if possible :) Hence Pl/PGSQL and all the other languages that can get embedded into SQL databases.
We use views mostly for 2 reasons:
Have you checked row-level security? I think it creates a good default, and then you can use security definer views for when you need to override that default.
Yes, That’s exactly how we use access control views! I’m a huge fan of RLS, so much so that all of our users get their own role in PG, and our app(s) auth directly to PG. We happily encourage direct SQL access to our users, since all of our apps use RLS for their security.
Our biggest complaint with RLS, none(?) of the reporting front ends out there have any concept of RLS or really DB security in general, they AT BEST offer some minimal app-level security that’s usually pretty annoying. I’ve never been upset enough to write one…yet, but I hope someone someday does.
That’s exactly how we use access control views! I’m a huge fan of RLS, so much so that all of our users get their own role in PG
When each user has it its own role, usually that means ‘Role explosion’ [1]. But perhaps you have other methods/systems that let you avoid that.
How do you do for example: user ‘X’ when operating at location “Poland” is not allowed to access Report data ‘ABC’ before 8am and after 4pm UTC-2, in Postgres ?
[1] https://blog.plainid.com/role-explosion-unintended-consequence-rbac
Well in PG a role IS a user, there is no difference, but I agree that RBAC is not ideal when your user count gets high as management can be complicated. Luckily our database includes all the HR data, so we know this person is employed with this job on these dates, etc. We utilize that information in our, mostly automated, user controls and accounts. When one is a supervisor, they have the permission(s) given to them, and they can hand them out like candy to their employees, all within our UI.
We try to model the UI around “capabilities”, all though it’s implemented through RBAC obviously, and is not a capability based system.
So each supervisor is responsible for their employees permissions, and we largely try to stay out of it. They can’t define the “capabilities”, that’s on us.
How do you do for example: user ‘X’ when operating at location “Poland” is not allowed to access Report data ‘ABC’ before 8am and after 4pm UTC-2, in Postgres ?
Unfortunately PG’s RBAC doesn’t really allow us to do that easily, and we luckily haven’t yet had a need to do something that detailed. It is possible, albeit non-trivial. We try to limit our access rules to more basic stuff: supervisor(s) can see/update data within their sphere but not outside of it, etc.
We do limit users based on their work location, but not their logged in location. We do log all activity in an audit log, which is just another DB table, and it’s in the UI for everyone with the right permissions(so a supervisor can see all their employee’s activity, whenever they want).
Certainly different authorization system(s) exist, and they all have their pros and cons, but we’ve so far been pretty happy with PG’s system. If you can write a query to generate the data needed to make a decision, then you can make the system authorize with it.
My philosophy is “don’t write half-baked abstractions again and again”. PostgREST & friends (like Postgraphile) provide selecting specific columns, joins, sorting, filtering, pagination and others. I’m tired of writing that again and again for each endpoint, except each endpoint is slightly different, as it supports sorting on different fields, or different styles of filtering. PostgREST does all of that once and for all.
Also, there are ways to test SQL, and databases supporting transaction isolation actually simplify running your tests. Just wrap your test in a BEGIN; ROLLBACK; block.
Idk, I’ve been bitten by this. Probably ok in a small project, but this is a dangerous tight coupling of the entire system. Next time a new requirement comes in that requires changing the schema, RIP, wouldn’t even know which services would break and how many things would go wrong. Write fully-baked, well tested, requirements contested, exceptionally vetted, and excellently thought out abstractions.
Or just use views to maintain backwards compatibility and generate typings from the introspection endpoint to typecheck clients.
I’m a fan of tools that support incremental refactoring and decomposition of a program’s architecture w/o major API breakage. PostgREST feels to me like a useful tool in that toolbox, especially when coupled with procedural logic in the database. Plus there’s the added bonus of exposing the existing domain model “natively” as JSON over HTTP, which is one of the rare integration models better supported than even the native PG wire protocol.
With embedded subresources and full SQL view support you can quickly get to something that’s as straightforward for a FE project to talk to as a bespoke REST or GraphQL backend.. Keeping the schema definitions in one place (i.e., the database itself) means less mirroring of the same structures and serialization approaches in multiple tiers of my application.
I’m building a project right now where PostgREST fills the same architectural slot that a Django or Laravel application might, but without having to build and maintain that service at all. Will I eventually need to split the API so I can add logic that doesn’t map to tuples and functions on them? Sure, maybe, if the app gets traction at all. Does it help me keep my tiers separate for now while I’m working solo on a project that might naturally decompose into a handful of backend services and an integration layer? Yep, also working out thus far.
There are some things that strike me as awkward and/or likely to cause problems down the road, like pushing JWT handling down into the DB itself. I also think it’s a weird oversight to not expose LISTEN/NOTIFY over websockets or SSE, given that PostgREST already uses notification channels to handle its schema cache refresh trigger.
Again, though, being able to wire a hybrid SPA/SSG framework like SvelteKit into a “native” database backend without having to deploy a custom API layer has been a nice option for rapid prototyping and even “real” CRUD applications. As a bonus, my backend code can just talk to Postgres directly, which means I can use my preferred stack there (Rust + SQLx + Warp) without doing yet another intermediate JSON (un)wrap step. Eventually – again, modulo actually needing the app to work for more than a few months – more and more will migrate into that service, but in the meantime I can keep using fetch
in my frontend and move on.
I think it’s true that, historically, Haskell hasn’t been used as much for open source work as you might expect given the quality of the language. I think there are a few factors that are in play here, but the dominant one is simply that the open source projects that take off tend to be ones that a lot of people are interested in and/or contribute to. Haskell has, historically, struggled with a steep on-ramp and that means that the people who persevered and learned the language well enough to build things with it were self-selected to be the sorts of people who were highly motivated to work on Haskell and it’s ecosystem, but it was less appealing if your goals were to do something else and get that done quickly. It’s rare for Haskell to be the only language that someone knows, so even among Haskell developers I think it’s been common to pick a different language if the goal is to get a lot of community involvement in a project.
All that said, I think things are shifting. The Haskell community is starting to think earnestly about broadening adoption and making the language more appealing to a wider variety of developers. There are a lot of problems where Haskell makes a lot of sense, and we just need to see the friction for picking it reduced in order for the adoption to pick up. In that sense, the fact that many other languages are starting to add some things that are heavily inspired by Haskell makes Haskell itself more appealing, because more of the language is going to look familiar and that’s going to make it more accessible to people.
There are tons of tools written in Rust you can name
I can’t think of anything off the dome except ripgrep. I’m sure I could do some research and find a few, but I’m sure that’s also the case for Haskell.
You’ve probably heard of Firefox and maybe also Deno. When you look through the GitHub Rust repos by stars, there are a bunch of ls clones weirdly, lol.
Agree … and finance and functional languages seem to have a connection empirically:
I think it’s obviously the domain … there is simple a lot of “purely functional” logic in finance.
Implementing languages and particularly compilers is another place where that’s true, which the blog post mentions. But I’d say that isn’t true for most domains.
BTW git annex appears to be written in Haskell. However my experience with it is mixed. It feels like git itself is more reliable and it’s written in C/Perl/Shell. I think the dominating factor is just the number and skill of developers, not the language.
OCaml also has a range of more or less (or once) popular non-fintech, non-compiler tools written in it. LiquidSoap, MLDonkey, Unison file synchronizer, 0install, the original PGP key server…
I think the connection with finance is that making mistakes in automated finance is actually very costly on expectation, whereas making mistakes in a social network or something is typically not very expensive.
Not being popular is not the same as being “ineffective”. Likewise, something can be “effective”, but not popular.
Is JavaScript a super effective language? Is C?
Without going too far down the language holy war rabbit hole, my overall feeling after so many years is that programming language popularity, in general, fits a “worse is better” characterization where the languages that I, personally, feel are the most bug-prone, poorly designed, etc, are the most popular. Nobody has to agree with me, but for the sake of transparency, I’m thinking of PHP, C, JavaScript, Python, and Java when I write that. Languages that are probably pretty good/powerful/good-at-preventing-bugs are things like Haskell, Rust, Clojure, Elixir.
In the past, a lot of the reason I’ve seen people being turned away from using Haskell based tools has been the perceived pain of installing GHC, which admittedly is quite large, and it can sometime be a pain to figure out which version you need. ghcup
has improved that situation quite a lot by making the process of installing and managing old compilers significantly easier. There’s still an argument that GHC is massive, which it is, but storage is pretty cheap these days. For some reason I’ve never seen people make similar complaints about needing to install multiple version of python (though this is less off an issue these days).
The other place where large Haskell codebases are locked up is Facebook - Sigma processes every single post, comment and massage for spam, at 2,000,000 req/sec, and is all written in Haskell. Luckily the underlying tech, Haxl, is open source - though few people seem to have found a particularly good use for it, you really need to be working at quite a large scale to benefit from it.
hledger is one I use regularly.
Cardano is a great example.
Or Standard Chartered, which is a very prominent British bank, and runs all their backend on Haskell. They even have their own strict dialect.
I used pandoc for a long time before even realizing it was Haskell. Ended up learning just enough to make a change I needed.
Sorry about that. I didn’t develop the sites color scheme with so many types in mind and I did notice it’s a bit hard to read on some monitors (but of course it looked fine on the monitor I was using to write the post). I’ll try to tweak the theme a bit soon to improve contrast.
It’s great to see folks talking about the practical immediate benefits you can get from nix. OP I hope you don’t let some of the negativity here get you down too much!
For the detractors: Nix is one of those things where the benefits can be really hard to see at first. So much of what nix does can be done in other ways- so I think sometimes miss out that the real benefit of nix is that it offers a single cohesive and principled way to solve classes of problems that might otherwise require several different dedicated tools. Even narrowing down your view to just the case of version or dependency management, you can do most of what you’d do in nix with virtualenv or go modules or cabal sandboxes or cargo or whatever, but then you are dealing with the individual quirks of each of those different tools. Go up a level and you might also need to deal with alternatives or whatever other mechanism your particular OS uses to select between different system level packages. Up another level and you have to think about versions of docker containers or AMI images. Nix lets you handle all of that configuration coherently, with a single language and set of tools. Buying into nix for a project, your OS, or your entire approach to infrastructure can feel odd at first, but it has a huge force multiplier effect with itself when you buy into it.
I’m hoping to finish up the last set of changes for the first draft of my book Effective Haskell. If my new computer arrives, I’m also going to spend some time migrating my nixpkgs based nixOS setup to use Flakes, and possibly start experimenting with writing a Wayland compositor in Haskell so that I can eventually see about getting something heavily inspired by XMonad running on Wayland (an actual port doesn’t seem plausible given the architectural differences between X and Wayland, but it would be great to be wrong about that and I’ll definitely investigate it more deeply)
I would very much welcome an XMonad clone on Wayland. I’ve been poking into the issues from time to time to see if somebody’s managed to pick up the ball on it, but alas. One of the main issues I’ve seen frequently mentioned is the challenges involved with having
xmonad-contrib
and all other extensions working.