Upcoming/proposed breaking changes to Haskell
github.com
github.com
This has a couple of effects:
- It does make it harder to maintain code for businesses, since keeping up with language updates means that you will have to do relatively more maintenance work to keep up with these breaking changes.
- It slowly makes the core libraries more and more elegant over time and this paves the way for new advances in eg type systems and whatnot. Linear types would have been way harder to add if the existing system had been (even more) a giant mess of hacks to maintain backwards compatibility 20 years back.
- The ease of writing GHC extensions makes it so that it is relatively easy to extend the base language in some way and back out if it doesn't turn out to work. This makes experimentation way cheaper than if every core language change has to be "permanent".
These changes combined mean that Haskell itself will probably never be a mainstream language for business applications, and that is fine. Because many (most?) programming language implementors have had some exposure to Haskell in university and because they all speak to one another on conferences etc, many of the ideas first explored by Haskell (and its predecessors in academia) are "leaking out" if they are good (like list comprehensions in Python or type classes in Rust) and they don't get taken over if they turn out to have been mistakes (like lazy I/O). The true value of Haskell is having a language in which to experiment with new concepts, so they can be proven useful (or not) before they make their way into the wild.
To be honest — I think even in my 90KLOC Haskell codebase — handling these breaking changes are a cheap and easy change because of the compiler.
IMO, the existence of the Haskell Report and the inability of the community to update it in a reasonably timely manner is the biggest cause of the persistence of the biggest warts like partial head and foldl. I don't think anyone wants to keep those but "The Haskell Report specifies that they are in the prelude and with the exact implementation they have" tends to kill any discussion. Let's hope the HF makes some progress on that soon!
Wait, what? This runs completely counter to my experience of Haskell. I use it whenever I can, and I’m pretty sure I’m not a PL researcher. Lots of other programmers write actual, real-world programs in Haskell as well. Much of the discussion I see in Haskell communities concerns areas such as performance, toolchains and libraries — areas which PL researches are famous for ignoring. I will admit that we often talk about GHC extensions and type theory and whatnot, but the discourse around those areas is not all that mathematical; it tends towards ‘how is this useful for writing programs?’. In other words, exactly like every other real-world programming language out there.
(That being said, maths is fun, and I regularly see people defining weird and wonderful abstractions. But this rarely gets in the way of writing programs. If anything, every now and then someone comes up with an abstraction which turns out to be incredibly useful in practice: lenses, free monads, applicativeṣ, HKTs…)
The point in your last sentence that "every now and then someone comes up with an abstraction which turns out to be incredibly useful in practice" is exactly what I meant in the OP: these abstractions seem to be developed way more often in Haskell first and then they leak to other programming languages later than the other way around.
By contrast, a typical mainstream programming language might, say, completely neglect one or more of these communities in favor of whatever is best for commercial programmers. Particularly when, like most mainstream languages, it's mainly funded by those interests.
There are plenty of sites still on PHP 5 for example.
If you are doing something where you rely on third party libraries, and you have to keep those third party libraries up to date (e.g., a third party library that uses a remote service and that remote service keeps changing their API), then you may be forced to update to the new version of the language because the library switches to the new version.
For a language that isn't really mainstream for business use, I'd expect that there aren't a lot of third party business libraries that you'd be using and so the "library made me do it" language update would not be necessary. That should allow staying on the old version as long as an OS upgrade doesn't kill the ability to run the compiler or interpreter.
Java 1.4, 2002: assert
Java 1.5, 2004: enum
Java 10, 2018: var
Java 14, 2020: yield
Java 16, 2021: record
Java 17, 2021: permits, sealed
Almost all were added in such a way that the extent of the breakage would be that a program which previously worked would now fail to compile (i.e. fail safely.)(The only exceptions I can see are for `assert` and `var`, and even then only when some parts of a program are compiled with older versions of the compiler, and even then only when various other conditions are met.)
With PHP, you could argue that they were needed because it developed organically, instead of through a process by experienced language designers. I'm not sure about Python though.
Counter-example, Go hasn't had backwards-incompatible changes yet, and at the moment there's no compelling reasons to make a breaking 2.0 version - and any plans for a 2.0 version so far have minimal changes, so going to 2.0 should be a smooth and quick process.
Langages generally try to add contextual / soft keywords these days but otherwise it’s absolutely a breaking change: any variable named the same will trigger a parse error. That is why languages try to either not add keywords, or find ways to make them opt-in somehow.
Of course adding a keyword is a breaking change. It will invalidate all uses of that keyword where used as a variable. Perhaps PHP is unaffected, as it has sigils on its variables, but most languages, including haskell, do not.
Locals are prefixed but functions, constants, and classes are not.
Also barewords but that horror was removed in php 8.
I like that Haskell has inspired Scala, my main language. While it lacks some of the elegance of Haskell, it is a nice compromise between an advanced and Haskell-inspired type-system and stability for business, plus having the whole JVM ecosystem is awesome.
Therefore, hopefully Haskell keeps advancing and bringing innovations and having them trickle into more mainstream languages.
If it weren't for the JVM Scala could make do without an Any type and without having to deal with null values. I hope that alternative runtimes like Scala Native will become more popular, since Scala could evolve away from its JVM roots then.
I'm also not a fan of JVM performance optimization, since there are too many knobs to turn, and the knobs often have (undocumented) side-effects on other knobs. This is a lot simpler with the Haskell runtime (in my experience) since you mainly need to tweak the GC settings there.
As for scala, missing the java ecosystem would pretty much decimate the language, no matter how cool it is.
That being said, the Any type would be there in any case - this is not JVM specific. "null" is, but I think it's not too much of a big deal with Java 3's Union-types, see: https://dotty.epfl.ch/docs/reference/other-new-features/expl...
The only problem is that if you now use native Java libraries, you'll be confronted with everything being explicitly nullable (no runtime error problems though). But that's a small price to pay for having access to this huge ecosystem.
That is so incredibly slow, that this point is moot, unless you are comparing it to C++.
You will have much more work keeping up with change on the Haskell library ecosystem, will have more work keeping up the changes on the core of most mainstream languages, and will have orders of magnitude more work keeping up with the ecosystem of any other language.
They are all logical, but will unfortunately add to the burden of new learners who read books and guides that contain obsolete references. I know a lot of the material I learned with will be confusing when examples start spitting out compile errors. I didn't see if there are efforts to account for this with some custom error messages that notify you why the obsolete code no longer works and suggests how to fix it, but I think that would be helpful.
Edit: It looks like that concern was already raised and addressed, glad to see it.
For example, in Idris 2, the `[ foo | x <- bar ]` syntax is not just a "list comprehension", but a "monad comprehension", and `map` is not specialized for lists but it is actually what `fmap` is in Haskell. There is also other interesting shorthand syntax like `!foo` which is equivalent to something like `do x <- foo ; x`.
The fact that it compiles to Chez Scheme and has a fairly easy-to-use FFI is even nicer.
Would I deploy it in production yet? No. But the language is trying to be fundamentally practical and user-friendly while still being dependently- and quantitatively-typed. And in my opinion, it's succeeding so far! It also has a language server, so you can get advanced interactive editing features in pretty much any code editor that supports LSP.
Oh, and there's no `return` in Idris 2, only `pure` :)
And then, with the possibility of success on the horizon, the Idris folk(s) got distracted by the QTT hype and decided to abandon ship so they could start on Idris 2. And, like, don't get me wrong - the QTT stuff is cool, and I'm a Rust fan in part because getting to encode linearity in your types is a real superpower - but it's so depressing that the world of practical dependent types feels just as far away now as it did ten years ago, after getting so excited about it five years ago. And it underscores what might be a bigger problem: at the end of the day - and by the Idris FAQ's own admission - Idris is a research project into practical dependent types, and Edwin Brady is an academic. Which means that when he gets interested in some other area of research, he's free to just...do that instead, or start over on Idris n+1, and leave Idris 1, 2, 3 ... n to wither.
[1] https://docs.microsoft.com/en-us/dotnet/fsharp/language-refe...
GHC has an enormous amount of machinery to infer types and check equality automatically. I wonder if Idris has anywhere near that much. Automatic inference is why Haskell resisted dependent types until quite recently, though there were already some cases that it couldn't do automatically.
As for pure vs return, that's because the typeclass system was rearranged pretty recently. Before that, Monad instances weren't necessarily also Applicative. "return" was kind of cool in that you could write what looked like imperative subroutines that returned values.
!foo in Idris sounds like foo >>= whatever or whatever =<< foo in Haskell.
Idris 2 has more or less no type inference at all. I think maybe type inference for dependent types isn't decidable, or something like that?
But I know that they are interested in adding inference to Idris 2 in certain cases that can be proven to work correctly.
It's definitely a deficiency in the Idris user experience vs the Haskell user experience.
> !foo in Idris sounds like foo >>= whatever or whatever =<< foo in Haskell.
Idris has >>= as well. But see here for a nice demo of ! and other "do"-related syntax: https://idris2.readthedocs.io/en/latest/tutorial/interfaces....
Imo inference is a bit overrated anyway. I find annotations to be very useful.
There's a series of steps here:
- "Static type annotations are too verbose, dynamic languages allow you to avoid the ceremony." - "But Hindler Milner type systems can infer the types, so you usually don't have to write the type annotations!" - "Writing type annotations is useful anyway, because it serves as valuable documentation, makes reasoning about the program easier, and results in more useful type errors."
Thinking about it more now, there's another step: - "If you have good type inference, you can use editor tooling (eg the LSP) to automatically insert the type signatures for you!"
Part of the usefulness of top level annotations in Haskell is that if your program has a type error, inference can fail in multiple ways. I.e. the unification algorithm might assign type T to your function F and chug along happily until it runs into a conflict in some unrelated part of the program, then throw an unhelpful error message in the case where T wasn't the type that you intended for F. By annotating F with the type you really want, you get an error message that does a better job pinpointing the problem.
This is less of an issue in ML, since ML's type system is less general than Haskell's, so the inference algorithm can't go as crazy, and you usually get good error messages even without annotations.
I'm more saying that I'm fine with type system features that break global inference, as long as there's still local inference that's usually good enough in practice.
What does affect the usability of the language is when (a) the line in the sand isn't very clear from an end-user point of view [see simplified subsumption in GHC 9, for example], or (b) the line is on the wrong side of simple features [see monomorphic local bindings, for example].
I guess what I'm saying is that Idris having minimal type inference is kind of terrible, and the excuse that it's not decidable for dependent types is a flimsy one.
Part of the fun of being a research language is that you don't know everything up-front!
We made Applicative a superclass of Monad around 2015, and since then return has been a historical artifact (with a dangerously misleading name).
On the one hand, this is a change whose software engineering benefit is very small. (There is a small performance advantage, but it's not significant.) It has a large aesthetic benefit, though, in that it makes the standard library have one less ugly wart. But that wart wasn't really doing anything except being ugly. Some community members have a point of view that "ugly" shouldn't matter, and that in particular, the bar for breaking changes should be very high, and far higher than wart removal.
On the other hand, this particular change should affect extremely little actual code, because defining (/=) explicitly is dumb. When it does affect code, the solution is just to delete the lines of code that are doing the dumb thing. They don't even need to be replaced, because the only reasonable behavior is already provided by the default. Just deleting the dumb code is enough.
On the OTHER other hand, even a small amount of code change can add up to a lot of dependency management work. If just one library somewhere at the leaf of a dependency tree needs it's explicit (/=) deleted, then version bounds need to be added for all upstream bodies of code to ensure that they don't try to build against the old now-broken. Changing all those version bounds across perhaps dozens of packages is far, far more work than just deleting those few lines of code. You can look at this as a broken process, but I don't think anyone has a great answer to dependency management.
So I think it's in the end kind of a coin flip between (a) it's infuriating to some people not that this specific change is being made, but that it indicates the community values aesthetics above backward compatibility, and the absence of almost any practical software engineering benefit makes this a compelling test case upon which to direct their fury, and (b) despite the tiny amount of actual code change, it's quite possible it could lead to a non-trivial amount of coordination work between libraries, and THAT is a real (and not very fun) job.
a /= b = not (a == b)
is the kind of thing that makes Haskell beautiful. Slowly ascending to purity. Didn't break anything of mine, though. class Eq a where
(==), (/=) :: a -> a -> Bool
x /= y = not (x == y)
x == y = not (x /= y)
https://www.stackage.org/haddock/lts-18.17/ghc-prim-0.6.1/sr...The way this works is that when you implement the `Eq` typeclass for a type, you can provide more specific implementations. A minimal complete implementation is to implement either == or /= for your type, and the other definition will apply.
The way this breaks existing code (iiuc) is that as a top-level function, if you have imported the `Eq` type class but haven't imported the `Prelude` module implicitly (on by default but can be turned off), you will not have `/=` in scope anymore.
I think people are upset by this less because of how hard it would be to fix, but more because of how it acts as a sort of signaling change ("they're willing to break my code for this, which I didn't want, but not for X which I did"). The most common value of X I've seen is removing or reworking the partial functions in the Prelude (e.g., head / tail on `[a]` raising an exception instead of returning `Maybe a`).
I think elm has shown that it’s ok to avoid functions being partial out of the box, even you’re nowhere near a total language and you certainly allow the user to create partial functions or panic / throw errors.
I think the right solution ultimately might be different preludes geared to different things, though doing the right dance around making the right things available in the right places (and unavailable in the wrong ones) while also making it easy to understand (and remember) which context you're working in and also not splitting the community is going to be... delicate.
It would be great if the entries in the list each included a very brief rationale for the change. Yes I can see there is a linked document for each one, but that is a lot more reading, when in many cases a one-liner is probably enough.
Anyway, making a hierarchy more consistent with mathematical tradition is a good thing.
As for usefulness, without redundant esoteric abstractions Monoid is all we need, and the abstract Monad type-class, of course (for building abstraction barriers at a type level).
The problem with Haskell is whole Himalayas of bullshit under which its clarity and principles has been buried.
Haskell became the way of "virtue" signalling for narcissistic snowflakes, second only to the Category Theory, which is, buy the way, is "empty" outside of monoidian composition and the notion of an abstract functor.