Why Haskell?
gtf.io
gtf.io
Haskell doesn't prevent endless recursion. (try e.g. `main = main`)
As the typed FP ecosystem is moving towards dependent typing (Agda, Idris, Lean), this becomes an issue, because you don't want the type checker to run indefinitely.
The many ad-hoc extensions to Haskell (TypeFamilies, DataKinds) are tying it down. Even the foundations might be a bit too ad-hoc: I've seen the type class resolution algorithm compared to a bad implementation of Prolog.
That's why, if you like the Haskell philosophy, why would you restrict yourself to Haskell? It's not bleeding edge any more.
Haskell had the possibility of being a standardized language, but look at how few packages MicroHS compiles (Lennart admitted to this at ICFP '24[0]). So the standardization has failed. The ecosystem is built upon C. The Wasm backend can't use the Wasm GC because of how idiosyncratic GHC's RTS is.[1]
So what does unique value proposition does GHC have left? Possibly the GHC runtime system, but it's not as sexy to pitch in a blog post like this.
[0]: Lennart Augustsson, MicroHS: https://www.youtube.com/watch?v=uMurx1a6Zck&t=36m
[1]: Cheng Shao, the Wasm backend for GHC: https://www.youtube.com/watch?v=uMurx1a6Zck&t=13290s
I went through Software Foundations [0] (Coq) which was fun and interesting but I can't say I ever really applied what I used there in software (I did get more comfortable with induction proofs).
You're mentioning Lean with Agda and Idris - is Lean usable as a general purpose language? I've been curious about Lean but I got the impression it sort of steps away from Haskell's legacy in terms of syntax and the like (unlike Agda and Idris) so was concerned it would be a large investment and wouldn't add much to what I've learned from Coq.
I'd love any insights on what's a useful way to learn more in the area of dependent types for a working engineer today.
That's why I've been writing an HTTP server in Idris2 instead. Here's a todo list demo app[1] and a hello world demo[2]. The advantage of Idris is that it compiles to e.g. Racket, a high level language with a concurrent runtime you can bind to from Idris.
It's also interesting how languages don't need their own hosting (e.g. Hackage) any more. Idris packages are just listed in a TOML file[3] (like Stackage) but still hosted on GitHub. No need for versions, just use git commit hashes. It's all experimental anyway.
[1]: https://janus.srht.site/docs/todolist.html [2]: https://git.sr.ht/~janus/web-server-racket-hello-world/tree/... [3]: https://github.com/stefan-hoeck/idris2-pack-db/blob/main/STA...
I don't have much experience with Haskell, but one of the worst experiences has been Stack's compile time dependency on GitHub. GitHub rate limits you and builds take forever.
- Binary upgrade of Stack fails due to GitHub API request limit #4979 (https://github.com/commercialhaskell/stack/issues/4979)
- GitHub rate limiting can affect Stack CI #6034 (https://github.com/commercialhaskell/stack/issues/6034)
And a few more. The "fix" is having Stack impersonate the user (https://github.com/commercialhaskell/stack/pull/6036) and authenticate to the API. This unblocks progress, but this is really a design bug and not something I think people should emulate.
Every other language I've used allows you to build code without authenticating to a remote service.
> Every other language I've used allows you to build code without authenticating to a remote service.
Sure, the problem here wasn't "building". It was downloading a package template (which one doesn't tend to do 60 times per hour). I agree packages shouldn't be fetched from GitHub.
That comment is from someone other than the ticket filer who was seeing another issue even after sending the GitHub token. It was this second issue that wasn't caused by GitHub rate limiting -- the original one was.
> It was downloading a package template (which one doesn't tend to do 60 times per hour).
I've personally had Stack-related GitHub API rate limiting delay builds by at least an hour due to extreme slowness. So whatever the rate limits are, Stack occasionally hits them.
However...
it's no where near ready for production use. They don't care about maintaining backwards compatibility. They are more focused on getting the language itself right than they are about helping people build and maintain software written in it. At least for the foreseeable future. If you do build things in it you're working on shifting ground.
But it has a lot of potential. The C code generated by Lean 4 is good. Although, that's another trade-off: compiling to C is another source of "quirks."
As far as I understand, that work is in fact done.
But it's frankly still early-days and we're still far from nirvana; Rust is starting to show some warts (despite still being a massive improvement over C from a safety perspective), and people are looking around for what's next.
One barely-touched thing is that there are compiler optimizations made possible by pure functional/pure immutable languages (such as caching a guaranteed result of an operation where those guarantees simply can't be given elsewhere) that have simply been impossible until now. (Roc is trying to go there, from what I can tell, and I'm here for it! Presumably, Rust has already, as long as you stick with its functional constructs, which I hear is hard sometimes)
The way it seems to me is that actually Rust aims to be an improvement over C++ rather than C. (And Zig aims to be an improvement over C rather than C++.)
The major downsides of both will be the same as their reference point: Rust will eventually be too complicated for anyone to understand while still not being really safe (and the complexity then comes back to bite you one more time). Zig will be easy to understand and use but too unsafe to use for important applications (at least once people start really caring about software in important applications).
Both of these will be fairly niche because compiling to a single binary just isn't as important, as elegant as it might be.
This is an outside perspective on Zig, and I have to say, not an informed one.
If you'd like to understand Zig (and what I mean) better, this video is a good start: https://www.youtube.com/watch?v=w3WYdYyjek4
Zig is, right now, being used for high-assurance systems where correctness is a terminal value, and it provides many tools and affordances to assist in doing so. It isn't a good choice for projects which give lip-service to correctness, but for ones which actually mean it, and are willing and able to put in the effort and expense to achieve it, it's an excellent choice. I'm willing to gloss that domain as "important applications", but perhaps you meant something different by that term.
I'm not convinced this is telling us very much. I was talking about software that, e.g., causes deaths when it fails. But regardless of what level of assurance one looks at, most "high assurance" systems continue to be built using C. The very few that use Zig surely chose it primarily due to compatibility with C, with the hope that it's safer than C (a low bar). Maybe in some cases also a wish to play around with new toys played a role in the decision, though in "important applicatios" I'd like to hope that's rare.
In the end, we'd have to look at harm done due to bugs. For C, I'd say the record is abysmal. For Zig, it's way to early to look at this.
My judgement above is mostly based on Zig not making it at all hard to violate memory safety and the analogy with C. Needless to say, Zig is better than C in this respect and that's a good thing. If your argument is something like "memory safety doesn't really matter, even for critical applications", we'll just not agree on this.
This sparked and interesting memory for me. I was once working with a customer who was producing on-board software for a missile. In my analysis of the code, I pointed out that they had a number of problems with storage leaks. Imagine my surprise when the customers chief software engineer said "Of course it leaks". He went on to point out that they had calculated the amount of memory the application would leak in the total possible flight time for the missile and then doubled that number. They added this much additional memory to the hardware to "support" the leaks. Since the missile will explode when it hits it's target or at the end of it's flight, the ultimate in garbage collection is performed without programmer intervention.
https://groups.google.com/g/comp.lang.ada/c/E9bNCvDQ12k/m/1t...
while a funny example, you are very wrong. High assurance systems have a specification and a bazillion tests because memory safety is an insanely tiny problem in the sea of problems they face. [that is why, frama-C and friends are preferred over Rust,Ada, ATS, and whatever else that exists.] Correctness of the spec and perfectly following the spec is far more important. Zig allows correct code to feel easier to write when compared to C. Thats why it was chosen in tigerbeetle and thats why, if the community wants, it will have an insanely bright future in high assurance systems.
Zig has some aliasing issues which are to-date unsolved. The core and community are keenly aware of them, and they'll be solved or greatly ameliorated before a 1.0 release. It's why TigerBeetle has a coding standard which requires all container types to be passed by constant pointer, not reference.
It isn't ideal, but I think it's reasonable to withhold judgement on a pre-1.0 language for some sorts of known issues which are as yet unaddressed. It's worth taking the time to find an excellent solution to the problem, rather than brute-force it or regress one of the several features which combine into the danger zone here.
If you're curious or interested in the details, look up "Attack of the Killer Features" on YouTube. Given your background, I'm sure the community would value your feedback on the various issues tracking this.
Not at all, not even close. What I will say is "memory safety can be achieved by policy as well as by construction, and indeed, can only be achieved by construction through policy".
Let's break that down. Rust and Go are two examples of languages generally referred to as memory-safe. Rust achieves this by construction, through the borrow checker, Go achieves it by construction through the garbage collector.
If there are bugs in the borrow checker, or the garbage collector, then the result is no longer memory-safe. That assurance can only be achieved by policy: the garbage collector and the borrow checker must be correct.
TigerBeetle, the program I linked to, achieves memory safety with a different policy. All allocation is performed once, at startup. After this, the replica's memory use is static. This, if correct, is memory-safe.
Zig makes this practical, because all allocation in the standard library is performed using the allocation interface: any function which allocates receives an Allocator as an argument. Libraries can violate that policy, but they generally don't, and TigerBeetle is a zero-dependency program, so that's not a concern for them. Other languages where it maybe isn't immediately obvious if something goes on a heap? Not so easy to achieve a memory policy like that one.
So this:
> Zig not making it at all hard to violate memory safety
Is irrelevant. What's needed in high-assurance systems is the practical ability to create memory safety by policy, and Zig provides a difference-in-class in this matter compared to C.
> most "high assurance" systems continue to be built using C
Yes, other than your scare quotes, this is correct. Programs like SQLite are memory safe by construction and exhaustive testing, you're welcome to try and land a CVE if you disagree. Every few years someone gets a little one, maybe you'll be the lucky next player. Or you could try your luck at QEMU.
My premise is that Zig makes it massively easier to achieve this, through numerous choices which add up: the allocator interface, built-in idiomatic leak detection, a null-safe type system, slices and arrays carrying bounds, and much else. It has `std.testing.checkAllAllocationFailures`, which can be used to verify that a function doesn't leak or otherwise misuse memory even if any one allocation anywhere downstack of a function call fails. You might want to compare this with what happens if a Rust function fails to allocate.
Basically you're taking the lessons of C, with all of its history and warts, and trying to apply them, without due consideration, to Zig. That's a mistake.
Idris at least does state that they what people building real programs with it and don't want it to just be a research language.
For dependent types, I myself am skeptical about languages trying to continuously push more and more stuff into types. I am not certain that such efforts are a net positive on writing good software. By their very definition, the more typed a language gets, the less programs it can represents. That obviously reduces buggy programs, but it also reduces non-buggy programs that you can implement. Highly typed languages force more and more effort into pre-compile time and you will often find yourself trying to fit a problem into the chains of the type system.
Rather, I think reasonably multi-paradigm languages like F# are the sweet spot. Just enough strict typing and functional core to get you going for most of your program, but then it allows classes and imperative programming when those paradigms are appropriate.
I think the way to go to write better software is better tooling and ergonomics. I don't think type systems are going to magically save us.
While I generally share your skepticism, I think this is quite wrong. A good part of the point of advanced type systems is to make more complex problems possible while still being well typed. For example, in C, if you want a function whose return type is tied to an input argument's type, you either use void* and casts (no type safety), or you don't write that function. In languages with even slightly more advanced type systems, you can write that function and still get full type safety.
Even more advanced type systems achieve the same things: you can take programs that can only be written in a simpler type system and make them safe. In standard Haskell, for example, you can't write a Monad and actually have the compiler check that it respects the Monad laws - the implementation of Monad functions just assumes that any type that implements the right shape of functions will work as a monad. With dependent types, you can actually enforce that functions designed to work with monads only apply to types that actually respect the monad laws.
The trade-off with very complex type systems is different, in my opinion: after some point, you start duplicating your program's logic, once in the implentation code, but again in the type signatures. For example, if you want to specify that a sort function actually sorts the input list, you might find that the type specification ends up not much shorter than the actual code of the function. And apart from raw effort, this means that your type specifications start being large enough that they have their own bugs.
[1] of the countably infinite possible programs, virtually all
If instead I have a good idea what I want to write, the type system may either guide me towards the solution, or hinder me. It usually hinders me, I don't need a type system to guide me, but I like a type system that can check for trivial errors (oh, I meant to pass a list of numbers, not just a single number).
Not to mention the tools to debug complex type errors are generally much less mature than the tools to debug runtime errors.
But even so, I think we could still benefit from going a little further towards the "proof" end of the type system spectrum in most cases. I don't think anyone really wants to deal with Coq and similar, but having used a language with dependent types for integers and vector lengths it's really nice to be able to say stuff like "this integer is in the range [0, 8)" and then have it catch errors when you pass it to a function that expects [0, 3) or whatever.
I remember exploring different proof assistants for the first time in the 2000s. Back then, only people with a background in logic were involved, and most of the proofs that were formalized as showcases were of textbook results from the 19th century at most, or some combinatorial stuff like the four-color theorem.
I believe Voevodsky was one of the first prominent non-logicians to become interested in proof assistants, using Coq around 2010. Nowadays, several mathematicians coming from algebraic geometry, number theory, etc. are promoting formal proofs, and it seems like most of them have chosen Lean. I don't know whether this is because Lean is somehow better suited for working mathematicians, or if it was simply a random personal preference among people who got enthusiastic about this stuff and started advertising it to their colleagues?
I am not familiar with every proof assistant out there, but many of them are a very hard sell for mathematicians and lack a comprehensive math library. Lean seems to be one of the few exceptions.
People routinely publish new proofs there, it is actually a regular referred journal.
Can you elaborate? I am using Lean as a general-purpose language writing simple little programs, so I have not encountered the deeper parts of the runtime etc. I'd like to see some criticism/different perspectives from more experienced people.
I'm not using Haskell because it's bleeding edge.
I use it because it is advanced enough and practical enough. It's at a good balanced spot now to do practical things while tapping into some of the advances in programming language theory.
The compiler and the build system have gotten a lot more stable over the past several years. The libraries for most production-type activities have gotten a lot more mature.
And I get all of the above plus strong type safety and composability, which helps me maintain applications in a way that I find satisfactory. For someone who aims to be pragmatic with a hint of scholarliness, Haskell is great.
GHC2021 promises backwards compatibility, but it includes ill-specified extensions like ScopedTypeVariables. TypeAbstractions were just added, and they do the same thing, but differently.[0] It hasn't even been decided yet which extensions are stable[1], yet GHC2021 still promises compatibility in future compiler versions. So either, you'll have GHC retain inferior semantics because of backwards compatibility, or multiple ways of doing the same thing.
GHC2024 goes even further and includes extensions that are even more unstable, like DataKinds.
Another sign of instability is the fact that GHC 9.4 is still the recommended[2] release even though there are three newer 'stable' GHCs. I don't know of other languages where the recommendation is so far behind! GHC 9.4.1 is from Aug 2022.
It was the same situation with Cabal, it took forever to move beyond Cabal 3.6 because the subsequent releases had bugs.[3]
[0]: https://serokell.io/blog/ghc-dependent-types-in-haskell-3 [1]: https://github.com/ghc-proposals/ghc-proposals/pull/669 [2]: https://github.com/haskell/ghcup-metadata/issues/220 [3]: https://github.com/haskell/ghcup-metadata/issues/40
But DataKinds is not stable. It's one of the most stable extensions possible! The link you provided even says it's stable:
https://github.com/telser/ghc-proposals/blob/initial-extensi...
> It hasn't even been decided yet which extensions are stable
It's essentially known, but it's not formally agreed. The fact that this proposal exists is evidence of that!
> GHC2021 still promises compatibility in future compiler versions. So either, you'll have GHC retain inferior semantics because of backwards compatibility, or multiple ways of doing the same thing.
GHC2021 will always provide ScopedTypeVariables. A future edition will probably provide TypeAbstractions instead. Being able to make progress to the default language like this is the point of having language editions!
I mean "not unstable"
Can you provide an example of a "mature" way to match a regular expression with Unicode character classes or download a file over HTTPS?
> import Control.Regex.Lens.Text
> "Foo, bar" ^.. [regex|\p{L}+|] . match
["Foo", "bar"]
> "Foo, bar" & [regex|\p{L}+|] . ix 1 . match %~ T.intersperse '-' . T.toUpper
"Foo, B-A-R"
For web requests wreq has a nice interface. The openssl bindings come from a different library so it does need an extra config line, the wreq docs have this example: import OpenSSL.Session (context)
import Network.HTTP.Client.OpenSSL
let opts = defaults & manager .~ Left (opensslManagerSettings context)
withOpenSSL $
getWith opts "https://httpbin.org/get"
There are native Haskell tls implementations that you could plug into the manager config. But openssl is probably the more mature option.Can you translate the examples at https://developer.mozilla.org/en-US/docs/Web/JavaScript/Refe... to Haskell? This Control.Regex.Lens.Text library doesn't seem to believe in documenting the supported syntax, options, etc.
ghci> "California rolls $6.99\nCrunchy rolls $8.49\nShrimp tempura $10.99" ^.. [regex|\p{Sc}\s*[\d.,]+|] . match
["$6.99","$8.49","$10.99"]
"Spacing combining mark" seems to be "Mc" so this works:https://unicode.org/reports/tr18/#General_Category_Property
ghci> "foo bar \x093b baz" ^.. [regex|\p{Mc}|] . match
["\2363"](U+093b is a spacing combining mark, according to https://graphemica.com/categories/spacing-combining-mark)
I think in general that Haskellers would probably move to parser combinators in preference to regex when things get this complicated. I mean, who wants to read "\p{Sc}\s*[\d.,]+" in any case?
And I am not sold on lens-regex-pcre documentation; "anything that works in PCRE will work" comes across as very dismissive. What string-like types are supported? What version of PCRE or PCRE2 does it use?
It uses https://hackage.haskell.org/package/pcre-light , which seems to link with the system version. So it depends on what you install. With Nix, it will be part of your system expression, of course.
I'm sorry, I don't know what that means. If you have a specific character you'd like me to try then please tell me what it is. My Unicode expertise is quite limited.
> I am not sold on lens-regex-pcre documentation
Nor me. It seems to leave a lot to be desired. In fact, I don't see the point of this lens approach to regex.
> "anything that works in PCRE will work" comes across as very dismissive
Noted, thanks, and apologies. That was not my intention. I was trying to make a statement of fact in response to your question.
> By the way, what text encodings for source files are supported by GHC?
UTF-8 I think. For example, pasting that character into GHC yields:
ghci> mapM_ T.putStr ("foo bar ः baz" ^.. [regex|\p{Mc}|] . match)
ः
> What string-like types are supported?ByteString (raw byte arrays) and Text (Unicode, internal representation UTF-8), as you can see from:
https://hackage.haskell.org/package/lens-regex-pcre
> What version of PCRE or PCRE2 does it use?
Whatever your system version is. For me on Debian it's:
Package: libpcre3-dev
Source: pcre3
Version: 2:8.39-15As the other comment mentioned pcre-compatible Regex are a standard, though the pcre spec isn't super readable. There are some projects that have more readable docs like mariadb and PHP, but it doesn't really make sense to repeat the spec in library docs https://www.php.net/manual/en/regexp.reference.unicode.php
There are libraries for pcre2 or gnu regex syntax with the same API if you prefer those
In the essay, I didn't say "Haskell is the only thing you should use", what I said was:
> Many languages have bits of these features, but only a few have all of them, and, of those languages (others include Idris, Agda, and Lean), Haskell is the most mature, and therefore has the largest ecosystem.
On this:
> It's not bleeding edge any more.
"Bleeding edge" is certainly not something I've used as a benefit in this essay, so not really sure where this comes from (unless you're not actually responding to the linked essay itself, but rather to ... something else?).
I'm not really sure where the borders of "the typed FP language ecosystem" would be but feel pretty certain that such a thing would enclose also F#, Haskell, and OCaml. Any one of which has more users and more successful "public facing" projects than the languages you mentioned combined. This is not a dig on those languages, but they are niche languages even by the standards of the niche we're talking about.
You could argue that they point to the future but I don't seriously believe a trend among them represents a shift in the main stream of functional programming.
Because it has a robust and mature ecosystem that is more viable for mainstream commercial use than any of the other "bleeding edge" languages.
First of all, does ecosystem move to dependent types? I think the practical value of Hindley-Milner is exactly in the fact that there is a nice boundary between types and terms.
Second, why would type checking running indefinitely be a practical problem? If I can't prove a theorem, I can't use it. The program that doesn't typecheck in practical amount of time is in practice identical to non-type-checked program, i.e. no worse than a status quo.
I think we'll continue to see lots of work in this direction and, subsequently, a lot of more mainstream FP languages will adopt features derived from dependent types research, but it's not like everybody's going to be writing Agda or Coq or Idris in 10 years instead of, like, OCaml and Haskell.
The point is that programming in a pure language with typed side effects and immutable data dramatically reduces the size of the state space that must be reasoned about. This makes programming significantly easier (especially over the long term).
Of the languages that support this programming style Haskell remains the one with the largest library ecosystem, most comprehensive documentation, and most optimised compiler. I love lean and use it professionally, but it is nowhere near the usability of Haskell when it comes to being a production ready general purpose language.
True
> This makes programming significantly easier (especially over the long term)
Not true. (As in, the implication is not true.)
There are many many factors that affect ease of programming and the structure of the stage space is just one of them.
Either way Haskell is also probably the language that lets you produce the most succinct code of anything that could be reasonably be used in production.
It is. Haskell has the low verbosity of scripting languages like Ruby and Python while letting you manage applications that would otherwise be written in high verbosity languages like C++, Java, and Rust.
That (in)famous goto paper isn't really about spaghetti code, it's about how on Earth do you mathematically define the semantics of any statement in a language with unrestricted goto. If any continuation can follow literally anything then you're pretty much in no man's land. On the other hand imperative code is easy and natural to reason about when it uses a small set of well defined primitives.
If that sounds surprising, consider how mathematical logic itself, especially obviously in the calculational proof style, is essentially a series of assignment statements.
Regarding semantics my experience is that defining formal semantics for languages with unrestricted mutation (or even worse aliased pointers into mutable state) than one that avoids those features.
loopType : Int -> Type
loopType x = loopType x
foo : List (loopType 3) -> Int
foo _ = 42
bar : List (loopType 4)
bar = []
baz : Int
baz = foo bar
Determining if baz type-checks requires evaluating loopType 3 and loopType 4 to determine if they're equal.Do you mean to say Haskell hasn't solved the halting problem yet?
Would you recommend using cabal or stack to package Haskell components in a Yocto layer, for sandboxed, reproducible, cross-compiled builds that are independent of host toolchains?
* The compiler is slower than most mainstream language compilers
* Its ability to effectively report errors is poorer
* It tends to have 'first error, breaks rest of compile' problems
* I don't mind the more verbose 'stack trace' of errors, but I know juniors/noobs can find that quite overwhelming.
* The tooling, although significantly better than it was, is still poor compared to other some other functional languages, and really poor compared to mainstream languages like C#
* This ^ significantly steepens the learning curve for juniors and those new to Haskell and generally gets in the way for those more experienced.
* The library ecosystem for key capabilities in 'enterprise dev' is poor. Many unmaintained, substandard, or incomplete implementations. Often trying their best to be academically interesting, but not necessarily usable.
The library ecosystem is probably the biggest issue. Because it's not something you can easily overcome without a lot of effort.
I used to be very bullish on Haskell and brought it into my company for a greenfield project. The company had already been using pure-FP techniques (functors, monads, etc.), so it wasn't a stretch. We ran a weekly book club studying Haskell to help out the juniors and newbies. So, we really gave it its best chance.
After a year of running a team with it, I came to the conclusions above. Everything was much slower -- I kept telling myself that the code would be less brittle, so slower was OK -- but in reality it sapped momentum from the team.
I think Haskell's biggest contribution to the wider community is its ideas, which have influenced many other languages. I'm not sure it will ever have its moment in the sun unfortunately.
So, it's much, much better than it was. It's still not comparable to mainstream languages, but it's going the right way. So, I wouldn't necessarily take that as the killer.
The biggest issue was the library ecosystem. We spent an not-small amount of time evaluating libraries, realising they were not up to scratch, trying to do build our own, or interacting with the authors to understand the plans. When you're trying to get moving at the start of a project, this can be quite painful. It takes longer to get to an MVP. That's tough when there are eyes on its success or not.
Even though I'd been using Haskell for at least a decade before we embarked upon that path, I hadn't really ever built anything substantial. The greenfield project was a complex beast on a number of levels (which was one of the reasons I felt Haskell would excel, it would force us to be more robust with our architecture). But, we just couldn't find the libraries that were good enough.
My sense was there's a lot of academics writing libraries. I'm not implying that academics write poor code; just that their motivations aren't always aligned with what an industry dev might want. Usually this is around simplicity and ease-of-use. And, because quite a lot of libraries were either poorly documented or their intent was impenetrable, it would take longer to evaluate.
I think if the Haskell Foundation are going to do anything, then they should probably write down the top 50 needed packages in industry, and then put some funding/effort towards helping the authors of existing libraries to bring them up to scratch (or potentially, developing their own), perhaps even create a 'mainstream adoption style guide', that standardises the library surfaces -- there's far too much variability. It needs a keen eye on what your average industry dev needs though.
I realise there are plenty of companies using Haskell successfully, so this should only be one data point. But, it is a data point of someone who is a massive Haskell (language) fan.
Haskell has had a massive influence on me and how I write code. It's directly influenced a major open-source project I have developed [1]. But, unfortunately, I don't think I'll use it again for a pro project.
My $DAYJOB language:
* Can't build a binary
* Uses an inexplicable amount of memory.
* Has an IDE which constantly puts itself into a bad state. E.g. it highlights and underlines code with red even when I know it's a pristine copy that passes its tests. I periodically have to close the project, navigate to it in the terminal, run 'git status --ignored' and delete all that crap and re-open the project.
* Is slow to start up.
* Has a build system with no obvious way to use a 'master list' of version numbers. In our microservice/microrepo system, it is a PITA to try to track down and remove a vulnerable dependency.
* Has been receiving loads of praise over the last 18 months for starting to include stuff that Haskell has included for ages. How's the latest "we're solving the null problem" going?
What the GHC compiler does for me is just so much better at producing working software than $DAYJOB language + professional $DAYJOB IDE, that I don't think about the tooling.
If you want to put yourself in my shoes: imagine you're getting shit done with TypeScript every day, and some C programmers come along and complain that it's missing the bare minimum of tools: strace, valgrind and gdb. How do you even reply to that?
You tell them to strace/valgrind node whatever.js and instead of gdb use built-in v8 debugger as node inspect whatever.js
`-fdefer-type-errors` will report those errors as warnings and fail at runtime, which is good when writing/refactoring code. Even better the Haskell LSP does this out of the box.
> * The tooling, although significantly better than it was, is still poor compared to other some other functional languages, and really poor compared to mainstream languages like C#
Which other functional programming languages do you think have better tooling? Experimenting lately with OCaml, feels like Haskell's tooling is more mature, though OCaml's LSP starts up faster, almost instantly.
F#, Scala
> OCaml's LSP starts up faster
It was two years ago that I used Haskell last and the LSP was often crashing. But in general there were always lots of 'niggles' with all parts of the tool-chain that just killed developer flow.
As I state in a sibling comment, the tooling is on the right trajectory, it just isn't there yet. So, this isn't the main reason to not do Haskell.
Since you mention F#, and C# in your previous comment, are you on the Windows platform? Maybe our experience different because of platform as well. Using GHCup to keep in sync compatible versions of GHC, Cabal and the LSP probably contributed a lot to the consistent feel of the tooling.
I use the Haskell LSP for its autocompletion, reporting compile errors in my editor, and highlighting of types under cursor. There are still shortcomings with it that are annoyances:
* When I open up vim, it takes a good 5-10 seconds (if not a bit more) until the LSP is finally running.
* When a new dependency is added to the cabal file, the LSP needs to be restarted (usually I quit vim and reopen the project).
* Still no support for goto definition for external libraries. The workaround I have to use in this case is to `cabal get dependency-version` in a gitignored directory and use hasktags to keep a tags file to jump to those definitions and read the source code/comments.
The later two have open GitHub issues, so at least I know they will get solved at some point.
Linux Mint.
Since dotnet core (now dotnet 5+), the Microsoft version of dotnet has not been tied to windows outside a few exceptions like old Windows UI libraries (WPF/WinForms) and stuff like WCF once they revived it.
Speaking of LSP, the lsp standard is developed by Microsoft so naturally any dotnet language will have good lsp support.
SBT still makes me want to throw the laptop through the window.
But we can talk specifics if you want. Name some compromises.
Sbt is better than Gradle IMO, as it has a saner mental model, although for apps you can use Gradle or Maven. Sbat has had some awesome plugins that can help in bigger teams, such as Scalafmt (automatic formatting), Scalafix (automatic refactoring), Wartremover and others. Scalafmt specifically is best in class. With Sbt you can also specify versioning schemes for your dependencies and so you can make the build fail on problematic dependency evictions.
Scala CLI is also best in class, making it comfortable to use Scala for scripting – it replaced Python and Ruby for me: https://scala-cli.virtuslab.org/
Note that Java and Kotlin have Jbang, but Scala CLI is significantly better, also functioning as a REPL. Worth mentioning that other JVM languages hardly have a usable REPL, if at all.
The Scala compiler can be slow, but that's when you use libraries doing a lot of compile-time derivation or other uses of macros. You get the same effect in similar languages (with the exception of Ocaml). OTOH the Scala compiler can do incremental compilation, and alongside Sbt's support for multiple sub-projects or continuous testing, it works fairly well.
Scala also has a really good LSP implementation, Metals, built in cooperation with the compiler team, so you get good support in VS Code or Vim. To get a sense of where this matters, consider that Scala 3.5 introduces "best effort compilation": https://github.com/scala/scala3/pull/17582
I also like Kotlin and sadly, it's missing a good LSP implementation, and I don't think Jetbrains is interested in developing it.
Also you get all the tooling that's JVM specific, including all the profilers and debuggers. With GraalVM's native image, for example, Scala fares better than Java actually, because Scala code relies less on runtime reflection.
I'd also mention Scala Native or ScalaJS which are nice to have. Wasm support is provided via LLVM, but there's also initial support for Wasm GC.
So to answer your question, yes, Scala has really good tooling compared to other languages, although there's room for improvement. And if you're comparing it to any other language that people use for FP, then Scala definitely has better tooling.
Let me give you an example … in Gradle, the order in which you specify plugins matters, due to the side effects. Say, if you specify something complex, like Kotlin's multiplatform plugin, in the wrong order with something else, it can break your build definition. I bumped into this right off the gate, with my first Kotlin project.
In Sbt this used to matter as well, but because Sbt has this design of having the build definition as an immutable data structure that's fairly declarative, people worked on solving the problem (via auto-loading), and since then, I've never bumped again into ordering issues.
There are other examples as well, such as consistency. In Sbt there's only one way to specify common settings, and the keys used are consistent. Specifying Java's targeted version, for example, uses the same key, regardless if the project is a plain JVM one, or a multiplatform one.
Sharing settings and code across subprojects is another area where Gradle is a clusterfuck, whereas in Sbt it's pretty straightforward.
Don't get me wrong, Gradle doesn't bother me, and it has some niceties too. Other ecosystems would be lucky to have something like Gradle. But I find it curious to see so many people criticizing it when almost everything else is pretty terrible, with few exceptions.
---
Note that Li Haoyi has great taste, and Mill is looking good, actually. But he also likes reinventing the wheel, and the problem with that for build tools is that standardization has value.
Standardization has so much value for me that I would have liked for Scala to use Gradle as the standard build tool, and for Scala folks to work with Gradle's authors to introduce Scala as an alternative scripting language for it, despite me liking Gradle a lot less. Because it would've made switching and cross-language JVM development easier.
I like scala-cli a lot. It's very promising, but I think it's too new to be proclaimed best-in-class yet. Time will tell, and I'm rooting for it.
Usually people do not use objects, and if they do, they don't create a tightly coupled object mess that can only be unraveled by an IDE.
I feel like OCaml has been on a fast upward trajectory the past couple of years. Both in terms of language features and tooling. I expect the developer experience to surpass Haskell if it hasn't already.
I really like Merlin/ocaml-lsp. Sure, it doesn't have every LSP feature like a tool with a lot of eyes on it, such as clangd, but it handles nearly everything.
And yeah, dune is a little odd, but I haven't had any issues with it in a while. I even have some curve-ball projects that involve a fair amount of C/FFI work.
My only complaint with opam is how switches feel a little clunky. But still, I'll take it over pip, npm, all day.
I've been using OCaml for years now and compared to most other languages, the experience has been pretty pleasant.
For what I had to do thus far, at one point I needed to step debug through my code. Whereas in GHC land I reload my project in the interpreter (GHCi or cabal repl), set a break point on function name and step through the execution. With OCaml I have to go through the separate bytecode compiler to build it with debug symbols and the I can navigate through program execution. The nice thing is that I can easily go back in execution in flow ("timetravel debugging"), but a less ergonomic. Also less experienced with this flow, to consider my issues authoritative.
I don't have that much experience with dune (aside from setting up a project and running dune build), but one thing that confused me at first, is that the libraries I have to add to the configuration do not necessarily match the Opam package names.
The LSP is fast, as mentioned before, it supports goto definition, but once I jump to a definition to one of my dependencies I get a bunch of squiggly lines in those files (probably can't see transitive dependency symbols, if I where to guess). I can navigate dependencies one level deeper than I can with the Haskell language server, though.
I actually want to better understand how to build my projects without Dune, and probably will attempt to do so in the future. The same way I know how to manage a Haskell project without Cabal. Feels like it gives me a better understanding of the ecosystem.
I'd love to know which things specifically you're thinking about. For what we've been building, the "integration" libraries for postgres, AWS, etc. have been fine for us, likewise HTTP libraries (e.g. Servant) have been great.
I haven't _yet_ encountered a library problem, so am just very curious.
You have to realise that none of the problems were insurmountable, I had a talented team who could overcome any of the issues, it just became like walking through treacle trying to get moving.
And yes, Servant was great, we used that also. Although we didn't get super far in testing its range.
One issue I ran into was that Servant didn't have a proper way of overriding content negotiation: the CAS protocol specified a "?format=json" / "?format=xml" parameter, but Servant had no proper way of overriding its automatic content negotiation - which is baked deeply into its type system. I believe at the time I came across an ancient bug report which concluded that it was an "open research question" which would require "probably a complete rework".
Another issue was that Servant doesn't have proper integrated error handling. The library is designed around returning a 200 response, and provides a lot of tooling to make that easy and safe. However, I noticed that at the time its design essentially completely ignored failures! Your best option was basically a `Maybe SomeResponseType` which in the `None` case gave a 200 response with a "{'status': 'error'}" content. There was a similar years-old bug report for this issue, which is quite worrying considering it's not exactly rocket science, and pretty much every single web developer is going to run into it.
All of this gave a feeling of a very rough and unfinished library, whose author was more concerned about writing a research paper than actually making useful software. Luckily those issues had no real-world implication for me, as I was only a student losing a few days on some minor project. But if I were to come across this during professional software development I'd be seriously pissed, and probably write off the entire ecosystem: if this is what I can expect from "great" libraries, what does the average ones look like - am I going to have to write every single trivial thing from scratch?
I really love the core language of Haskell, but after running into issues like these a few dozen times I unfortunately have trouble justifying using it to myself. Maybe Haskell will be great five or ten years from now, but in its current state I fear it is probably best to use something else.
[0]: https://en.wikipedia.org/wiki/Central_Authentication_Service
This seems to be an area where my tastes diverge from the mainstream, but I'm not a fan of folding errors together. I'd rather a http status code only correspond to the actual http transport part, and if an API hosted there has an error to tell me, that should be layered on top.
HTTP status ranges in a nutshell:
1xx: hold on
2xx: here you go
3xx: go away
4xx: you fucked up
5xx: I fucked up
One project did a bunch of calculation based on geolocation and geometry. I needed to output graphs and after looking around, reached for gnuplot. Turns out, it’s a wrapper around a system call to launch gnuplot in a child process. There is no handle returned so you can never know when the plot is done. If you exit as soon as the call returns, you get to race gnuplot to the temp file that gets automatically cleaned up by your process. The only way to eliminate the race is by sleeping… so if you add more plots, make sure you increase your sleep time too. :-/
Another utility was a network oriented daemon. I needed to capture packets and then run commands based on them… so I reached for pcap. It uses old bindings (which is fine) and doesn’t expose the socket or any way to set options for the socket. Long story short, it never worked. I looked at the various other interfaces around pcap but there was always a significant deficiency of some kind for my use case.
Now, I’m not a seasoned Haskell programmer by any means and it’s possible I am just missing out on something fundamental. However, it really looks to me like someone did a quick hack that worked for a very specific use-case for both of these libraries.
The language is cool but I’ve definitely struggled with libraries.
I’m personally quite interested in the Koka language. It has some novel ideas (functional-but-in-place algorithms, effect-handler-aware compilation, it uses reference counting rather than garbage collection) and is a Microsoft Research project. It’s starting to look more and more like an actual production-ready language. I can daydream about Microsoft throwing support behind it, along with some tooling to add some sort of Koka-Rust interoperability.
It sees sporadic bursts of activity, probably when an intern is working on it, and otherwise remains mostly dormant. There is no package manager that could facilitate ecosystem growth. There is no effort to market and popularize it.
I believe it is fated to remain a research language indefinitely.
I'm also a little sad at this defeatist attitude. What you said might be true, but those things are solvable problems. Just requires a coordinated force of will from a few dedicated individuals.
- The Haskell ecosystem doesn't have the budget of languages like Java or C# to build its tooling.
- The haskell ecosystem was innovative 20 years ago, but some newer languages like Rust or Elm have much better ergonomics due to learning from their forebearers.
Yes, it's true. And it's true for almost any smaller language out there.
The thread is "Why Haskell?", I'm offering a counterpoint based on experience. YMMV and that's fine.
Out of curiosity does this also hold true for F#?
I listed C# because that’s the mainstream language I know the best, and arguably has best-in-class tooling.
Of course you have to be prepared to lose some of the creature comforts when using a more left-field language. But, you still need to be productive. The whole ecosystem has to be a net gain in productivity, or stability, or security, or maintainability — pick your poison depending on what matters to your situation.
I had hoped Haskell would pay dividends due to its purity, expressive type-system, battle tested-ness, etc. I expected us to be slower, just not as slow as it turned out.
Ultimately the trade off didn’t work for us.
> The whole ecosystem has to be a net gain in productivity, or stability, or security, or maintainability — pick your poison depending on what matters to your situation.
I very much agree with you on this. I’ve worked in places where we used Typescript on the back-end because it was easier for a small team to work together (and go on vacations) while working in the same language even though there was a trade off performance wise. Ultimately I think it’s always about finding the best way to be productive.
Also, it has support for features that probably shouldn't be in the language, but are because of C# (interfaces and type-inheritance for example).
The compiler is slower than C# but arguably fast enough. And they have made a weird choice about ordering of source-files dictating ordering of compilation, so you have to manually sort source-files in the IDE. Which is both a pain and makes it sometimes hard to visually find your source-file because they're not in alphabetical order.
I like F# but it doesn't have enough unique features over C# to make it worthwhile imho.
Disclaimer: The last time I wrote any F# was about 5 years ago. Things may be different now!
I'm also curious about the slowness of compilation and whether that's intrinsic to the design of GHC.
Simplifying cabal probably, though that's a system-level problem, just just a cabal codebase problem.
I like it better than the ormolu family because it respects your placement of comments and just formats the code itself. But it isn’t maintained as of a few years ago.
As for GHC compile times... hard to say. The compiler does do a lot of things. Type checking and inference of a complex type system, lots of optimizations etc. I don't think it's just some bug/inefficient implementation, bc. resources have been poured into optimizations and still are. But there are certainly ways to improve speed. For single issues, check the bug-tracker: https://gitlab.haskell.org/ghc/ghc/-/issues/?label_name%5B%5...
For the big picture, maybe ask in the discourse[1] or the mailing list. If you want to contribute to the compiler, I can recommend that you ask for a gitlab account via the mailing list and introduce youself and your interests. Start by picking easy tickets - GHC is a huge codebase, it takes a while to get familiar.
Other than that, I'd say some of the tooling could use some IDE integration (e.g., VS Code plugins).
Also, I have found very little use for the various runtime flags like `+RTS -p`. These flags aren't serious debugging tools; I couldn't find any way to even trigger them internally at runtime around a small section, which becomes a problem when it takes 10 minutes to load data from disk when the profiler is on.
The debugging situation with Haskell is really, really bad and it's enough that I try to steer people away from starting new projects with the language.
All mainstream, general purpose programming languages are (basically) Turing-complete, and therefore any programme you can write in one you can, in fact, write in another. There is a computational equivalence between them. The main differences are instead in the expressiveness of the languages, the guardrails they give you, and their performance characteristics (although this is possibly more of a runtime/compiler implementation question).
I decided to have a go at learning the basics of Haskell and the first error I got immediately phased me because it reminded me of unhelpful compilers of the 80s. I have bashed my head against different languages and poor tooling enough times to know I can learn, but I've also done it enough times that I am unwilling to masochistically force myself through that gauntlet unless I have a very good reason to do so. The "joy" of learning is absent with unfriendly tools.The syntax summary in the article is really good. Short and clear.
Mostly because while I found of the tooling occasionally difficult, I didn’t find Haskell particularly bad compared to other language ecosystems I’ve played with, with the exception of Rust, for which the compiler errors are really good.
> The syntax summary in the article is really good
Thanks, I wasn’t so sure how to balance that bit.
That stuck out to me as well, I said out loud "that is a very Haskell thing to say". It would be more accurate to say that Turing Completeness means that any programme you write in one language, may be run in another language by writing an emulator for the first programme's runtime, and executing the first programme in the second.
Because it is not "in fact" the case that a given developer can write a programme in language B just because that developer can write the program in language A. It isn't even "in principle" the case, computability and programming just aren't that closely related, it's like saying anything you can do with a chainsaw you can do with a pocketknife because they're both Sharp Complete.
I shook it off and enjoyed the rest of the article, though. Haskell will never be my jam but I like reading people sing the virtues of what they love.
> computability and programming just aren’t that related
I … don’t think I understand
> I … don’t think I understand
That's such a Haskell thing to say!
Ok, I'm teasing a bit now. But there's a kernel of truth to it: a good model of the FP school which forked off Lisp into ML, Miranda, Haskell, is as an exploration of the question "what if programming was more like computability theory?", and fairly successfully, by its own "avoid success at all costs" criteria.
Computability: https://en.wikipedia.org/wiki/Computability_theory
> Computability theory, also known as recursion theory, is a branch of mathematical logic, computer science, and the theory of computation that originated in the 1930s with the study of computable functions and Turing degrees.
Programming: https://en.wikipedia.org/wiki/Computer_programming
> Computer programming or coding is the composition of sequences of instructions, called programs, that computers can follow to perform tasks.
Related, yes, of course, much as physics and engineering are related. But engineering has many constraints which are not found in the physics of the domain, and many engineering decisions are not grounded in physics as a discipline.
So it is with computability and programming.
> “therefore you can write any programme in Haskell” which, while true
It is not. That's my point. One can write an emulator for any programme in Haskell, in principle, but that's not at all the same thing as saying you can write any programme in fact.
For instance, you cannot write this in Haskell:
You could write something in Haskell in which you could write this, but those are different complexity classes, different programs, and very, very different practices. They aren't the same, they don't reduce to each other. You can write an AVR emulator and run avrforth in it. But that's not going to get the blinkenlichten to flippen floppen on the dev board.
Haskell, in fact, goes to great lengths to restrict the possible programs one can write! That's one of the fundamental premises of the language, because (the hope is that) most of those programs are wrong. About the first half of your post is about things like accidental null dereferencing which Haskell won't let you do.
In programming, the tools one chooses, and ones abilities with those tools, and the nature of the problem domain, all intersect to, in fact, restrict and shape the nature, quality, completeness, and even getting-startedness, of the program. Turing Completeness doesn't change that, and even has limited bearing on it.
> In programming, the tools one chooses, and ones abilities with those tools, and the nature of the problem domain, all intersect to, in fact, restrict and shape the nature, quality, completeness, and even getting-startedness, of the program.
Language shapes thought and hence once the simpler Imperative programming models (Procedural, OOP) are learnt it becomes quite hard for the Programmer to switch mental models to FP. The FP community has really not done a good job of educating such programmers who are the mainstay in the Industry.
So my original intent with that paragraph was very different, but you're right that I was not very precise with some of those statements.
Thanks for taking the time to explain, you've definitely helped expand the way I've thought about this.
From what I understand about the link, you likely meant that one cannot write an interpreter for avrforth in Haskell which reads avrforth source code and executes it on bare metal, because such an interpreter will need to directly access the hardware to be able to manipulate individual bits in memory, access registers, ports, etc. and all of this is not possible in Haskell today. If this is not the case, please feel free to correct me.
However, if my understanding is correct, I don't see how this is a problem of Haskell being a functional or "leaning more towards computability theory" language rather than a mismatch of model of computation between the language and the hardware. Haskell can perform IO just fine by using the IO monad which uses system calls under the hood to interact with the hardware. If a similar mechanism is made available to Haskell for accessing the hardware directly (e.g. a vector representing the memory and accessible within the IO monad), it should be possible to write an interpreter for avrforth in Haskell. This means that the current constraint is a tooling/ecosystem limitation rather than a limitation of language itself.
I say this as someone who prefers symbols, point free, and highly appreciates "notation as a tool of thought".
Sort of. It has a "failure at a stage prevents progress to next stage", so a parse error means you won't type check (or indeed, continue parsing). See these proposals for some progress on the matter
* https://github.com/haskellfoundation/tech-proposals/pull/63
I didn't mention refactoring in my list because it may just be personal experience: my style of coding is to write fearlessly knowing that I will also refactor fearlessly. So less upfront thinking, more brute force writing (on instinct) & aggressive refactoring. I find I get my solution much faster and it ends up being more elegant.
Having a parse error or a type inference error in another module causing all other inference to fail kills the refactoring part of that process where there are syntax/semantic errors everywhere for a period of time whilst I fix them up.
It's good to see the issue acknowledged and hopefully resolved in the future.
Additionally, it would be good to see some proper refactoring tooling. Renaming, moving types/functions from one module to another, etc.
Doesn't HLS have renaming, at least?
Depends on which mainstream languages one compares with; there's always C++.
My project here has 50k lines Haskell, 10k C++, 50k lines TypeScript (code-only, not comments). Counting user CPU time (1 core, Xeon E5-1650 v3 3.50GHz):
TypeScript 123 lines/s
Haskell 33 lines/s
C++ 7 lines/sFor comparison, I just compiled a 25k line .cpp (probably upwards of 100k once you add all the headers it includes) from a large project, in 15s. Admittedly, on a slightly faster processor - let's call it 30s.
(Insert "This is C++" Sparta image here.)
It is quite clean application code but it _uses_ some of the most template heavy open-source libraries around (e.g. Eigen, CGAL, boost) -- all hallmarks of the strength of C++.
If you look at other popular open-source C++ projects, such as Ceph or the Point Cloud Library (PCL), 8-hour single-core compile times are, unfortuanately, normal.
I fully agree that C++ code bases that are more C-like compile much faster. But many typical C++ projects that use standard C++ features compile at 7 lines per second.
The same holds for Haskell: If you write very simple code and do not use common functionality (TemplateHaskell, Generics deriving), you'll also get a 20x compile time speedup.
All the other FP stuff (union types, etc) existed before Haskell in non-lazy FP languages.
I’m not sure I’m experienced enough in PLT enough to have a strong opinion myself.
However, from experience, laziness has a lot of advantages both from a program construction and performance perspective.
(Of course a small minority of people don't consider laziness as a mistake as it enables equational reasoning; let's not go there.)
From everything I've ready about people's experiences with using Haskell for large projects, it sounds like lazy evaluation unfortunately adds more problems than it removes.
This is quite the claim. I know plenty of experienced and productive Haskellers who disagree with this (myself included)
I try them out (ocaml,haskell,clojure,etc) from time to time and think they're fairly interesting, but i struggle to figure out how to make bigger programs with them as I've never seen how you build up a code base with the tools they provide and with someone to review the code i produce and so never have any luck with jobs i've applied to.
On the flipside I never had too much trouble figuring out how to make things with Go, as it has so little going on and because it was the first language i worked with professionally for an extended period of time. I think that also leads me to trying to apply the same patterns because I know them even if they dont really work in the world of functional languages
Not sure what the point of this comment is, but I think i just want to experience the moment of mind opening-ness that people talk about when it comes to working with these kinds of languages on a really good team
1. Decide on an effect system
Remember, Haskell is pure, so any side-effect will be strictly explicit. What broad capabilities do you want? Usually, you need to access some program-level configuration (e.g. command-line options) and the ability to do IO (networking, reading/writing files, etc), so most people start with that.
https://tech.fpcomplete.com/blog/2017/06/readert-design-patt...
2. Encode your business logic in functions (purely if possible)
Your application does some processing of data. The details don't matter. Use pure functions as much as possible, and factor effectful computations (e.g. database accesses) out into their own functions.
3. Glue everything together in a monadic context
Once you have all your business logic, glue everything together in a context with your effect system (usually a monad stack using ReaderT). This is usually where concurrency comes in (e.g. launch 1 thread per request).
---
Beyond this, your application design will depend on your use-case.
If you are interested, I strongly suggest to read 'Production Haskell' by Matt Parsons, which has many chapters on 'Haskell application structure'.
This shouldn't even be proposed as a question to someone new to Haskell. They should learn how monad transformers work and just use them. 90% of developers playing around effect systems would be just fine with MTL or even just concrete transformers. All Haskell effect systems should be considered experimental at this point with unclear shelf lives.
Everything else you said I agree with as solid advice!
Once you've learned what is necessary to, say, modify already-existing applications, you should be familiar with monads and some basic monad transformers like ReaderT.
Once you're there, I don't think 'choosing an effect system' is a perilous question. The monad transformer library, mtl, is an effect system, the second simplest one after IO.
> Once you're there, I don't think 'choosing an effect system' is a perilous question. The monad transformer library, mtl, is an effect system, the second simplest one after IO.
I'm aware of that, generally when people say "choose effect system" they mean choose some algebraic effect system, all of which (in Haskell) have huge pitfalls. The default should be monad transformers unless you have some exceptional situation.
I should have mentioned that, as you say, monad transformers should be the default effect system choice for 99% of people
This isn't really true. Bluefin and effectful are effect systems, but not algebraic effect systems.
This is highly debatable. I would say that the effect systems effectful and Bluefin are actually significantly simpler than MTL and transformers, particularly as soon as you need to do prompt resource cleanup.
Personally I'd say that if newbies should start with naked IO and then switch to effectful or Bluefin once they've realised the downside of IO being available everywhere.
> All Haskell effect systems should be considered experimental at this point with unclear shelf lives.
effectful and Bluefin are here to stay. I guarantee it. For non-IO-based effect systems (e.g. polysemy, freer-effects) I agree.
(Disclaimer: I'm the author of Bluefin)
Just to be clear I think Bluefin is really cool and I'm a fan of your work overall.
I'm speaking from the industry/production perspective here. When polysemy and freer-effects were released there was a similar belief that they were here to stay.
transformers and MTL have stood the test of time, are heavily documented, and are pervasive throughout Hackage. Understanding them and how to build programs with them is essential for anyone trying to break into 'production haskell' as a career move.
Thanks!
> I'm speaking from the industry/production perspective here. When polysemy and freer-effects were released there was a similar belief that they were here to stay.
Well, from my perspective, I've never worked at a place that's used polysemy of freer-effects.
> transformers and MTL have stood the test of time, are heavily documented, and are pervasive throughout Hackage. Understanding them and how to build programs with them is essential for anyone trying to break into 'production haskell' as a career move.
Sadly true.
I have with another deprecated effect library. Its a bummer to have something so core to the architecture that is deprecated but then to not have the time or buy-in to do anything about it.
I haven't published my effectful-Bluefin interoperation layer, but you can see it here:
https://github.com/tomjaguarpaw/bluefin/blob/caa59700d910c76...
You mention performance. However, if we look at how many Python shops there are, this can hardly be a problem. I imagine ecosystems to be a much bigger issue than performance. Many implementations of functional languages have better performance than Python anyway.
There are many reasons why a company can have issues retaining people. A shitty uninteresting product, bad management, low wages, bad culture ... Lets eliminate those and see whether they still have issues retaining devs. I suspect, that an interesting tech stack could make people stay, because it is not so easy to find a job with such a tech stack.
However, many companies want easily replaceable cogs, which FP programmers are definitely not these days. So they would rather hire low skill easily replaceable than highly skilled but more expensive workforce. They know they will not be able to retain the highly skilled, because they know their other factors are not in line for that.
So the teams self-select to let you work with the people you want to work with? Tell me more!
There are still plenty of ways to do things wrong. Strong types don't prevent that. Laziness is a double-edged sword and can be difficult to reason about.
I am one of these people.
People are much more reluctant to share what it is that led them to the conclusion that Haskell isn't something they want to use professionally, or something they can't use professionally. It's a combination of things, such as it just generally being less energizing to talk about that, and also some degree of frankly-justified fear of being harassed by people who will argue loudly and insultingly that you just Don't Get It.
I am not one of those people.
I will share the three main reasons I don't even consider it professionally.
First, Hacker News has a stronger-than-average egalitarian streak and really wants to believe that everybody in the world is already a developer with 15 years of experience and expert-level knowledge in all they survey from atop their accomplished throne, but that's not how the real world works. In the real world I work with coworkers who I have to train why in my Go code, a "DomainName" is a type instead of just a string. Then, just as the light bulb goes off, they move on from the project and I get the next junior dev who I have to explain it to. I'm hardly going to get to the point where I have a team of people who are Haskell experts when I'm explaining this basic thing over and over.
And, to be 100% clear, this is not their "fault", because being a junior programmer in 2024 is facing a mountain of issues I didn't face at their age: https://news.ycombinator.com/item?id=33911633 I wasn't expected to know about how to do source control or write everything to be rollback-able or interact with QA, or, well, see linked post for more examples. Haskell is another stack of requirements on top of a modern junior dev that is a hell of an ask. There better be some damn good reasons for me to add this to my minimim-viable developer for a project. I am not expressing contempt for the junior programmers here from atop my own lofty perch; I am encouraging people to have sympathy with them, especially if you also come up in the 90s when it was really relatively easy, and to make sure you don't spec out projects where you're basically pulling the ladder up after yourself. You need to have an onboarding plan, and "spend a whole bunch of time learning Haskell" is spending a lot of your onboarding plan currency.
Second, while a Haskell program that has the chef's kiss perfect architecture is a joy to work with, it is much more difficult to get there for a real project. When I was playing with Haskell it was a frequent occurrence to discover I'd architected something wrong, and to essentially need to rewrite the whole program, because there is no intermediate functioning program between where I was and where I needed to be. The strength of the type system is a great benefit, but it does not put up with your crap. But "your crap" includes things like being able to rearchitect a system in phases, or partially, and still have a functioning system, and some other things that are harder to characterize but you do a lot of without even realizing it.
I'd analogize it to a metalworker working with titanium. If you need it, you need it. If you can afford it, great. The end result is amazing. But it's a much harder metal to work with for the exact same reason it's amazing. The strength of the end part is directly reflected in the metal resisting you working with it.
I expect at a true expert level you can get over this, but then as per my first point, demanding that all my fellow developers become true experts in this obscure language is taking it up another level past just being able to work in it at all.
Finally, a lot of programming requirements have changed over the years. 10-15 years ago I could feasibly break my program into a "functional core" and an external IO system. This has become a great deal less true, because the baseline requirement for logging, metrics, and visibility have gone up a lot, and suddenly that "pure core" becomes a lot less appealing. Yes, of course, our pure functions could all return logs and metrics and whathaveyou, and sure, you can set up the syntax to the point that it's almost tolerable, but you're still going to face issues where basically everything is now in some sort of IO. If nothing else, those beautiful (Int -> Int -> Int) functions all become (Int -> Int -> LoggingMetrics Int) and now it isn't just that you "get" to use monadic interfaces but you're in the LoggingMetrics monad for everything and the advantages of Haskell, while they do not go away entirely, are somewhat mitigated, because it really wants purity. It puts me halfway to being in the "imperative monad" already, and makes the plan of just going ahead and being there and programming carefully a lot more appealing. Especially when you combine that with the junior devs being able to understand the resulting code.
In the end, while I still strongly recommend professional programmers spend some time in this world to glean some lessons from it that are much more challenging to learn anywhere else, it is better to take the lessons learned and learn how to apply them back into conventional languages than to try to insist on using the more pure functional languages in an engineering environment. This isn't even the complete list of issues, but they're sufficient to eliminate them from consideration for almost every engineering task. And in fact every case I have personally witnessed where someone pushed through anyhow and did it, it was ultimately a business failure.
What a beautiful, succinct analogy. I'm stealing this.
I’d say you missed one of the main points of Haskell and functional programming in general.
The combinator is the most modular and fundamental computational primitive available in programming. When you make a functional program it should be constructed out of the composition of thousands of these primitive with extremely strict separation from IO and multiple layers of abstraction. Each layer is simply composed functions from the layer below.
If you think of fp programming this way. It becomes the most modular most reconfigurable programming pattern in existence.
You have access to all layers of abstraction and within each layer are independent modules of composed combinators. Your titanium is given super powers where you can access the engine, the part, the molecule and the atom.
All the static safety and beauty Haskell provides is actually a side thing. What Haskell and functional programming in general provides is the most fundamental and foundational way to organize your program such that any architectural change only requires you replacing and changing the minimum amount of required modules. Literally the opposite of what you’re saying.
The key is to make your program just a bunch of combinators all the way down with an imperative io shell that is as thin as possible. This is nirvana of program organization and patterns.
One of the reasons you end up with "refactoring the entire program because of some change" is when you discover that your entire composition scheme you built your entire program around is wrong, e.g., "Gee, this effects library I built my entire code base around to date is really nifty but also I can't actually express my needs in it after all". In a conventional language, you just build in the exceptions, and maybe feel briefly sad, but it works. It can ruin a codebase if you let it, but it's at least an option. In Haskell, you have a much bigger problem.
Now filter that back through what I wrote. You want to explain to your junior developer who is still struggling with the concept of using things other than strings why we have to rewrite the entire code base to use raw IO instead of the effects system we were using because it turns out the compilation time went exponential and we can't fix it in any reasonable amount of effort? How happy are they going to be with you after you just spent a whole bunch of time explaining the way to work with the effects system? They're not going to come away with a good impression of either Haskell or you.
This is why I recommend IO-based effect systems like Bluefin and effectful. If you find that you get stuck you always have the escape hatch of just doing whatever you want in IO. Maybe feel briefly sad, but it works.
(I'm the author of Bluefin)
Use modules as your basic unit of abstraction. Don’t go out of your way to make their organization over-engineered, but each module should basically do one thing, and should define everything it needs to do that thing (types, classes, functions).
Use parametric polymorphism as much as you can, without making the code too hard to read. Prefer functions and records over type classes as much as possible. Type classes that only ever have a single instance, don’t have laws, or type classes defined for unit data types are major code smells.
Don’t worry about avoiding IO, but as much as you can try to keep IO code separate from pure code. For example, if you need to read a value from the user, do some calculations, then print a message, it’s far better to factor the “do some calculations” part out into a pure function that takes the things you read in as arguments and returns a value to print. It’s really tempting to interleave logic with IO but you’ll save so much time, energy, and pain if you avoid this.
Essentially, keep things as simple as you can without getting belligerent about it. The type system will help you a lot with refactoring.
Start at the beginning. Write functions. When you see some piece of functionality that you need, use `undefined` to make a placeholder function. Then, go to your place holder and start implementing it. Use undefined to fill in bits that you need, and so on.
Fancy types are neat but it’s easy to end up with a solution in search of a problem. Avoid them until you really have a concrete problem that they solve- then embrace them for that problem (and only that problem).
You’ll refactor a lot, and learn to have a better gut feeling for how to structure things, but that’s just the process of gaining experience. Leaning into the basics of FP (pure functions, composed together) will be the path of least resistance as you are getting there.
- how easy is it to make a web application with a hello world endpoint?
- How easy is it to auth a JWT?
- Is there a good ORM that supports migrations?
- Do I have to remodel half my type system because a product owner told me about this weird business logic edge case we have to deal with?
- How do I do logging?
Etc.
If that's all you want it to do, it's very easy with Wai/Warp.
> - How easy is it to auth a JWT?
We don't use JWTs, but we did look at it and Servant (which is a library for building HTTP APIs) has built in functionality for them.
> - Is there a good ORM that supports migrations?
There are several with quite interesting properties. Some (like persistent) do automatic migrations based on your schema definitions. Others you have to write migration SQL/other DSL.
> - Do I have to remodel half my type system because a product owner told me about this weird business logic edge case we have to deal with?
I think that's going to really depend on how you have structured your domain model, it's not a language question as much as a design question.
> - How do I do logging?
We use a library called Katip for logging, but there are others which are simpler. You can also just print to stdout if you want to.
For the "hello world" webserver, this might be a bit instructive: https://github.com/gfarrell/gtf.io/blob/main/src/GTF/Router....
Imagine you talk to someone who has done assembly his whole life and now wants to write something in, let's say, Java.
What would you think if he asks the question in the way you did?
Sometimes, when you learn a language that is so different you really really should NOT try to assume that your current knowledge just translates.
The type system in Haskell, particularly union types, is incredibly powerful, easy to understand for the most part (you don't need to understand monads that deeply to use them), and highly useful.
And I've had a lot of fun micro-optimizing Haskell code for Project Euler problems when I was studying.
Give it a try. Especially, if you don't know what to expect, I can guarantee that you'll be surprised!
Granted, the tooling is sh*t.
In real life, I find that Haskell suffers from trying too hard to use the most general concept that‘s applicable (no pun intended). Haskell programs happily use “Either Err Val” and “Left x” where other languages would use the more expressive but less general “Result Err Val” and “Error x”. Also, I don’t want to mentally parse nested liftM2s or learn the 5th effect system ;-)
Whoever wrote that is wrong.
You are the one who is wrong, I'm afraid.
Rust's most important feature! The bootstrapped implementation.
Additionally, unlike some languages that are formally specified before turning to implementation, Rust has subscribed to design-by-implementation. The implementation is the language.
I'm not one to hold that one shouldn't form tentative conclusions until one "has all the fact". Also, I'm not one to hold that readers should trust the opinion of an internet comment writer they know nothing about. I could write a long explanation to support my opinion, but I'm probably not going to.
If not, what is the relevance of your comment?
https://github.com/mozilla/rust/tree/ef75860a0a72f79f97216f8...
> Haskell (GHC): typeclasses, type families
Although SML is older (1983), OCaml is younger than Haskell.
In Python, it's very easy to just write out a for-loops to do these things, and you don't necessarily go looking for alternative ways to do these things unless you know the functional equivalents already. But in Haskell you're forced to do things this way since there is no for-loop available. But after learning that way of thinking, the result is then more compact code with arguably less risk of bugs.
map(function, iterable)
That seems very logical to me, but then, I’m not a functional programmer, I just like map. It’s elegant, compact, and isn’t hard to understand. Not that list comps are hard to understand either, but they can sometimes get overly verbose.filter has also lost ground in favor of list comps, partially because Guido hates FP [0], and probably due to that, there has been a lot of effort towards optimizing list comps over the years, and they’re now generally faster than filter (or map, sometimes).
[0]: https://www.artima.com/weblogs/viewpost.jsp?thread=98196
map(func4, map(func3, map(func2, map(func1, iter))))
vs iter.map(f1).map(f2).map(f3).map(f4)
I made up the syntax for the last one, but most functional languages have a nice syntax for it. Here's F#: iter |> f1 |> f2 |> f3 |> f4
Or plain shell: command | f1 | f2 | f3 | f4Use generator syntax, which is really the more pythonic way to it.
>>> iter = [1,2,3,4]
>>> f1 = lambda x: x*2
>>> f2 = lambda x: x+4
>>> f3 = lambda x: x*1.25
>>> [f3(f2(f1(x))) for x in iter]
[7.5, 10.0, 12.5, 15.0]Second, that's all good and well if all you want to do is map. But what if you need combinations of map and filter as well? You're suddenly dealing with nested comprehensions, which few people like.
In F#, it'll still be:
iter |> f1 |> f2 |> f3 |> f4
Here's an example from real code I wrote: graph
|> Map.filter isSubSetFunc
|> Map.filter doesNotContainFunc
|> Map.values
|> Set.ofSeq
This would not be fun to write in List Comprehensions, but you could manage (only two list comprehensions). Now here's other code: graph
|> removeTerminalExerciseNodes
|> Map.filter isEmpty
|> Map.keys
|> Seq.map LookUpFunc
|> Seq.map RemoveTrivialNodes
|> Seq.sortBy GetLength
|> Seq.rev
|> Seq.toList
BTW, some of the named functions above are defined with their own chain of maps and filters. iter = [1,2,3,4]
f1 = lambda x: x*2
f2 = lambda x: x+4
f3 = lambda x: x*1.25
result = iter
for f in [f1, f2, f3]:
result = [f(v) for v in result]
Then the list comprehension can be moved up to mimic more closely what you're doing with F#, allowing for operations other than "map": result = iter
for f in [
lambda a: [f1(v) for v in a],
lambda a: [f2(v) for v in a],
lambda a: [f3(v) for v in a],
]:
result = f(result)
And a step further if you don't like the "result" reassignment: from functools import reduce
result = reduce(lambda step, f: f(step), [
lambda a: [f1(v) for v in a],
lambda a: [f2(v) for v in a],
lambda a: [f3(v) for v in a],
], iter)In my F# file of 300 lines[1], I do this chaining over 20 times in various functions. Would you really want to write the Python code your way every time, or wouldn't you prefer a simpler syntax? People generally don't do it your way often because it has a higher mental burden than it does with the simple syntax in F# and other languages. I don't do it 20 times because of an obsession, but because it's natural.
[1] Line count is seriously inflated due to my habit of chaining across multiple lines as in my example above.
For example: https://ryi.medium.com/flexible-piping-in-python-with-pipey-...
And another mentioned there: https://pypi.org/project/pipe/
> It's certainly not as clean as F# but neither is it as bad as the original example if there's a lot of functions
items = [1,2,3,4]
gen1 = (x*2 for x in items)
gen2 = (x+4 for x in gen1)
gen3 = (x*1.25 for x in gen2)
result = list(gen3)
It's nicer in a way, certainly closer to the pipe syntax the commenter your replying to is looking for, but kind of janky to have to name all the intermediate steps.This is still backwards?
filter(lambda x: x<5, map(lambda x: 2*x, [1,2,3,4,5]))
filter (<5) . map (*2) $ [1,2,3,4,5]
(Technically the Python version should be cast to a list to have identical behavior.)Same with comprehensions (although nesting comprehensions will always get weird).
[x for x in [2*x for x in [1,2,3,4,5]] if x<5]
[x | x <- [2*x | x <- [1,2,3,4,5]], x<5]Laziness is mostly an anti-pattern.
I hear this a lot, but am curious about two things: (a) which bit(s) of the toolchain are you thinking about specifically -- I know HLS can be quite janky but I haven't really been blocked by any tooling problems myself; (b) have you done much Haskell in production recently -- i.e. is this scar tissue from some ago or have you tried the toolchain recently and still found it to be lacking?
I’ve experienced this too, the tools can certainly be improved, but also a little more understanding of what they do and how to interpret their error messages could help you (I am guessing).
Java has recently added sealed classes/interfaces which offer the same features as sum types, and I would argue that Java is definitely mainstream.
Kotlin has a similar feature. It might be used less than Java, but it's the default language for Android.
Swift has `enum` for sum types and is the default language for iOS and MacOS.
Likewise for Rust, which is gaining traction recently.
Typescript also has union/sum types and is gaining lot of traction.
How does the ABI work? If a library adds a new constructor, but I am still linking against the old version, I imagine that it could be reading the wrong fields, since the constructor it's reading is now at a different index?
optional_int: int | None = None
Python's `A | B` is a union operation, but in Rust a sum type is always a disjoint union. In Python, if `A = B = None`, then `A | B` has one possible instance.
In Rust, this sum type has two possible instances. This might not sound like a big deal, but the semantics are quite different.
def foo(int | None = None) ...
... just means the variable's default value is None in a function definition. But it could be either in an actual function call.
Say you wanted to define some function taking `YYMMDD | MMDDYY`. If both YYMMDD and MMDDYY are just aliases to `str`, then you gain no information, you cannot discriminate on which one it is, since the union `str | str` just reduces to `str`.
Sum types are disjointed unions, you can't just say `str | str`, the terms are wrapped in unique nominal data constructors, like:
enum Date { MMDDYY(String), YYMMDD(String) }
Then when accepting a `Date` you can discriminate which format it's in. You could do the same in Python by defining two unique types and using `MMDDYY | YYMMDD`.
> And I've had a lot of fun micro-optimizing Haskell code for Project Euler problems when I was studying.
Sounds a lot like my experience. I never really used Haskell for "real work", where I need support for high-performance numerical calculations that is simply better in other languages (Python, Julia, C/C++, Fortran).
But learning functional programming through Haskell – mostly by following the "Learn you a Haskell" book and then spending time working through Project Euler exercises using it – had a quite formative effect on how I write code.
I even ended up baking some functional programming concepts into my Fortran code later. For instance, I implemented the ability to "map" functions on my data structures, and made heavy use of "pure functions" which are supported by the modern Fortran standard (the compiler then checks for side effects).
It's however hard to go all the way on functional programming in HPC contexts, although I wish there were better libraries available to enable this.
I think it is a shame Haskell has gained a reputation of being hard, because it can be an enriching learning experience. Lots of its complexity is accidental, and comes from the myriad of language extensions that have been created for research purposes.
There was an initiative to define a simpler subset of the language, which IMHO would have been great, but it didn't take off: https://www.simplehaskell.org. Ultimately, one can stick to Haskell 98 or Haskell 2010 plus some newer cherry-picked extensions.
In my time learning Haskell a decade ago, it was rare to find some code that wasn't using an experimental extension.
Interestingly, Elm has inspired a host of "successors", including Gleam + Lustre, which look really great (I haven't had a chance to really try them yet).
With haskell you can look to the ecosystem to see how to accomplish specific things with a pure functional approach. When you look at ocaml projects in that way you often find people choosing not to.
I've tried and failed several times to write Haskell in an Elm style, even though the syntax is so similar. It's probably me (it's definitely me!), but I've found that as soon as you depend on a library or two outside of prelude their complexities bleed into your project and eventually force you into peppering that readable, simple code with lifts, lenses, transformations and hidden magic.
Not to mention the error messages and compile times make developing in Haskell a chore in comparison.
p.s. Elm has not been abandoned, it's very active and getting better every day. You just can't measure by updates to the (stable, but with a few old bugs) core. For a small, unpopular language there is so much work going into high quality libraries and development tools. Check out
https://elmcraft.org/lore/elm-core-development
for a discussion.
Elm is so nice to work in. Great error messages, and near instant compile times, and a great ecosystem of static analysis, scaffolding, scripting, and hot reloading tools make the live development cycle super nice - it actually feels like what the lispers always promised would happen if we embraced repl-driven development.
I feel like I understand that perspective, but I also don't think I'm wrong in claiming Elm has been effectively abandoned in a world where an FAQ like that needs to be written.
I'm not going to try to convince you though, enjoy Elm!!
Except the obvious functional programming concepts, one thing I enjoyed exploring in Haskell was “infinite data structures” and “lazy evaluation” – like how you could provide a recursion relation to define an infinite list of numbers, but only the finite elements in the list you ask to print are actually calculated behind the scenes. This “lazy evaluation” can be used to build algorithms that would be completely unreasonable in any other language.
I also liked how easy it is to define new data structures. Like implementing a minimal tree data structure can be done in literally one line of code, which is just ridiculous compared to most languages that I’ve tried.
My take on this is that this was all nice and interesting but a lot of this stuff was a bit academic. F# is probably the closest the community got to having a mature tooling and developer ecosystem.
I don't use Haskell myself and have no strong opinions on the topic. But usually a good community response to challenges like this is somebody stepping up and doing something about it. That starts with caring enough. If nobody cares, nothing happens.
Smalltalk kicked off a small tool revolution in the nineties with its refactoring browser. Smalltalk was famous for having its own IDE. That was no accident. Alan Kay, who was at Xerox PARC famously said that the best way to predict the future was to invent it. And of course he was (and is) very active in the Smalltalk community and its early development. Smalltalk was a language community that was from day one focused on having great tools. Lots of good stuff came out of that community at IBM (Visual Age, Eclipse) and later Jetbrains and other IDE makers.
Rust is a good recent example of a community that's very passionate about having good tools as well. Down to the firmware and operating system and everything up. In terms of IDE support they could do better perhaps. But I think there are ongoing efforts on making the compiler more suitable for IDE features (which overlap with compiler features). And of course Cargo has a good reputation. That's a community that cares.
I use Kotlin myself. Made by Jetbrains and heavily used in their IDEs and toolchains. It shows. This is a language made by tool specialists. Which is why I love it. Not necessarily for functional purists. Even though som Scala users have reluctantly switched to it. And the rest is flirting with things like Haskel and Elixir.
The languages of engineering-aligned communities may appear to have won the race, though they have been adopting significant ideas from Haskell and related languages in their victories.
Haskell's biggest benefit is functions, not methods. To define a function, you need to stop directly mutating, and instead rely maps, folds, filters, etc. The bargain was: you give up your familiar and beloved for-loops, and in return you get software that will yield the same output given the same input.
So what happened with the adoption? The Java people willingly gave up the for-loops in favour of the Streams/maps/filters. But they didn't take up the reward of software that yields the same input given the same output.
What's something else in the top-5 killer Haskell features? No nulls. The value proposition is: if you have a value, then you have a value, no wondering about whether it's "uninitialised". The penalty you pay for this is more verbosity when representing missing values (i.e. Maybe).
Again, the penalty (verbose missing values ie. Optional<>) was adopted, and the reward (confidently-present values) was not.
JavaScript promises can work analogously to the Maybe monad if you want them to.
Swift’s optionals are essentially the same thing as the Maybe monad.
Ah, the joys of having a Scala `Option` type and still having to consider the cases or Some[thing], Nothing and... null!
Yes, well-written Scala code knows not to use null with reckless abandon, but since when your coworkers coming from Java know to show restraint?
I'd say it's more of a reflection of how having a very big company funding the language is making a difference.
People like to link Haskell's situation to its academic origins, but in reality, most of the issues with the ecosystem are related to acute underfunding compared to mainstream languages.
Either the community is large enough for it, or it comes from the sponsoring company.
Few languages start off by being in the first situation. The first example that comes to my mind (Python), well... Tooling was a long and painful road. And if the language hadn't been used/backed by many prominent companies, I don't see how man-hours would have flowed into tooling.
Also time wise it's a good example because python emerged early nineties around the same time the Haskell community started forming. Haskell had a few years head start actually.
The difference was that python became popular quite early in things like Linux distributions and even though Haskell was available and similarly easy to install in those, it never really caught on. Sponsored development usually happens as a result of people finding uses for a language, not before.
Exactly the same for me.
> Granted, the tooling is sh*t.
Stack and Stackage (one of the package managers and library distribution systems in Haskell-land) is the best I found in any language.
Other than that I also found some tools to be lacking.
All other ecosystems there are some libs that do not work together well, and usually you find out the hard way.
This is valuable because you as the developer have a lot less stuff to think about when you're trying to reason about your program's behavior.
>> Would you mind explaining what you mean by stateless?
> the same input to a function produces the same output every time
It's good that the question about 'stateless' was raised, but because these are two different things. Working with pure functions does indeed have the above benefits, but a dumb web node deferring its behaviour to a stateful database is not stateless in that sense, and so does not have the above benefits.
FP statelessness means no mutable state, so two identical GETs will of course return the same result.
And I will as strongly as possible emphasize the opposite you should not.
If you are are already experienced in functional programming, as well as in statically typed functional programming or something lovely in the ML family of languages then only then does Haskell make sense to learn.
If you are looking to learn about either FP in general, or staticly typed FP Haskell is about the single worst language anyone can start with. More people have been discouraged from using FP because they started with Haskell than is probably appreciated. The effort to insight ratio for Haskell is incredibly high.
You can learn the majority of the concepts faster in another language with likely 1/10th the effort. For general FP learn clojure, Racket, or another scheme. For statically typed FP learn F# or Scala or OCAML or even Elm.
In fact if you really want to learn Haskell is is faster to learn Elm and then Haskell than it is to just learn Haskel. Because the amout or weeds you have to navigate through to get to the concepts in Haskell are so high that you can first learn the concepts and approach in a tiny language like Elm and it will more than save the amount of time it would take to understand those approaches from trying to learn Haskell. It seems unbelievable but ai found it to be very try. You can learn two languages faster than just one because of how muddy Haskell is.
Now that said FP is valuable and in my opinion a cleaner design and why in general our industry keeps shifting that way. Monoids, Functors, Applicative are nice design patterns. Pushing side effects to the edge of your code (which is enforced by types) is a great practice. Monads are way overhyped, thinking in types is way undervalued. But you can get all of these concepts without learning Haskell.
So that's the end of my rant as I've grown tired of watching people dismiss FP because they confuse the great concepts of FP with the horrible warts that come with Haskell.
Haskell is a great language, and I'm glad I learned it (and am in no way an expert at it)- but it is the single worst language for an introduction to FP concepts. If you're already deep in FP it's and awesome addition to your toolbox of concepts and for that specific purpose I highly recommend it.
And finally, LYAH is a terrible resource.
Could you elaborate? I know LYAH doesn't teach enough to write real programs, and does not introduce necessary concepts such as monad transformers, but why is it so terrible as an introduction to Haskell and FP? (In my mind, incomplete/flawed != terrible... Terrible means "avoid at all costs").
As for your overall point, I remember articles posted here on HN about someone teaching Haskell to children (no prior exposure to any other prog lang) with great success.
Bunch of places where the tone masked or downplayed real issues in ways that made other text suspect. As a concrete example, `head [] -> Exception` with something like "of course it errors if you take the first part of something that's not there" and `take 1 [] -> []` with "obviously taking one thing from an empty list gets you an empty list" -- uh, no. Maybe it's a historical wart, maybe there are good technical reasons, but different behavior in these cases is definitely not obvious!
That said the language is small enough you can go through the tutorial in a weekend. You'll know pretty quickly if you feel like you're picking it up or feels too foreign.
My gut feel is general programming experience is enough, but don't hold me to that one.
In imperative languages, the program will have a list of entities, and there will be an update() function for each entity that updates its state (position, etc) inline, i.e. new values are overwriten onto old values in memory, invoked at each simulation step.
In Haskell, how is that handled? do I have to recreate the list of entities with their changes at every simulation step? does Haskell have a special construct that allows for values to be overwritten, just like in imperative languages?
Please don't respond with 'use the IO monad' or 'better use another language because Haskell is not up for the task'. I want an actual answer. I've asked this question in the past in this and some other forums and never got a straight answer.
If you reply with 'use the IO monad' or something similar, can you please say if whatever you propose allows for in place update of values? It's important to know, for performance reasons. I wouldn't want to start simulations in a language that requires me to reconstruct every object at every simulation step.
I am asking for this because the answer to 'why Haskell' has always been for me 'why not Haskell: because I write simulations and performance is of concern to me'.
World simulation(Stream<Event> events, World world) =>
events.IsComplete
? world
: simulation(applyEventToWorld(events.Head, world), events.Tail);
World applyEventToWorld(Event event, World world) =>
// .. create a new World using the immutable inputs
That takes the first event that arrives, transforms the World, then recursively calls itself with the remaining events and the transformed World. This is the most pure way of doing what you ask. Recursion is the best way to 'mutate', without using mutable structures.However, there are real mutation constructs, like IORef [1] It will do actual in-place (atomic) mutation if you really want in-place updates. It requires the IO monad.
[1] https://hackage.haskell.org/package/base-4.20.0.1/docs/Data-...
Monads are more-or-less syntax sugar. They give you a structure that allows these optimizations more easily, and also make the code more readable sometimes.
But in your example, update returns a new copy of the state, and you map it over a list for each step. The compiler tries to optimize that into in-place mutation.
IMO, having to rely so much on optimization is one of the weak points of the language.
ST and IO are "libraries" though, in the sense that they not special parts of the language, but appear like any other types.
When I worked on a relatively simple simulation in Haskell, that's exactly what I did: the individual entities were immutable, but the state of the system was stored in a mutable vector and updated in place. The actual "loop" of the simulation was a stream[2] of events, which is what managed the actual IO effect.
My favorite aspect of designing the system in Haskell was that I could separate out the core logic of the simulation which could mutate the state on each event from observers which could only read the state on events. This separation between logic and pure metrics made the code much easier to maintain, especially since most of the business needs and complexity ended up being in the metrics rather than the core simulation dynamics. (Not to say that this would always be the case, that's just what happened for this specific supply chain domain.)
Looking back, if I were going to write a more complex performance-sensitive simulation, I'd probably end up with state stored in a bunch of different mutable arrays, which sounds a lot like an ECS. Doing that with base Haskell would be really awkward, but luckily Haskell is expressive enough that you can build a legitimately nice interface on top of the low-level mutable code. I haven't used it but I imagine that's exactly what apces[3] does and that's where I'd start if I were writing a similar sort of simulation today, but, who knows, sometimes it's straight-up faster to write your own abstractions instead...
[1]: https://hackage.haskell.org/package/vector-0.13.1.0/docs/Dat...
Yes and no.
No, the language doesn't have a special construct. Yes, there are all kinds of mutable values for different usage patterns and restrictions.
Most likely you end up with mutable containers with some space reserved for entity state.
You can start with putting `IORef EntityState` as a field and let the `update` write there. Or multiple fields for state sub-parts that mutate at different rates. The next step is putting all entity state into big blobs of data and let entities keep an index to their stuff inside that big blob. If your entities are a mishmash of data, then there's `apecs`, ECS library that will do it in AoS way. It even can do concurrent updates in STM if you need that.
Going further, there's `massiv` library with integrated task supervisor and `repa`/`accelerate` that can produce even faster kernels. Finally, you can have your happy Haskell glue code and offload all the difficult work to GPU with `vulkan` compute.
TLAs aren't my forte. It's SoA of course.
For me, I stopped trying to learn Haskell because I couldn't quite make the jump from writing trivial (but neat) little self-contained programs to writing larger, more involved, programs. You seem to need to buy into a contorted way of mentally modelling the problem domain that doesn't quite pay off in the ways advertised to you by Haskell's proponents (as arguments against contrary approaches tend to be hyperbolic). I'm all for persistent data structures, avoiding global state, monadic style, etc. but I find that OCaml is a simpler, pragmatic, vehicle for these ideas without being forced to bend over backwards at every hurdle for limited benefit.
The same way games do it. The whole world, one frame at a time. If you are simulating objects affected by gravity, you do not recalculate the position of each item in-place before moving onto the next item. You figure out all the new accelerations, velocities and positions, and then apply them all.
import Data.Vector.Unboxed.Mutable
import Data.Foldable (for_)
import Prelude hiding (foldr, read, replicate)
-- ghci> main
-- [0,0,0,0,0,0,0,0,0,0]
-- [0,5,10,15,20,25,30,35,40,45]
main = do
v <- replicate 10 0
printVector v
for_ [1 .. 5] $ \_ -> do
for_ [0 .. 9] $ \i -> do
v_i <- read v i
write v i (v_i + i)
printVector v
printVector :: (Show a, Unbox a) => MVector RealWorld a -> IO ()
printVector v = do
list <- foldr (:) [] v
print list
It does roughly the same as this Python: # python /tmp/test28.py
# [0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
# [0, 5, 10, 15, 20, 25, 30, 35, 40, 45]
def main():
v = [0] * 10
print(v)
for _ in range(5):
for i in range(10):
v_i = v[i]
v[i] = v_i + i
print(v)
if __name__ == '__main__': main()> In Haskell, how is that handled? do I have to recreate the list of entities with their changes at every simulation step? does Haskell have a special construct that allows for values to be overwritten, just like in imperative languages?
You don't _have to_ recreate the list each time, but that's probably where I'd suggest starting. GHC is optimized for these kinds of patterns, and in many cases it'll compile your code to something that does in-place updates for you, while letting you write pure functions that return a new list. Even when it can't, the runtime is designed for these kinds of small allocations and updates, and the performance is much better than what you'd get with that kind of code in another language.
If you decided that you really did need in-place updates, then there are a few options. Instead of storing a vector of values (if you are thinking about performance you probably want vectors instead of lists), you can store a vector of references that can be updated. IO is one way to do that (with IORefs) but you can also get "internal mutability" using STRefs. ST is great because it lets you write a function that uses mutable memory but still looks like a pure function to the callers because it guarantees that the impure stuff is only visible inside of the pure function. If you need concurrency, you might use STM and store them as MVars. Ultimately all of these options are different variations on "Store a list of pointers, rather than a list of values".
There are various other optimizations you could do too. For example, you can use unboxed mutable vectors to avoid having to do a bunch of pointer chasing. You can use GHC primitives to eek out even better performance. In the best case scenario I've seen programs like this written in Haskell be competitive with Java (after the warmup period), and you can keep the memory utilization pretty low. You probably won't get something that's competitive with C unless you are writing extremely optimized code, and at that point most of the time I'd suggest just writing the critical bits in C and using the FFI to link that into your program.
But let's put that aside. You can instead use the ST monad (not to be confused with the State monad) and get the same performance benefit of in-place update of values.
Want to just have fun? Sure.
Also from the apecs README:
> Fast - Performance is competitive with Rust ECS libraries (see benchmark results below)
Sounds like that "woefully slow" judgment of yours wasn't based in any real experience but rather just your opinion?
"Haskell Researchers Announce Discovery of Industry Programmer Who Gives a Shit"
http://steve-yegge.blogspot.com/2010/12/haskell-researchers-...
It's only when proving theorems that a mathematical proof of totality matters, and there are specialized languages for that.
For most people, we test in order to make a scientific claim, that we tried running it, and it worked for the inputs we tried, and completed in a reasonable amount of time.
This is true of property testing and even model-checking; in simple cases, sometimes an exhaustive test can be done, but they don't actually prove statements outside the bounds used when testing.
Of course, just like any other system that allows correctness proofs, it wouldn't be nearly useful enough to justify the effort for all but a negligible number of applications. That's at least until the levels of effort required are significantly reduced.
So there's still going to be a theoretical math versus science and engineering divide.
Another perspective is that we have a useful division of concerns. Static checking is useful to find some kinds of errors. API's help to ensure that certain things don't change in a new version, so that performance improvements are less likely to break callers.
Depending on the domain, leaving some things like performance and size limits deliberately unspecified in API's seems like more of a feature than a bug? Stricter interfaces aren't always an improvement.
In rare cases there might be exceptions. Hard real time applications and constant time cryptography come to mind.
Regardless, I didn't mean for such proofs to be part of an API or any kind of interface. It's just a guarantee you would get about your program. E.g., "it never times out", or "the worst case data throughout is X" (in whatever hardware model the proof assumes, which in theory could be made very close to the actual hardware).
This is true. You can write a function which is the natural numbers, and I wouldn't want the type system to preclude it.
nats :: [Integer]
nats = [1..]
However, I would love to be able to opt in to declaring functions total on a case-by-case basis, like Idris can.> This fine, though, because for ordinary programming, a proof of totality isn't a useful guarantee. You care how long programs actually take to run, not whether they would theoretically finish eventually.
This wouldn't be the point of the guarantee for me. Haskell functions already give you assurances that you've not made any mistakes:
* You haven't accidentally introduced null, or missed any null checks. * You haven't written code which will do a different thing tomorrow than it did today. * Your function won't race, nor will it cause other functions to race.
If you have a function Foo->Bar, and you give it a Foo, you've all-but-proved that you'll get a valid Bar back. How can you screw that up? By diverging. I'm not trying to put a bound on a long-running function, I just want the type system to make sure the function isn't sucking on its own tailpipe.
Sure, a determined Fortran programmer can write Fortran in any language, but if they have trouble doing so, maybe the issue isn't the language.
I think exactly that all the time. It’s ridiculous.
> That's a problem no haskell user has, honestly.
I had this problem all the time when trying to write games in Haskell. Not every subject matter decomposes into semirings. Just like not everything decomposes nicely into objects. People tried to fix this with FRP or lenses. Both are worse than imperative programming for games imo.
In a sense, that's true: people who do have this trouble constantly (e.g. me) very quickly cease being Haskell users. But that's hardly an argument for TFA's claim that "Haskell is probably the best choice for most programmers, especially if one cares about being able to productively write robust software, and even more so if one wants to have fun while doing it"; if anything, it's a counter-argument.
"Execution in the Kingdom of Nouns" comes to mind.
https://steve-yegge.blogspot.com/2006/03/execution-in-kingdo...
You missed "lazy evaluation by default" in that list ;) Those properties are kind of the definition of Haskell, so without all of them you'd have another language.
Like the sibling commenter mentions, this seems more of a "I'm unfamiliar with this" thing rather than a problem with Haskell...
I think this comes from early and continuous exposure to some forms of programming, rather than inherent to pure functional programming.
I personally find it much easier to use recursion and other functional techniques because it composes better. This probable comes from my exposure to Haskell much earlier than most.
> I believe the vast majority of programmers.
Yups. We get educated with imperative langs. The majority of us do.
Mutations becomes first class values like numbers or arays, and hence can be primitives for more complex mutations, whose types can be inferred from the types of primitive mutations.
This means that the we have compile time guarantees that certain piece of code wont change anything in certain part of the state - This function wont change anything in that part of the data.
It is no joke, though not strictly true, that Haskell has been called the world's best imperative language.
This is what I was talking about in the section "Unlearning and relearning". While there are _some_ domains (like embedded systems) for which Haskell is a poor fit, a lot of the difficulties people have with it (and with FP in general) is that they have been heavily educated to think in a particular way about computation. That's an accident of history, rather than any fundamental issue with the programming paradigm.
Any paradigm shift requires re-learning I think. I don't actually think that's particularly hard, nor do I think it means the paradigm isn't a good one, it's just an inevitable consequence of a paradigm shift. Some shifts are easier than others, if the paradigms are closer together, but functional and imperative programming are quite distant in my view.
Nevertheless, I've seen some people find this easy, others find it hard. YMMV I guess.
Eh. Elm has achieved quite a bit of success just by having good tooling and a good ecosystem. Says something about people's willingness to learn pure functional programming, if the situation is right.
> I find some code easier to express with procedural/mutable loops than recursion
This is usually a familiarity problem, IMO.
I often say: people think mastering Haskell is about mastering monads. But it's actually about mastering folds.
with(require("ramda")) fn = pipe(…)
And yep, you end up with a lot of folds (well, reduces) where that ellipsis is. Or related functions that are really folds.For those wondering `with` in JS is a bit like `extract` in PHP except it creates a new context right after itself rather than modify the context it finds itself in. It's super deprecated because it's inscrutable and silly except in this case and/or when you need to solve Codewars katas in a limited number of characters.
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Refe...
https://www.php.net/manual/en/function.extract.php
EDIT ramda is a nice utility library in JS that supports partial application of arguments in arbitrary order https://ramdajs.com/docs/#subtract
Do you mean like this example of calculating the 5th triangular number with a procedural loop and mutable state? Haskell supports them just fine!
https://hackage.haskell.org/package/bluefin-0.0.7.0/docs/Blu...
The vast majority of business logic can be modelled with a handful of simple types and pattern matching. Very few design patterns are needed. And if you keep to the simple parts you can even teach the syntax (just the types) to non-technical contributors in an afternoon. Then they can read it and help verify that you implemented the business process correctly.
It's also just nice for how my brain works. I like being able to substitute terms and get an equivalent program. Or that I can remember a handful of transformation rules that often get me from a first cut of a program to an efficient, fast one.
And it's just fun.
Lack of typeclasses is a good example. Interface inheritance isn't my favorite, but after years working as the technical lead on a Scala project I've been forced to concede that haranguing people who just want to do their job and go home to their family about how to use them properly isn't a good use of anyone's time. Everyone comes out of school already knowing how to use interfaces and parametric polymorphism, and that is fine.
My opinion: If F# was on any other platform than .NET it would be a widely adopted language - the problems are cultural not technical/capability based. Not because .NET as a platform is bad technically (its gotten quite good and cross platform), but the culture in that space (typically enterprise dev shops) isn't really one of trying new things for marginal benefit - There's safety in the C# herd. There's also seems to be a reluctance for certain regions/areas (e.g. SV) to adopt .NET in general - culturally they are probably the regions more likely to adopt more niche languages and try new tech as well.
The bigger problem is that F# is even more affected by a problem that .NET suffers from in general, through no fault of its own: there is a degree of bias in the industry against .NET outside of its ecosystem (and even within it, some just skill to appreciate the tools they have) so perfectly capable and performant applications, the teams that own them are sometimes pushed to migrate to Go/TypeScript/even Java - all of which constitute a significant and equally frustrating downgrade. The most recent big example of that is Walmart forcing all ex-Jet.com teams to rewrite their F# codebase in Java, where the latter just faced with lack of understanding from Walmart's architects what their stack can even do or what it is.
Another factor is F# sometimes pushes just ever so slightly over the budget for "unconventional" - it's different enough from C-family of languages that it takes some getting used to, and has a learning curve. This combined with the fact that C# itself is very flexible and has been leaning into its functional side for a long time put F# in an awkward spot - it's not the kind of upgrade to C# that Scala or even Kotlin are to Java.
I think what will help F# is the growth of .NET adoption in the industry itself - it's just hands down better offering than most of the competition by sheer existence of ASP.NET Core and EF Core alone in the business domain, and is also a straightforward and fairly open core kind of an ecosystem - companies that use F# would likely find it much easier to contribute to F# itself or .NET runtime than companies that use Scala or Elixir or what have you. In a way, it really is just a marketing issue.
To clarify I didn't say the move was to C# specifically. What I meant was another tech stack often in a different problem space (e.g. frontend with Typescript, scripting with Python, etc) the staff just say F# does it for what we need as well. I agree with your point however - if you are in the F# space already and your staff are already familiar with it there's little reason to move to C# since they have the same ecosystem anyway expect maybe for some edge cases and generated code projects.
Turns out `IO` is the most essential and useful bit of a Haskell program. That part can be left to the programmers. Haskell has a lot of facilities for making that nicer to work with as well.
I find that when I tell folks I work in Haskell full-time you can see their opinion of you change on their face. I'm not some kind of PhD genius programmer. I'm pretty middle-of-the-road to be honest.
It's just nice to have a language that makes the work of writing BLOBS straight-forward.
That's not necessary for business logic, though. This would presumably be embedded in infrastructure that handled i/o separately.
Haskell, to some extent, can help you structure your program in this way where the business logic is just simple, plain, old functional code. You can write the data marshalling, logging, and networking layers separately. There are a few ways to tackle that in Haskell in varying levels of complexity as you would expect coming from other general-purpose programming languages.
If we were to rank the most important factors in choosing how to build our product, I think we may be able to agree that they're likely:
- The talent pool and availability of the language
- The ecosystem of libraries and ancillary tools like monitoring/debugging/observability
- The speed-of-development vs cost-of-maintenance tradeoff of the language
I will give Haskell that it can be rapidly written by those proficient, and tends to have less bugs if it compiles than many languages.
But for "what language is easy to employ and has an expansive ecosystem + tooling", I feel like you have to hand it to Java, .NET, Python, TypeScript, Go, etc...
My experience is Haskell is one of those ecosystems that has a greater talent pool than there are available positions. I feel like cost of maintenance is pretty nice because you have less bugs. You may have to roll up your sleeves and get your hands dirty to update open source libraries or make stuff that is missing, but code reliability seems to be worth it.
> The talent pool and availability of the language
There are certainly more Javascript or Python developers out there than Haskell developers, but I think it's wrong to imply that Haskell is a hard language to hire for. There are more people out there who want to work with Haskell than there are Haskell jobs, and picking Haskell can be a really great way to recruit high quality talent. It's also quite possible to train developers on Haskell. A lot of companies hire people who don't have experience with their particular language. The learning curve for Haskell may be a bit steeper, but it's certainly tractable if you are hiring people who are eager to learn.
> The ecosystem of libraries and ancillary tools like monitoring/debugging/observability
Other languages have _more_ of these, but it's not like Haskell is missing basic ecosystem things. I actually find that Haskell is pretty nice with this stuff overall. It's not quite as automatic as what you might get with running something in the JVM, but it's not that big of a lift, and for a lot of teams the marginal extra effort here is more than worth it because of the other benefits you get from Haskell.
> The speed-of-development vs cost-of-maintenance tradeoff of the language
Haskell is really excellent here in my experience. You can write unmaintainable code in any language, but Haskell gives you a lot of choice in how you build your application, and it makes refactoring a lot nicer than in any other language I've used. You don't get some of the nice IDE features to rename things or move code around automatically, but working in a large Haskell codebase you really do start to see ways that the language makes structural and architectural refactoring a lot easier.
> But for "what language is easy to employ and has an expansive ecosystem + tooling", I feel like you have to hand it to Java, .NET, Python, TypeScript, Go, etc...
Those are all perfectly good choices. I think what people tend to overlook is that Haskell is _also_ a perfectly good choice. Everything has tradeoffs, but Haskell isn't some terrible esoteric choice that forces you to leave everything practical on the table. It really is useful day to day as a general purpose language.
Which supports the argument that "it's good for startups" which generally want to exploit young cheap talent. But as soon as your business starts needing industrial wizards you're out of luck.
The reasons/requirements to choose haskell are:
1. You already have a confident haskeller or two on your team, ideally senior
2. Your tech needs are vanilla and back-end heavy -- writing UIs in haskell is a nonstarter, and heaven help you if you're not doing vanilla web apis + postgres etc
3. You want primarily junior- to mid- talent (great for keeping burn low)
There are niche examples like compiler tech or provers where haskell is in fact dominant. It's also fantastic for threading and parallelism. Otherwise the claim that it's a good choice requires all three of the above to be true.
> 1. You already have a confident haskeller or two on your team, ideally senior
There are really only three reasons any particular tech stack is ever selected for any project.
1. It's the standard at the company and you don't have a choice 2. You happen to be working in an extremely specialized field where one language is dominant (e.g. Haskell for compilers, python for AI) 3. Someone picks a language that they like and are confident in
I'd suggest that across the entire industry, (1) dominates by a large margin. It doesn't apply as much to startups though, because by definition startups aren't going to have a lot of existing code and won't have developed restrictive standards.
So yes, the biggest reason to choose Haskell is that you have a confident senior haskeller on the team.
> 2. Your tech needs are vanilla and back-end heavy -- writing UIs in haskell is a nonstarter, and heaven help you if you're not doing vanilla web apis + postgres etc
I agree with you about UIs. It's not a good experience and I wouldn't suggest Haskell for that. There are other specific areas where I wouldn't suggest it outside of a hobbyist project, like games or mobile development. There are still a lot of programs that need to be written and benefit from Haskell's strengths though, and sure, a lot of them are CRUDy web API things, because frankly that's most of all software that gets written.
> 3. You want primarily junior- to mid- talent (great for keeping burn low)
Haskell is perfectly viable if you want to hire a team of junior and mid level developers (so long as you have a more experienced person on the team to help them learn), but not exclusively useful in that situation. There are experienced Haskell developers out there, and there are a lot of highly skilled and experienced developers who are interested in- or at least willing to learn- haskell.
In general, I'd say that Haskell is a great choice for _most_ types of development, and can work for teams with a wide variety of experience levels. It's a real asset for hiring because there are more people who would like to work with Haskell than there are Haskell jobs out there, and in general I think Haskell still benefits from the python paradox (https://paulgraham.com/pypar.html) where people who know or are interested in it tend to be above average.
Looking beyond permanent hires-
In my python-based startup, we routinely offload work to contractors on an as-needed basis. Having a flexible staffing level saves us real dollars, and staffing flexibility delivers products to customers faster, which generates revenue dollars faster. From a financial perspective, our staffing flexibility delivers real, measurable economic gains.
Non-mainstream tools like Haskell break that model, effectively incurring an economic cost. Sure there are Haskell contractors, but their numbers are minuscule compared to python contractors. Given the enormous pool of python contractors, we cherry pick the very best who understand our business, our tech stack, and our work culture.
That probably wouldn’t scale up if you were hiring many tens or hundred of contractors. My experience with that degree of outsourcing hasn’t been positive irrespective of language though.
If that statement is true (I couldn't say), I might argue it's incomplete, because the elephant in the room is JS/TS. Haskell-in-the-browser is in horrible shape, which means that options for tight integration with UI code are severely limited. If there really is endless work for UI-less, JS-foreign web tech then that's great for Haskell ... maybe.
Because ... look what you've done to have an incrementally better experience writing CRUD apps:
1. You've got existential bus risk in your senior haskeller, and you live or die by their quality and experience, which since there's strictly less of them, is probably off the beaten path.
2. You've chosen a stack that has a massively smaller hiring pool. The benefit of "oh they'll take less pay because of the shiny" is maybe great at first, but if you have any adoption and attendant perf problems, you'll need those senior devs. Even if they are willing to learn, they can't use the tools they know best so at a minimum you're losing momentum.
3. This is more typical of smaller langs but Haskell has it's own spin on it: you're risking not having library support for a million things when you need it. Haskell has the false comfort of doing C FFI well, but that just means that when your core integration stops being maintained, now your haskellers better be C-savvy as well.
> dire and antagonistic view of things
If that's true, it's because the promise is so much greater than vanilla apps. I loooove runtime languages and Haskell should be the best of them: better-than-average GC, incredible concurrency support, great FFI, flawless refactoring, perfect DSLs, solid apps. It makes it worth forcing programmers to learn an unusual language if you can really conquer anything, and for a bright moment in the 2010s it seemed like you could.
Now, Haskell has lost that momentum and mindshare to Rust. This is literally true with some core Haskell library maintainers straight-up defecting. There are signs in the Rust community now of slowing momentum, and for similar reasons. There might be a boom-bust cycle for advanced languages with shaky industry support, I don't know.
Haskell leadership needs to change. There needs to be a laser focus on tooling, on browser compilation toolchain, on industry adoption. Avoiding success at all costs was a clever idea for a minute (ok for 3 decades) but post 2010s, there seems to be an active desire to shut off industrial adoption to keep it safe for the tinkerers. (End rant)
IIRC this was testing with students, which would be both a strength and a weakness of the experimental design.
Yeah I get a bunch of basic apps can be modeled easily, you get unparalleled static safety but programmers will spend an inordinate amount of time figuring out mind bending algebraic patterns.
I think something like ocaml or f sharp are more down to earth.
I tend to stay away from the advanced parts of Haskell when writing BLOBS.
The advanced stuff is there when you need to write libraries that need generic code that works with types you haven't defined yourself. You learn it as you go when you need to.
But when I'm writing BLOBS I mostly stick to using libraries and that's pretty straight-forward.
Math is not academic gibberish though, and actually often the hard part to make a great conceptual thing work finely in the wild is to give it a more approachable form than the ridiculously arcan soup of greek letters and made up symbols sprinkled with generous quantity of opaque acronyms and words shrinked into trigrams.
Not to say CS/IT industry always shines at making all these better, and it can actually be even more "enthusiast" with acronym jargon in my experience.
No one communicate perfectly, sure starting with me. :D
Of course there are a few people out there who do deliberately select an obscure symbol just to put some esoteric vibe on the topic. But I believe that’s definitely not the norm. Lambda as selected by church is almost there, should I believe https://math.stackexchange.com/a/2095748/85628 And in any case I by far prefer "anonymous function" as a term and prefer languages that doesn’t use `lambda` as keyword to implement them (looking at you Python!). Of course, just one symbol is not a big deal, but when each academic out there feels like they need to also introduce their special symbols to feel as special as the ones they admire, we end up with a mess of irrelevantly large number of symbols that don’t really bring a significant addition in term of expressiveness but do contribute to make things harder to grasp.
https://en.wikipedia.org/wiki/Mathematical_operators_and_sym... for what I have in mind about "made up symbols".
And yet, we still came to a consensus that these symbols have these meanings, completely arbitrary as they may be. I don't think + or ∪ or ≥ or λ is any more obscure than I or , or ?. I would much rather write "A⇒B" than "A implies B" 100 times on a page. Besides being shorter, using a symbol reinforces that we're referring to a strict mathematical interpretation and not the vagaries of ambiguous English.
TL;DR if you don't hassle your English teacher on why we use ? to ask a question, and it doesn't stop you from learning English, don't pester your math teacher to explain the etymology of λ. The history of those languages is a different subject from the practice of them.
We simply differ extremely on this, and it’s fine. Let me explain, though.
On the symbolic level, "my mind" will be eager to know where the symbols come from, what are the proposed histories of their formation through time. It’s a great way to gain completely unrelated knowledge. That is, unrelated in a discipline domain driven manner. Sometime it does come with some additional insights on the domain itself, or at least goes back to some references to the domain itself. For example the equal symbol (=) was selected because "nothing is more equal than two parallels lines".
On the representational level, the term "I" can be a very misleading one. Striving to supersede it completely in all utterances is an interesting challenge, one that can open original perspective compared to the most mundane ones. Seeking to put apart the "current conscious attention", which can detach itself from all the illusions that that often entangled in the term "I", is a rich and profound experience. For casual chitchat, it’s still nice to use "I", which can be a quick efficient way to make others aware that there are some emotions and personal engagement put at play in the exchange.
> I would much rather write "A⇒B" than "A implies B" 100 times on a page.
Wouldn’t it be even much better to do none of this? In Ruby `100.times do puts 'cause implies consequence' end` is valid code that will do that without any painful effort to make the aimed page.
If it’s too painful to produce the specimen that is aimed, the way it’s produced is to be reconsidered more importantly than the usability of the aimed specimen.
>using a symbol reinforces that we're referring to a strict mathematical interpretation and not the vagaries of ambiguous English
Using terse scriptural symbols per se doesn’t prevent to go into esoteric non-sense. And anything that can be expressed in graphical symbols can just as well and as clearly expressed in any spontaneous language if given the same careful attention.
It’s not like having the option to use terse scriptural symbols is always necessarily bad. It’s fine to have options and be aware of tradeoffs we are using one or the other. Going back to the previous Ruby snippet, `puts "cause implies consequence\n" * 100` works just as well, and might be more convenient to type in an interactive console, while the previous version might be a wiser selection to integrate in a large code base which strive to keep maintainability at its highest level.
Presumably it won’t be the only content on the page, the statements are going to be inside various other statements. Also I’m not sure how you’d execute Ruby code on a Blackboard.
> Using terse scriptural symbols per se doesn’t prevent to go into esoteric non-sense.
Who said you’d have to “go into esoteric non-sense”, whatever that means?
> And anything that can be expressed in graphical symbols can just as well and as clearly expressed in any spontaneous language if given the same careful attention.
Okay, but why woutd you want to use a word if a simple symbol does the trick? You have to define what it means either way and symbols have the advantage that you don’t need to translate them between languages.
You can choose examples like "sun rises in the east" or "things fall down to the ground" or even things like fire to see how we were long able to do useful things before having a deep fundamental understanding of what they were. And while trying to understand them, we had a whole bunch of wrong ideas along the way all while the practitioners got on with their lives.
We also see this in physiological spaces like sports and health. Just the other day I watched a video about how Chopin had some deep intuitive understandings about the anatomy of the hand that was contrarian to his peers but proved to be far ahead of his time.
With programming we have a lot of different theory to dip into: information theory, computer science, systems theory, category theory etc. some of these ideas have been slowly chugging along in academia but the early days of computing were built with dirty for loops, goto and hand crafted assembly or even less. We still managed to put people on the moon with that nevertheless and automated a bunch of commerce and put stuff on the internet - long before most devs had heard of an ML type system
But the ideas from Scheme gave us Ruby Python and JS which unlocked a whole new wave of programming and arguably made it accessible to more people increasing humanity's "productivity" in some sense. I'm sure people who believe in static typing would disagree but YouTube, Instagram and Github were built in these dynamic languages...
Would you be generous enough to share a link to this video, please. :)
The anecdote about Chopin be Schumann was pertinent especially if you were already familiar with the accounts that Schumann injured his hands
This may also be of interest
And I have found that's one of the tricky bits with Haskell, together with the language being in very active development, wich makes upgrading your compiler a thing.
https://h2.jaguarpaw.co.uk/posts/ghc-8.10-9.6-experience-rep...
(DeepSubsumption wouldn't have been a problem if we'd specified Haskell2010, as we should have.)
I’ve come across plenty of Haskell code that just expects a happy path all of the time and can’t deal with any other situation. That’s great for POC work but horrible in production.
For example, if the function call returns some data collection, returning an empty collection can be a safe way to allow the program to continue in the case of something unexpected. I don't need to ABORT. I can let the program unwind naturally as all the code that would work on that collection would realise there's nothing to do.
Debugging that can be a pain, but traces and logging tend to fix that.
> Haskell solves the problem of the representation of computations which may fail very differently: explicitly through the type system.
In my experience this is very hit and miss. Some libraries use exceptions for lots of error states that in Go would be a returned error value. I'm therefore left to decipher the docs (which are often incomplete) to understand which exceptions I can except and why and when.
Last library I remember is https://hackage.haskell.org/package/modern-uri
From their docs: > If the argument of mkURI is not a valid URI, then an exception will be thrown. The exception will contain full context and the actual parse error.
The pit of success would be if every function that can fail because of something reasonable (such as a URI parser for user supplied input) makes it a compile time message (warning, error, whatever you prefer) if I fail to consider the error case. But there's nothing that warns me if I fail to catch an exception, so in the end, in spite of all of Haskell's fancy type machinery, in this case, I'm worse off than in Golang.
Yes. This is a very very bad aspect of the design of many Haskell libraries. They just throw away the whole point of doing Haskell.
This just seems like bad libraries, I'd agree that this is bad and sort of defeats the point. I haven't actually encountered this with any libraries I've used, and we tend to avoid MonadThrow / Catch except in particular circumstances.
> in this case, I'm worse off than in Golang.
Having (unfortunately) had to write some Golang, I don't think this is true -- I've encountered plenty of code in Golang in which it seems idiomatic to return things like empty strings and empty objects instead of error values which, I think, it's still possible to mishandle.
Perhaps this can be summarised as: you can still write bad Haskell, but I don't think it's particularly idiomatic looking at the libraries I've spent most of my time using, and the machinery you are provided allows you to do much, much better.
The issue isn’t what the language is capable of in the best case. The issue is what the community has produced for future members of the community to consume. See my other comment about pcap and gnuplot for two specific examples.
There's also the huge performance divergence between IO exceptions (fast) vs a mtl stack built around Either which will have huge problems successfully inlining and thus be slowwww.
Indeed this is a great example of how Haskell can have serious performance issues in areas that would never occasion a second look in other mature GP langs. Who ever heard of well-modelled error handling having performance problems? Only In HaskellTM
But I don't want to spend any more time near purely functional programmers in my professional life, so I will spend some time to unfairly nitpick the article.
> a lot of the new features ... are either inspired by, or at least more robustly implemented in, Haskell.
This is like some programmers, who don't know much about the history of programming languages, saying Java is stealing features from Kotlin (a language I enjoy). In both cases the inspiration usually comes from ML (a language family which includes OCaml - a language I deeply admire).
Type classes (first implemented in Haskell) are neat though. I heard that Java is considering adopting them.
> Let’s imagine we represented these as plain old strings ... `Theatre String String -- venue name and event name respectively` `checkForSeats :: String -> String -> IO [Seat]`
If your language has named arguments, and sum type cases are records where you have to name your fields, using plain strings is just fine, and probably more ergonomic than wrapping the strings in another type.
> `Either AddressParseError ValidAddress`
Real world procedures usually involves many steps which can fail in many ways. The HM type system does not have subtypes. To stuff all possible failures into the `Left` case of `Either` you have to wrap all possible failure types into a sum type, possibly with multiple layers of nesting (huge PITA).
> concurrent Haskell
For all the hassle with the IO monad, you cannot offload your understanding of memory model to the compiler when you use an `IORef`.
Maybe most your problems are embarrassingly parallel and STM is fast enough for the rest. Maybe.
> exactly encode the effects we want a function to be permitted to perform
https://degoes.net/articles/no-effect-tracking "Effect Tracking Is Commercially Worthless"
See "Tagless-Final Effect-Tracked Java™" for a chuckle.
> small sample program
The `Money` type is monomorphic, and `Functor`s are higher-kinded. You get the error message `Expected kind ‘* -> *’, but ‘Money’ has kind ‘*’`
Is this not inherent complexity? Is there some language or approach you believe makes this easier? Does it provide the same or similar correctness?
Here is some more elaboration:
I said "untagged union" to distinguish it from "tagged union", which is another name for sum types. There are several comments that incorrectly called sum types "union types".
But it isn't tagging that's the problem, it's the limitation that a value can only be of one type in vanilla HM. And indeed both OCaml and Haskell go beyond vanilla HM: https://ocaml.org/manual/5.2/objectexamples.html https://hackage.haskell.org/package/vary-0.1.0.3
BTW `thumbnailService :: String -> VEither [ServerUnreachable, IncorrectUrl, NotAnImage, TooBigImage] Image` is isomorphic to `Image thumbnailService(String url) throws ServerUnreachable, IncorrectUrl, NotAnImage, TooBigImage`.
And nobody likes checked exceptions in Java. (hyperbole)
https://github.com/augustss/MicroHs
https://www.youtube.com/watch?app=desktop&v=uMurx1a6Zck&t=36...
Haskell manages to do a lot of both. The kinds of problems I ran into in my Haskell error were much, much weirder than the problems I run into in other environments - things that when I explain them to other programmers they often don't even believe me.
On balance, for me, the new problems were worse than the old problems, but your mileage may vary.
A functional language with a simple set of structural types would be the sweet spot for me. Clojure is probably the closest to this.
Then I needed to ingest RSS feeds. So I found a library that handled that for me. Except that library uses another type for URLs. Uh oh. What should I do? I could change my URL type to be a wrapper type around both types, or write code to convert between the types. I chose to convert. Now I'm writing code to shuffle between the types where this RSS parsing module is used.
Then I needed to make HTTP requests. So I pulled in a library to handle HTTP requests. Of course, that library uses another type for URLs (from another library it depends on). Great. Now I have 3 types for a URL.
Then I needed to parse XML... and you know where this story is going.
So now my codebase has many different URL types.
The type-a-holics will say: "This is actually good! Each implementation of the URL type might have slightly different constraints, and the type system makes this all explicit. You should be grateful you spend half of your development time fiddling with types. The fact that `unpack . decodeUtf8` is littered around your codebase isn't code smell, it's the splendor of a type system that's saving you from yourself. You should learn to love the fact that you have to deal with String, Text, and ByteString and 4 URL types to fetch and parse an RSS feed. Otherwise your software would be full of bugs! Silly developer."
One day I finally woke up from this type nonsense. There's integers, rationals, strings, lists, and maps. The end.
I quickly realized the folly of my ways, however, and became what I would call a Haskell zealot. It was my favorite and most-used programming language, and I even wrote it professionally very early in my career.
As my career progressed and I had to use other languages, Haskell quickly lost its shine. It's just not a very practical language, no matter what they like to claim from their academic ivory tower. I wasn't some sort of novice. I could speak fluent lens and was well-versed in arcane type theoretic concepts, wrote entire data pipelines using recursion-schemes and what have you.
The problem with Haskell it's just way too far out on the spectrum of purity, I think. Everything is way too cumbersome because the language is so rigid. Building real software in Haskell, while trying to make use of its advanced features, is sort of like wanting to do basic arithmetic for your budget, but starting out by proving that mathematics actually make sense. Naturally, you don't have to build your software like that, and it seems like a lot of actually practical haskell apps out there aren't written like this.
Nevertheless, Haskell fundamentally changed the way I view programming and I learned many incredibly useful things, which I have taken with me. Try to keep your functions pure and small. Use static types, and lean on the type system to help you prove that you have covered all cases. Stuff like that.
Laziness incurs amortized costs all over the place, and makes reasoning about the application state more difficult, can you help me justify the cost for the extra electricity?
And, btw, last time I wrote a web server in Haskell, I was very much surprised that the standard way of implementing it was to use lenses, which mimic imperative style of assignments to a variable. And logging. Turned out you need logging for anything real, and in Haskell they are side effects implemented via a dirty hack that can be invoked anywhere.
Finally, you can pry my Knuth tomes from my dead cold hands.
On a more serious note, if you are really into immutability and functional style, F# is deemed to be a far better and practical choice. I reserve only praise for Common Lisp and Clojure, although I typically prefer static typing.
THIS - and none of the other things cited in the article - is the main reason why Haskell is getting no traction in the real world.
The syntax is an abomination, and worse, IMO pretty much unnecessary. The ideas of the language are interesting, and could be expressed in a much more familiar syntax which would be a huge win for the language (although the convoluted hoops you have to jump through when you truly and actually need to mutate data would still be a sheer nightmare).
Haskell's syntax is exactly what keeps it niche and academic, except for the happy few who have managed to twist their brain into being able to parse that grotesque syntax.
I was going through a Scala codebase at work that uses `Future`s and `map`ing and `flatMap`ing them. Sometimes the callbacks went 5-6 levels deep. Is there a way to "linearlize" such code?
I come from JS/TS background and have not much experience with pufe functional languages. But I love how TS handles discriminated unions - if we handle a branch and `return` early, that branch is removed from the union for the subsequent scope, and I was wondering if something of that sort can be achieved in Haskell/Scala.
do
a <- somePartialResult
b <- partialFunction1 a
c <- partialFunction2 a b
return c
where we assume signatures like somePartialResult : Either<A, Error>
partialFunction1 : A -> Either<B, Error>
partialFunction2 : A -> B -> Either<C, Error>
this overall computation has a signature Either<C, Error>. The way it works is that the first failing computation (the first Either that's actually Left-y) will short-circuit and become the final result value. Only if all of the partial computations succeed (are Right-y) will the final result by Right(c).In Haskell we don't have an early return syntax like `return` and function scope. Instead, we construct something equivalent using `do` syntax. This can be a little weightier than `return`, but the upside is that you can construct other variants of things like early returns that can be more flexible.
Or, should we resort to a method of `Either` that transforms its `Left` in that case?
Rust offers a similar facility (though specialized to just handle a couple kinds of error handling) using its `?` syntax. This works essentially identically to the do syntax above, but also includes a call to transform whatever error type is provided into the error type of the function return.
Note that in Rust (a) this technique only, today, works at function boundaries and (b) will always be explicitly annotated since all functions require an explicit type. This helps a bit over Haskell's more general approach as it provides some additional data to help type inference along.
That said, if you were interested, it's likely possible to emulate something very similar to Rust's technique in Haskell, too.
But I don't think I've ever seen that. It just doesn't feel as stylish in Haskell. The From/Into traits define a behavior that's much more pervasive than most type classes in Haskell. It works well for Rust, but is I think less compelling to the Haskell community.
result <- do
a <- someEitherValue
b <- anotherEitherValue
return (doStuff a b)
In the above example the do notation will unwrap the values as an and b, but if one of the results is Left, the computation is aborted, returning the Left value.This is one just of the many techniques available to make error checking linear.
https://typelevel.org/cats/datatypes/eithert.html
def divisionProgramAsync(inputA: String, inputB: String): EitherT[Future, String, Double] =
for {
a <- EitherT(parseDoubleAsync(inputA))
b <- EitherT(parseDoubleAsync(inputB))
result <- EitherT(divideAsync(a, b))
} yield result def divisionProgramAsync(inputA: String, inputB: String): IO[String, Double] =
for {
a <- parseDoubleAsync(inputA)
b <- parseDoubleAsync(inputB)
result <- divideAsync(a, b)
} yield result
(the annoying wrapping/unwrapping isn't necessary with ZIO here)You can also write this shorter if you want:
def divisionProgramAsync(inputA: String, inputB: String): IO[String, Double] =
for {
(a, b) <- parseDoubleAsync(inputA) <*> parseDoubleAsync(inputB)
result <- divideAsync(a, b)
} yield resultdoes haskell have any facility for inspecting the "lazy evaluation queue" in a human readable way? I'm doing some binary-level math-as-logic calculations and I'd like to look at the patterns that emerge deep down, what cancels out and can be optimized away: think of it this way, I want to multiply AxB, where in bits thats (a0 a1 a2...) x (b0 b1 b2...) which turns into a bunch of a0 OR b0 , a0 XOR b0, (don't forget the carries :) etc, which after a few operations in an expression would be really hairy looking but completely straightforward.
so, I can write a little program to do that in any language and print out what happens (lisp/scheme would be a good one). If a wrote a little program like that in Haskell, would it afford me any extra "free" options for inspecting what's going on? where if a0 and b3 where unknown, they'd be variables, but if I had actual values for them they could be evaluated away.
maybe i should ask this as its own thread...
Yes, as I like to tell project managers, everything can be implemented.
... But the Google drive sdk is available in python, java, and node.
Tooling, deployment, library ecosystem, and talent pool are just as important when choosing a language or framework as performance, syntax, or guardrails. Unless you have unlimited budget!
I still love the language, BTW.
I actually really like the syntax as it makes it easy to write DSLs which are actually just Haskell functions.
Haskell's syntax, like many FP syntaxes, is inscrutable on first acquaintance. There's a reason hybrid languages like Elixir thrive...
Rust's memory management is a great boon for a close-to-the-metal language, but if all your types are immutable you don't actually need/want to deal with the borrow checker.
Typescripts toolchain and ecosystem are... ok, at best? I'd give a solid pitch for the Rust ecosystem having reproduced the best parts thereof (and there is still room for improvement even so)
The type system means you don't have to worry about folks hiding pointers in arbitrary pointer-sized integers, so you know all the roots ahead of time.
Immutability means you can only ever create references in one direction (i.e from new objects to old objects), and you can't ever create cycles.
This lets you do fun shit like a mark&sweep garbage collector in a single pass (rather than the usual two) - and if you have process isolation guarantees (a la Erlang), you don't necessarily have to suspend execution while it runs. Or maybe a generational collector where the generations are entirely implicit.
You can write extremely haskell-like code with Rust.
Here's the first example in rust:
fn safe_head<T>(list: &[T]) -> Option<&T> {
match list {
[first, ..] => Some(first),
[] => None,
}
}
fn print_the_first_thing(my_list: &[String]) {
match safe_head(my_list) {
Some(something) => println!("{}", something),
None => println!("You don't have any favourite things? How sad."),
}
}
fn main() {
let my_favourite_things = vec!["raindrops on roses".to_string(), "whiskers on kittens".to_string()];
let empty_list: Vec<String> = vec![];
print_the_first_thing(&my_favourite_things);
print_the_first_thing(&empty_list);
}
(of course Rust has a lot of helper functions that avoid all that verbosity -- you can do the whole thing in one expression, if you want) println!(
"{}",
my_favourite_things.get(0).map_or(
"You don't have any favourite things? How sad.".to_string(),
|something| something.to_string()
)
);Basically Rust is much better at producing efficient code with zero abstraction cost (while still doing a decent job at controlling mutation and having an expressive non-dependent type system) and having a large package ecosystem, and Lean, Agda and Idris are much better at being theoretically perfect languages while sacrificing code efficiency, so why use Haskell?
Obviously if such a language is possible to make and gets made, it will be the strict best programming language overall and make all other languages obsolete (just like Rust obsoleted C/C++, etc.)
I haven't taken a lot of measurements and my production code is biased to Go, C++, Python and Java, with most of my Haskell experience being side projects and toys for learning from, and writing a type-unifier for a production project. I can summarize what I learned but I would be interested in seeing better measurements.
First, though, let's be more specific about what you mean by "writing code as fast," which I think should be refined to "time spent writing code" + "time compiling code" + "time spent in language runtime" + "time spent debugging / reading code". Depending on your project, and who if anyone is collaborating, each of these might be more or less important. Sometimes runtime speeds dwarf the needs of development or even debugging time. Sometimes compilation speeds afford the quick feedback loop that contributes to flow in development time.
Within that framing, Haskell can excel at development time with small teams and limited scopes. It affords writing a domain-specific language within the code, including very custom operators, and this carries the risk of overburdening with complexity, and strongly proportional to the number of people on the team. Things can get complex fast and it can contribute some to compilation time if there is a lot of recursive complexity to the type system. But if the source is organized well and doesn't need to be very dynamic, this may not be much of a concern. As an underlying engine for very dynamic data inputs and sufficiently complex numerical analysis or IO management as its primary purpose, it would probably do well.
The packaging system (Hackage) is pretty good, and that benefits the development time considerably. Adding some modules may impede compilation times, for much of the same reason as above, the type inference can become expensive. And undisciplined source management can lead to a lot of type implementations that are near copies of each other. Obviously this also ties into the reading/debugging time, too.
For runtime, though, yes Haskell can be competitive with bare-metal C implementations. I think there have been a few papers written about that going back a decade or so.
There are also some common patterns for less idiomatic but more performant code that you can use as a first pass when you need to optimize things further. Usually that’s sufficient, but when you need to go even further with optimization then it can get hard. Every language can. In Haskell, it usually means starting by dumping core and seeing what the compiler is doing, and getting familiar with the different passes the compiler makes. As a last resort you can also just write in C and use the ffi.
1. the lack of nullable types
I very rarely write these bugs, and when I do I can typically fix them in 5 minutes. This is because I typically do (2) in my projects, which does largely eliminate this error.
2. representations of “failable” computations
Basically any modern language can do this. It might not be 100% as ergonomic as it is in Haskell, but it also isn't a large source of bugs in my experience.
3. pattern matching and completeness checks
Okay, these are nice and ergonomic in Haskell. Other languages get pretty close. Again, not a source of time-consuming bugs.
4. the avoidance of “primitive obsession”
The example he gave for this is innanely contrived, and the bug would likely take a small amount of time to fix, even for a junior. Admittedly, this is a nice convenience feature and I would love having types for different color spaces, or radians vs degrees, but at the end of the day I spend basically 0% of my time on bugs like this, so it's barely worth mentioning.
You know what I'd like a language to help me with? Keeping track of inter-data dependencies so I don't have to litter my code with a million assertions to make sure the sub-type of the sum type I'm working with is correct. Or giving me a way to express structural dependencies between pieces of code when writing multithreaded programs. Like saying "hey, this render pass has to happen after 'DoEntitySimulation' has completed" .. or whatever. I'm not aware of any language that even tries to do that, although I think Bungie wrote something like this in their engine for Destiny2.
And metaprogramming. I ended up writing my own metaprogramming language because none of the ones I tried could even do the basics of what I wanted in an ergonomic way, and be runtime-performant.
For reference, my language of choice is typically C++99. Maybe I'm not the intended target audience.
On the other hand, you mention several features which Haskell provides, usually through libraries that can only be implemented due to the features provided by the base language:
- Refinement types [1] allow to add runtime invariants to existing types in an ergonomic fashion, or you can go even further and use something like LiquidHaskell[2] to enforce properties at compile time.
- For multithreaded programs, the existence of STM[3] allows to to write mutable variables which are safe to use across threads. Very few languages offer something like this.
- For structural dependencies, you can apply the techniques of "ghost of departed proofs"[4]. Personally I don't like to go that route since programming becomes an act of "proving" rather than "doing" but I appreciate the fact that you can encode it if you want/need to.
- Metaprogramming in Haskell is not as ergonomic as in a LISP yet you have the full access to the language through TemplateHaskell[5]. A more constrained form is available as QuasiQuotations that allow you, for example to write a regex[6] string and have it compiled alongside the rest of the code.
There are other features that I personally think are still far ahead from the competition, like lenses[7] to traverse nested data, the async[8] package for ergonomic concurrent programs, effect systems[9] for more granular dependency injection, immutability by default to avoid corrupting state, full type inference, top level functions and values (I can't believe the amount of times I find myself missing them in OOP languages like Java and C#), among others.
---
[1] https://hackage.haskell.org/package/refined [2] https://ucsd-progsys.github.io/liquidhaskell/ [3] https://hackage.haskell.org/package/stm [4] https://hackage.haskell.org/package/gdp [5] https://serokell.io/blog/introduction-to-template-haskell [6] https://hackage.haskell.org/package/regexqq [7] https://hackage.haskell.org/package/lens [8] https://hackage.haskell.org/package/async [9] https://hackage.haskell.org/package/effectful
Thank you, I appreciate that. And that you took the time to write such a complete response.
At the end of the day, Haskell is just way too slow (at runtime) for me to consider using, so debating that these features satisfy my requirements is purely an academic exercise. One which I don't have much interest in doing.
I am glad to hear that you seem to enjoy using Haskell, and I hope it continues to bring you joy :)
@dataclass
class InvalidResultError:
result: str
def do_something() -> int | InvalidResultError:
result = get_result()
return 42 if result == "a result" else InvalidResultError(result)
Using a dataclass like this seems a little overkill, but it does get you something close to `Either`.I also don't understand the argument against nullable types: you could return `int | None` here, which would be the same as treating a `Maybe` (you have to explicitly consider the case where there is no value)
Runtime immutability is brain dead
Haskell doesn’t “solve maintainability” even remotely. Most people leaving Haskell say maintainability is worse
Haskell doesn’t solve parallelism as Haskell devs claim
Haskell is not beautiful
And that’s literally every selling point. Why choose it?
One of the examples in the article:
Python:
def do_something():
result = get_result()
if result != "a result":
raise InvalidResultError(result)
return 42
Haskell: doSomething :: Either InvalidResultError Int
doSomething =
let result = getResult
in if result /= "a result"
then Left (InvalidResultError result)
else Right 42
Personally, I prefer the Python version. In my experience, the benefits of adding meta information like types and possible return values are very small. Because the vast majority of time fixing problems in software development is spent on systematic bugs and issues, not on dealing with type errors.Re. the example, the compiler can infer types, and you can write almost the exact same code in Haskell as in Python:
doSomething = do
let result = getResult
when (result /= "a result")
(throwError $ InvalidResultError result)
return 42
But as it has been noted, you are unlikely to find code like this in Haskell. An element of Either type at the top level, without arguments is either always going to be Left or always going to be Right, so it is a bit pointless. Since this example is so abstract it is hard to see what the idiomatic Haskell would be, because it would depend on the particulars.Actually this is the wrong takeaway, I think it's so that programmers can reason about it.
This isn't about type errors, it's about precisely describing a particular computational expression. In the python example, it's very unclear what `do_something` actually _does_.
You can make "systematic bugs and issues" a type errors, then deal with them as type errors.
If you can confuse between meters and seconds expressed as Double and erroneously add them, wrap them into types Meters and Seconds, autogenerate Num and other classes' implementations and voila, you cannot add Meters and Seconds anymore, you get type error.
I do that even in C++, if I use it for my personal projects. ;)
But where Haskell really shine is in effect control. You cannot open file and write into it during execution of transaction between threads. If you parse text, you also can restrict certain actions. There are many cases where you need control over effects, and Haskell gladly helps there.
Actually Haskell lets you to add more meta information to the code, similar to modern Python or TypeScript. Type information is optional. But you might want to add it, it is helpful most of the times.
In the example, doSomething implicitly depends on getResult, which doesn't show up in the type information, so the type information only tells you how you can use doSomething. To know what is doSomething, you actually have to read the code :\
Which part?
> the point I'm trying to make is that you can precisely describe what `doSomething` consumes and produces (because it's pure)
I think you failed to demonstrate it, and more or less demonstrated the opposite of it: the type signature of doSomething does not show its implicit dependence on getResult.
In Haskell you can do
foo :: Int
foo = 5
bar :: Int
bar = foo + 1
(run it: https://play.haskell.org/saved/hpo3Yaef)which your example does. In this example bar's type signature doesn't tell you anything about what bar 'consumes', and it doesn't tell you that bar depends on foo, and on foo's type. Also you have to read the body of bar, and also it is bad for code reuse.
This part: "the type information only tells you how you can use doSomething. To know what is doSomething, you actually have to read the code :\" I think we're disagreeing on something quite fundamental here, based on "it doesn't tell you that bar depends on foo, and on foo's type. Also you have to read the body of bar, and also it is bad for code reuse."
(Although I am certainly open to the idea that "[I] failed to demonstrate it".)
A few things come up here:
1. Firstly, this whole example was to show that in languages which rely on this goto paradigm of error handling (like raising exceptions in python) it's impossible to know what result you will get from an expression. The Haskell example is supposed to demonstrate (and I think it _does_ demonstrate it) that with the right types, you can precisely and totally capture the result of an expression of computation.
2. I don't think it's true to say that (if I've understood you correctly) having functions call each other is bad for code re-use. At some point you're always going to call something else, and I don't think it makes sense to totally capture this in the type signature. I just don't see how this could work in any reasonable sense without making every single function call have it's own effect type, which you would list at the top level of any computation.
3. In Haskell, functions are pure, so actually you do know exactly what doSomething consumes, and it doesn't matter what getResult consumes or doesn't because that is totally circumscribed by the result type of doSomething. This might be a problem in impure languages, but I do not think it is a problem in Haskell.
Yes, it does: `bar` in your example is an `Int`, it has no arguments. That is captured precisely in the type signature, so I'm not sure what you're trying to say.
Interesting how, when encountering Rust, I didn't have to unlearn reference based programming, just learn value based programming. Somehow pure functional language advocates insist you are doing things wrong and need to unlearn it first. Kind of reminds me of a sect that promises you great things if only you work hard on leaving all your prior life behind.
I have definitely seen people struggle to wrap their head around declaring expressions representing what they want to compute when they are very used to imperative control flow like mutating some state while iterating through a loop.
> Kind of reminds me of a sect that promises you great things if only you work hard on leaving all your prior life behind.
I think this is sort of saying "hey this one thing looks like this other thing I don't like, therefore it must carry all the same problems". Perhaps we can call it "the duck type fallacy", but I don't think it's true to say that "anything which tries to change paradigm" is equivalent to cults.
I think it's a top-down vs bottoms-up approach to solving a problem.
Most people actually think in terms of top-down. They break a problem down into smaller sub-problems, which they then either break down more or has done sufficient breaking down to solve it.
I think functional style of thinking would make you do a bottoms-up approach to problem solving. You will create a very small solution (function) to solve a very trivial version/type of the problem, then repeat for each small thing. Once you have sufficient number of basic functions written, you can assemble them into solving a bigger problem.
That's the whole point of Rust, that they are not that. They are named, sized memory slots that values can be moved into or moved out of.
> I have definitely seen people struggle to wrap their head around ...
The struggle comes from being forced to use recursion where it doesn't make things easier to express but harder. Then they remember it all compiles down to machine code that just uses iteration fueled by bare metal equivalent of goto-s and the struggle feels pointless.
Imagine somebody took away your stack so when you want to do recursion, you'd be forced to roll your own stack every time. You'd struggle too.
> A double arrow => describes constraints on the type variables used, and always come first: add1 :: Num a => a -> a describes a function which takes any type a which satisfies Num a, and returns a value of the same type.
Here, I don't understand what `Num a` syntax means. It was not defined before. And, what does "satisfies" mean? It is also not defined before it is used. (It is also never mentioned again in the article.) It is maddening to read such sloppily constructed prose. Define your terms before you use them!
[0] https://hackage.haskell.org/package/base-4.20.0.1/docs/GHC-N...
`a` is the type. Num is a `class`.
Here's an example. x is an Int32 and y is an Int64. If they had type Num, then this would be valid:
add :: Num -> Num -> Num -- Not valid Haskell
add x y = x + y
However it's not valid, because you can't add an Int32 and an Int64: add :: Int32 -> Int64 -> ? -- Doesn't compile
add x y = x + y
But you can add Nums together, as long as they're the same type. You indicate they're the same type by using the same type variable 'a': add :: a -> a -> a -- Doesn't compile
add x y = x + y
But now the above complains because you used (+) which belongs to Num, so you have to declare that these `a`s can (+) because they're Nums. add :: Num a => a -> a -> a
add x y = x + y
And it comes out shorter than your suggestion of putting the constraints afterward: add :: (a :: Num) -> (a :: Num) -> (a :: Num) -- Not valid Haskell
add x y = x + yThere's also the fact that `Num` is technically not a type, but a type class, which is like a level above a type: values are organized into types, and types are organized into classes. Though this is more of a limitation of Haskell: conceptually, type classes are just the types of types, but in practice, the way they're implemented means they can't be treated in a uniform way with ordinary types.
So that's why there's a syntactic distinction between `Num a` and `a :: Num`. As for why `Num` comes before `a`, there's certainly a reasonable argument for making it come after, given that we'd read it in English as "a is a Num". I think the reason it comes before is that it's based on the usual function call syntax, which is `f x` in Haskell (similar to `f(x)` in C-style languages, but without requiring the parentheses). `Num` is kind of like a function you call on a type which returns a boolean.
After you went through the effort of learning the syntax, it becomes very clear what it means, but I agree that dropping punctuation between a bunch of names isn't the clearest communication.
It becomes even worse when you start using third party libraries that abuse Haskell's ability to define custom operators, so you get entire APIs defined as arcane sigil invocations instead of readable functions.
That's why I gave up the idea of using Haskell for actual programming, and just took the functional programming philosophy from it to other languages.
As for the meaning, in a more conventional (rust) syntax, it'd be similar to this:
fn add1<A: Num>(a: A) -> ANaming everything as a function often leads to a problem of very deep visual nesting. For example, map-then-filter can be written as "filter p . map f" in Haskell, whereas in sigil-free languages you'd write a mess like (lambda (x) (filter p (map f x))) in Lisp or "lambda x: filter(p, map(f, x))" in Python.
Of course, function composition is a very simple case, but something like lenses are another less simple case where a library would be unusable without custom operators.
In the Lisp or Python, it is crystal clear that the entire map expression is a constituent of the filter expression.
The -> operator in C is obvious. Though it does have left to right associativity, it's largely moot because only A would be a pointer-valued expression. The B and C would have to be member names:
A->left->value.
Even if the associativity went right to left, the semantic interpretation would have to be that the left member of A is selected, and then the value member of that.When X -> Y means "mapping from X to Y", the associativity could be anything, and make sense that way. Moreover (X -> Y) -> Z is different from X -> (Y -> Z). One is a function which maps an X-to-Y function to Z, whereas the other is a function which maps X to a Y-to-Z function.
In this syntax note, I was not trying to teach someone to write Haskell programmes, but rather to give them just enough to understand the examples in the essay. I did test it on a couple of friends to see if it gave them enough to read the examples with, but was trying to balance the aim with not making this section a complete explainer (which would have been too long).
Perhaps I got the balance wrong, which is fair enough, but I don't think it's required to define _every single_ term upfront. It's also not crucial to the rest of the essay, so "The article lost me at following sentence" feels a bit churlish.
x + 3 = 4
the value of x which satisfies the equation is 1. To satisfy is to be a value which makes true some truth valued formula (such as an invocation of a predicate like blue(x) or equation like the above, or inequality or such).Satisfiability comes up in logic; a "SAT" problem is, in a nutshell, the problem of finding the combination of true/false values of a bunch of Boolean variables, which make some formula true.
When the Haskeller says that "Num a" is something that is satisfied by a, it means that it's a kind of predicate which says a is a Num. That predicate is true of an a which is a Num.
The promise "if Haskell program compiles, it is probably correct" have not materialized. The most popular Haskell project Pandoc has 1000 open issues.
> Haskell program, but the ability to reason about semantics and correctness is far ahead
> of the mainstream.
For any practical program, memory usage and number of operations are part of the engineering specification and no one will deem correct a program that exceeds those specifications. So you just confirmed “impractical”, “academic” and “niche” charges.
> It actually supports equational reasoning.
TLDR: to understand what a Haskell 5-liner does, you sometimes have to read a paper. Are you actually disputing “impractical” and “academic” labels, or saying that those _good_ things?
And yet if I read a C++ program, I have no idea with just a local inspection where, if any, the allocations are happening. Reasoning about operational behaviour is not exactly a solved problem in other languages either.
> TLDR: to understand what a Haskell 5-liner does, you sometimes have to read a paper.
You have to understand the syntax and the semantics and genuinely know what you are doing. This is no different to any other programming language. It would require a whole paper to explain JavaScripts equality operator! However, Haskell does has one distinct advantage, the abstractions often come from maths and are very widely applicable. These abstractions will still be around in 10 years time.
I've encountered few C programmers who can predict what instructions will be emitted by their compiler.
Update: You might be surprised, in the presence of optimizations, how similar the code emitted by gcc and GHC can be for similar programs.
Fewer still those who can specify their pre- and post-conditions and loop invariants in predicate calculus in order prove their implementation is correct.
Most people wing it and rely on past experience or the wisdom of the crowd. What I like to call, programming by folk lore. Useful for a lot of tasks, I use it all the time, but it's not the only way.
The nice thing about Haskell here is that, while there is a lot you cannot prove (termination, etc... please verification friends, understand I'm generalizing here), you can write a sufficient amount of your specification and reason about the correctness of the implementation in the same language.
This has a nice effect: you can write the specification of your algorithm in Haskell. It won't be efficient enough for use at first. However you can usually apply some basic algebra to transform the program you know is correct into one that is performant without changing the meaning of the program.
That's an irrelevancy. Predicting those specific instructions does not preclude one from making reasonably correct judgements about a program's performance.
It is a fact that reasoning about performance of Haskell program is virtually impossible, unless you're an active ghc developer, and that's why the language remains unused for practical problems. Apart from buggy pandoc and few blockchain scams, that is.
As for practical problems, I dunno. I work in Haskell full-time at a company that isn't doing block-chains. And I stream myself working in Haskell once a week on pretty practical things. I've made a couple of small games, a PostgreSQL logical replication client, have been learning different algorithms. All pretty practical to me.
That's simply not true. Reasoning about performance of imperative languages is fundamentally easier.
Several other null-safe languages solve this correctly by allowing disjunctions of types (often called unions, though countless other type related things are also called unions). They have a type operator "|" (or) and the function type (X -> Y|Null) can be improved by rewriting the function to (X|Null -> Y). Code outside the function doesn't have to be changed: Accepting X or Null implies accepting X, and returning Y implies returning Y or Null.
>> Because then you would have to modify all the code which already uses the function, as "A" and "Maybe A" are incompatible types. Which is illogical.
I'm not so sure about your statement. If the type of the function changes, revising its usage at every use point is good for your sanity. I would go further and say that sometimes Maybe X is too weak, because it doesn't contain precise semantics for its Nothing and Just x alternatives. For example, sometimes you want a `Nothing` for a value that hasn't yet been found, but could potentially be filled up the evaluation chain, e.g. `NothingYet`, and a different Nothing for a value that is conclusively not there, e.g. `TerrifyingVoid`. If you fork your `Nothing` value into these two variants after you discover the need for it, you will have to revise each call anyway, and check what's the proper course of action. And this is a feature I wish I could use from Python.
More generally, in large Haskell code bases, having the type-checker help you track, at compile time, code that breaks far away from where you made your changes, is an incredible time-saver.
data Maybe a = Just a | Nothing
So to make an X a Maybe X, you'd put a Just before a value of type X.
For example:
one :: Int
one = 1
mOne :: Maybe Int
mOne = Just one -- alternatively, pure one since our type signature tells us what pure should resolve to.
Reason we can do this is because Maybe is also an Applicative and a Monad and so implements pure and return which takes an ordinary value and wraps it up into an instance of what we want.
Swift interestingly doesn’t require this, but only because Optionals are granted lots of extra syntax sugar in the language. It’s really wrapping it in .some(x) for you behind the scenes, but the compiler can figure this out on its own.
This means that in swift, changing a function from f(T) to f(T?) (ie. f(Optional<T>)) is a source-compatible change, albeit not an ABI-compatible one.
I'd call that explicit casting. Implicit casting would be
mOne = one
Compiler already knows what "one" is, it could insert the "Just" itself, no? Possibly due to an operator defined on Maybe that does this transformation?That is, are there some technical reasons it doesn't?
Or is it just (no pun inteded) a language choice?
It seems you're asking for code to be both null safe and not null safe simultaneously.
Having a language just decide that it would like to change the types of the values flowing through a system is wild. It's one of the reasons that JavaScript is a trash fire.
Of course with great power comes great responsibility, so it's a tool that should be used sparingly and deliberately.
Now as mentioned I don't use Haskell, but that's why I like it in other languages.
I asked as I was curious if there was something that prevented this in Haskell, beyond a design choice.
If you make a breaking change to your API, then you should want your tools to tell you loud and clear that it’s a breaking change.
I also don’t agree that keeping the structure around values internally logically consistent “doesn’t really contribute to anything”. On the contrary, I think this idea is hugely important. How would your idea generalise? The compiler should just know that my `Int` should be a `Maybe Int` and cast it for me. Should the compiler also know that my `[a]` should be cast to a `(a, b)` because incidentally we’re fairly confident that list should always have two elements in it?
I think if this way of thinking is unfamiliar, then it’s a good reason to learn Haskell (or Elm, which is at least as good, or maybe better, for driving this point home).
The compiler should not "just" know it. It would know it because we told it how.
Consider a function that takes a float and returns a complex number. I then change the function to take a complex number ("Complex Float" in Haskell if I read the docs right), and returns a float.
I could then tell the compiler, by implementing an implicit cast operator, how to cast float to complex. The implicit part is then that the compiler tries it without me telling it to use the cast explicitly.
Then any code that worked with the old function should work perfectly fine using the modified function without modifications, since per definition the reals are contained in the complex numbers.
This is how I do it in several languages I've used.
Again, I don't know Haskell, so from the outside it looks like much the same as the float -> complex conversion.
You lose this ability if you start letting the compiler decide that `Int` is as good as `Maybe Int`. Or if an `Async (Async String)` may as well be an `Async String`.
That's not to say it's not easy to transform (just a few keystrokes), but explicit beats implicit when it comes to casting (in any language).
[1] Does this work?
var x = 1 == 2 ? Optional.of(2L) : Optional.empty().map(y -> y + y);
// Operator '+' cannot be applied to 'java. lang. Object', 'java. lang. Object'
How about Optional<Long> x = 1 == 2 ? Optional.of(2L) : Optional.empty().map(y -> y + y);
// Operator '+' cannot be applied to 'java. lang. Object', 'java. lang. Object'
or even Optional<Long> x = 1 == 2 ? Optional.of(2L) : Optional.empty().map((Long y) -> y + y);
// Cannot infer functional interface type
No, we needed Optional.<Long>empty() instead of Optional.empty() to make it workI was thinking more like explicitly telling the compiler that an implicit cast is OK, in other languages done by implementing the implicit cast operator for example.
edit: but if I understood you correctly, Haskell just doesn't support any implicit casting?
If no other info is given, it will treat `3 + 3` as Integer + Integer (and emit a compiler warning because it guessed the type).
With `(3 :: Int64) + 3`, the right 3 will resolve to Int64. Same if you swap their positions.
`(3 :: Int64) + (3 :: Int32)` is a compile error.
"Text literals" can become a String, a Text, or a ByteString if you're not explicit about it.
> implicit cast operator
Wouldn't that make it explicit?
No, the casting is still done implicitly. That is I can make the following compile fine in Delphi if I add an implicit cast operator to either Foo or Bar:
Foo x := Foo.Create();
Bar y := x;
If neither of them have a suitable implicit cast operator defined, it will of course fail to compile.Just an example, nothing unique about Delphi. You can see an example of the operator definition here[1].
[1]: https://docwiki.embarcadero.com/RADStudio/Alexandria/en/Oper...
Not without explicitly enabling `OverloadedStrings`!
https://ghc.gitlab.haskell.org/ghc/doc/users_guide/exts/over...
Then to get the result "b" you can use the `maybe :: b -> (a -> b) -> Maybe a -> b` function to get your "b" back and do the weakening as you desire.
`Maybe` assumes a computation can fail, and the `maybe` function forces you to give a default value in the case that your computation fails (aka returns Nothing) or a transformation of the result that's of the same resultant type.
Overall, you'd end up with a function call that looks like:
foo :: b
foo = maybe someDefaultValueOnFailure someFuncOnResult (pure aValue >>= mFunc)
or if you don't want to change the result then you can use `fromMaybe :: a -> Maybe a -> a`
bar :: b
bar = fromMaybe someOtherDefaultValueOnFailure (pure value >>= mFunc) -- if the last computation succeeds, return that value of resultant type of your computation
If function f returns b or nothing/error, and is then improved to return b always, client code that calls f should not require changes or become invalid, except perhaps for a dead code warning on the parts that deal with the now impossible case of a missing result from f.
You are suggesting not only a pile of monad-related ugly complications to deal with the mismatch between b and Maybe b, which are probably the best Haskell can do, but also introducing default values that can only have the practical effect of poisoning error handling.
Why do you need to change the type signature at all? You "improved" [1] a function to make impossible for the error case to occur, but it's used everywhere and the calling code must handle the error case (I mean, that's what static typing of this sort). So there you have it: the client code is not rendered invalid, it just has dead code for handling a case that will never happen (or more usually, this just bubbles up to the error handler, not even requiring dead code at every call site).
As an aside, I don't see the problem with the "pile of monads" and it doesn't seem very complicated.
----
[1] which I assume means "I know I'll be calling this with values that make it impossible for the error to occur". If you are actually changing the code, well, it goes without saying that if the assumptions you made when choosing the type changed when re-writing the function, well... the calling sites breaking everywhere is a strength of static typing.
Or you have a function which expects objects of type String as an input. But then you realize that in your case, null values can be handled just like empty strings. So the input type can be relaxed to String|Null.
Just "" != Nothing
If you want to handle empty strings as a input in haskell then you have a function of type `f :: String -> b` and you pattern match on your input?
f "" = someResult
f ...
Nothing assumes a proper null in that there is genuinely nothing to work with. Still you can make a function to handle it or use `maybe`?Haskell gets in the way by facilitating clashes of incompatible types: there are reasons to make breaking changes to type signatures that in more deliberately designed languages might remain unaltered or compatible, without breaking call sites.
You can achieve this by not changing the type and keeping the result as Maybe b. Dead code to handle `Nothing`, no harm done.
However, you clarify you don't want this because:
> Changing the type signature (which, by the way, could be at least in easy cases implicitly deduced by the compiler rather than edited by hand) allows new client code to assume that the result is present.
But this cuts both ways. If the old call site can assume there may be errors (even though the new type "b" doesn't specify them) then the new call site cannot assume there are no errors (what works for old must work for new).
I must say I see no real objection to the proposal at https://news.ycombinator.com/item?id=41519649 besides "I don't like it", which is not very compelling to me.
> he compiler doesn't know, so the only way the compiler can ensure null safety for using old code is by enforcing you to do null checks everywhere
This isn't necessarily true. Java's approach is to have 3 types: nullable, non-null, and platform (just like Kotlin). Platform types are types without nullness markers and don't require strict null checks to prevent backwards compatibility breaking. Yes, old code may still product null pointers, but we don't need 100% correctness right away. At some point platform types will be 1% of code in the wild rather than 100%.
In the case of Java this could take decades. Or people simply continue to write platform types because they are lazy (the compiler doesn't force them). Then platform types will never decrease substantially.
But in practice, as long as you use only Scala libraries and are careful when using Java libraries it's not really an issue. (speaking as a professional Scala developer for many many years)
Similar logic applies to code that has migrated over to .NET 6/8, or any newly written code past ~.NET 5.
If your value of Y is predicated on receiving an X, I have trouble seeing how you would write such a function. If you have a default value, then Haskell would solve it just like any language with optionals:
y :: Int -> String
(fromMaybe "defaultValue" (map y (Just 3))
> Several other null-safe languages [...] returning Y implies returning Y or Null.I have trouble seeing how the language is null-safe in that situation.
We didn't assume it is. Say you have a function of type (String -> String|Null). Further assume that you realize you don't necessarily need a String as input, and that you in fact are able to always output a string, no matter what. This means you can rewrite (improve!) the function such that it now has the type (String|Null -> String). Relaxing the type requirements for your inputs, or strengthening the guarantees for the type of your output, or both, is always an improvement. And there is no logical reason why you would need to change any external code for that. But many type systems are not able to automatically recognize and take advantage of this logical fact.
> > Several other null-safe languages [...] returning Y implies returning Y or Null.
> I have trouble seeing how the language is null-safe in that situation.
If you always assign a value of type Y to a variable of type Y|Null, the compiler will enforce a check for Null if you access the value of the variable, which is unnecessary (as the type of the variable could be changed to Y), but it can't result in a null pointer exception.
Haskell's type system definitely is a positive example of what can be done to avoid completely the null problem. Is it the utmost that can be done? No. But it's been a working proof of solution for 20 years, while proper typecheckers for union types are a recent thing.
Falls short compared to what? Arguably, if you're talking to someone using Java or Python, Maybe is plenty enough; and getting started on type families is certainly not going to work well.
If `Y` is implicitly `Y|Null`, then it is no longer possible to declare "this function does not return null" in the type system. Now understanding what a function can return requires checking the code or comments. This is the opposite of null safe.
It isn't. It's just that if you say "this function returns Y or null", and it returns Y, your statement was true. If you give me a hammer, this implies you gave me a hammer or a wrench.
I may still be thinking about this incorrectly. Do you have an language in mind that contradicts this?
> I may still be thinking about this incorrectly. Do you have an language in mind that contradicts this?
Any language which supports "union types" of this sort, e.g. Ceylon or, nowadays, Typescript.
# old
function f(x: number): number {
return 2 * x;
}
# new
function f(x: number|null): number {
x = x || 3;
return 2 * x;
}
# usage
# old
f(2)
# new
f(2) # still works!
But in Haskell this requires changing the call sites: -- old
f :: Int -> Int
f = (*2)
-- new
f :: Maybe Int -> Int
f = maybe 0 (*2)
-- usage
-- old
f 2
-- new
f (Just 2) -- different!
But I actually feel this is an antipattern in Haskell (and maybe TypeScript too), and a separate wrapper function avoids refactoring while making things even more user friendly. -- old
f :: Int -> Int
f = (*2)
-- new
fMaybe :: Maybe Int -> Int
fMaybe = maybe 3 f
-- usage
-- old
f 2
-- new
f 2 -- still works!
fMaybe Nothing -- works too!
Here's some wrappers for general functions (not that they're needed, they're essentially just raw prelude functions): maybeIn :: b -> (a -> b) -> (Maybe a -> b)
maybeIn = maybe
maybeOut :: (a -> b) -> (a -> Maybe b)
maybeOut = fmap Just
maybeBoth :: b -> (a -> b) -> (Maybe a -> Maybe b)
maybeBoth d = maybeOut . maybeIn d
Added bonus, this approach avoids slowing down existing code with the null checks we just added. maybeOut :: (a -> b) -> (a -> Maybe b)
maybeOut = (.) Just
Also the parenthesis around the last two output types are added for emphasis, but can be safely removed. const maybe = <T,>(f: (_: T) => T, d:T) => (x: T|null) => f(x || d);
console.log(maybe((x) => 2 * x, 3)(null)); // returns: 6One of the main points of encoding error information in the type system in the type system is that it forces you to account for it when you modify your code.
By "weakening" your assumptions on your function to allow it to produce Null values you have introduced a new requirement at all your call sites. Everywhere that you call this function now needs to handle the Null value. Its a GOOD thing that Haskell forces you to handle this via the type system.
No, but a function which returns Y | Null can be replaced with a function which returns Y without changing code on the call site.
Imagine I always used to give you a nail (Y) or nothing (Null), and you can handle both receiving a nail and receiving nothing. Then I can, at any time, change my behavior to giving you nails only. Because you can already handle nails, and the fact that I now never give you nothing doesn't bother you. You just perform a (now useless) check of whether you have received a nail or nothing.
Yes this falls out of injectivity.
> the function type (X -> Y|Null) can be improved by rewriting the function to (X|Null -> Y)
I agree that any value received by the former function (`X`) can be received by the latter function (`X|Null`). However you cannot rewrite the former to have the signature of the latter.
You would need to write:
prf : (X -> Y|Null) -> X|Null -> Y
You would have to be able to convert a `Null` value into a `Y`.
You could definitely use `X|Null -> Y` to implement `X -> Y|Null` but that is not what you are claiming.
Of course I can. I can always change the code to anything I like. That's what "rewriting" is. The question is only whether the business logic still makes sense, and whether the old call site code still works. Just look at the example I gave above.
> You could definitely use `X|Null -> Y` to implement `X -> Y|Null` but that is not what you are claiming.
"Implementing" is a special case of rewriting, so how can you say you can implement something but not rewrite it?
You have a function that can return a Null response and you are claiming you can rewrite it to be one that does not return a Null.
This means that in the cases where your function previously produced a `Null`, you have to produce a `Y`. You claimed you can do this if you write the function to receive `X|Null`. In other words you are claiming you can write `(X -> Y|Null) -> X|Null -> Y`. I challenge you to write this function.
> "Implementing" is a special case of rewriting, so how can you say you can implement something but not rewrite it?
I didn't say that. You claimed you can write `(X -> Y|Null) -> X|Null -> Y`. I am saying that is impossible but you could write `(X|Null -> Y) -> X -> Y|Null`. Do you see the difference?