PureScript always seemed a little more appealing from that perspective, but doesn't seem to have really caught on (eg; https://trends.google.com/trends/explore?date=today%205-y&ge...)
PureScript always seemed a little more appealing from that perspective, but doesn't seem to have really caught on (eg; https://trends.google.com/trends/explore?date=today%205-y&ge...)
When Python adds new string formatters or generators or async, they're just in the language. When Haskell does that they require you to explicitly turn the new feature on. It's just a different strategy of managing language change. Versus the classical approach you get a lot more eyes on your language additions and have a nice "beta" period where you can make improvements before there is wide adoption.
This is basically the same approach that Rust is taking but they've been better about removing the feature flag when the feature is stabilized.
And yes, I think the Rust comparison is a great one – I'm not the sort of person who would want to use nightly Rust and turn on feature flags; I'd want to use Stable rust with no extra features.
These features have tradeoffs around usability and power, so making them importable defers that tradeoff to the end programmer, who is best positioned to make those decisions. It's kind of the opposite of the paternalistic approach the Go designers take.
The developers of rustc define the language, while the developers of GHC are trying to be first an implementation of a purportedly independent standard.
I don't raise this as defense or attack on either side, but I think it's an important bit of context in understanding the differences.
This makes me feel that, totally apart from the syntax, Haskell/GHC is still more of a laboratory for doing programming language research. Which is great for what it is; I would say Haskell's original motto "avoid popularity at all costs" still holds. I also had hope for languages like PureScript that make an opinionated choice about a subset of those features for the sake of a consistent development framework.
This is simply false. Quite a few of them are not much more than syntax sugars, e.g. with LambdaCase I can write
eitherToMaybe = \case
Left _ -> Nothing
Right x -> Just x
instead of eitherToMaybe var = case var of
Left _ -> Nothing
Right x -> Just x
With OverloadedStrings I can write "foo" instead of (T.pack "foo") or whatever and it picks the right "pack" function based on the inferred type.OverloadedLists does the same for listy things, e.g. now [1] can be a literal vector and you don't have to say V.fromList [1].
TupleSections lets you write (1,) when you want a function that tuples something with 1 (so (1,) applied to 2 becomes (1,2)).
NamedFieldPuns lets you write
foo x = let myField = x + 1 in MyType{myField}
instead of foo x = let thing = x + 1 in MyType{myField = thing}
-----And then there are a bunch of "Deriving…" extensions that don't require any knowledge either, you just enable the pragma, and it lets you say e.g.
data X … deriving (FromJSON)
instead of having to explicitly implement the FromJSON instance by hand. I have used Deriving a bunch and have no idea how they work :-)Looking through my main project, the only other extension I use on a regular basis is ScopedTypeVariables. That one's a bit more difficult to explain since you have to have some idea what the words "scope" and "type variable" mean, but basically lets you refer to a type variable from the outer function signature in the type signature of a where-clause. Normally, with
foo :: Maybe a -> Int
… where helper :: Maybe a -> Int
the a's are seen as different types. With ScopedTypeVariables, you can say foo :: forall a. Maybe a -> Int
… where helper :: Maybe a -> Int
and the a's are the same type. So that lets you have explicit type sigs on helper functions that are restricted to the type of the outer function. (Though I am not an expert on this so maybe I've got it all wrong. But that doesn't stop me from using it to make stuff with.)Googling "most popular haskell language extensions", I see this article: https://lexi-lambda.github.io/blog/2018/02/10/an-opinionated... which lists 34 recommended extensions and admits "a few of them are likely to be more controversial than others". Half of them don't have names which make their purpose immediately obvious to me.
A more data-driven approach is turned up here: https://gist.github.com/atondwal/ee869b951b5cf9b6653f7deda0b... which concludes "only 10 extensions show up in more than 10% of the Haskell files on GitHub, and 20 show up in more than 5%!"
Its discussion on reddit (https://www.reddit.com/r/haskell/comments/916jj0/popularity_...) highlights some gaps in the analysis, but also the general notion that "everybody uses language extensions, but not everybody uses the same ones".
Of course, each language extension changes the syntax and/or semantics of the language, so in order to "learn Haskell" I'd have to learn one or more flavors/styles of it – and there doesn't seem to be one standard set of language extensions that are commonly used.
Which isn't to say they're not an issue - Haskell really needs a new language standard to clean up that mess, but they don't really fragment the ecosystem the way that, say, the Python 2 vs 3 split did.
To share more on where I'm coming from, one example: when I look at things like Yesod's "Hello world" section in "Getting Started", the first thing I see is 4 pragmas: https://www.yesodweb.com/book/basics and I just don't have much information on what those are, how commonly they're used[0], whether (and why) I need them to use Yesod, etc.
I have a kneejerk reaction to that since I want to be able to understand the code I'm seeing and I'm not sure what's going on behind the scenes. But kneejerk reactions are bad, so I'll give them the benefit of the doubt next time!
[0] Well, I definitely understand OverloadedStrings and TemplatHaskell to be very widely used, and think I get their gist.
[whamlet|Hello World!|]
I would say that they're closer to the situation you're concerned about - they're very much like C++ templates, and equally controversial for exactly the same reasons. Still, I don't usually think of C++ templates, or Haskell ones, as being a different style per-se, just an advanced feature to use sparingly, mostly just when they'll save you from writing a bunch of boilerplate.For completeness - Overloaded strings are the Haskell parallel to Python's b-string, f-string, u-string, etc. TypeFamilies are "let me write functions over types", which sounds like it would be dramatic enough to introduce a different coding style, but since it, like other extensions, needs to play nicely with the type inference, it doesn't conflict with other extensions, so things mostly just work as you'd expect. Since extensions generally work nicely together, most people just turn on "the usual" extensions for the whole project (all the more reason we need a new standard), and only add explicit pragmas in files to call out heavyweight things like TemplateHaskell. In tutorials you're more likely to see them all listed at the top, though, to be clear about what's being used.
> Of course, each language extension changes the syntax and/or semantics of the language, so in order to "learn Haskell" I'd have to learn one or more flavors/styles of it – and there doesn't seem to be one standard set of language extensions that are commonly used
> It seems like every Haskell project makes use of a different subset of 10 to 20 language extensions, each of which requires some deep knowledge of type theory to understand how it affects the language.
These comments are both pretty off the mark. It's unhelpful to think of language extensions (the most commonly-used ones at least) as extending the language. It's more that there is one overall Haskell language and the absence of a particular language extension disables a particular feature. The language is, by and large, more coherent with language extensions turned on, not less coherent.
If the language is more coherent with the extensions, they should enable them by default.
I personally don't know most of them, and I've been doing haskell professionally for years now. Once in a while, I will need to do a specific thing and will google up stuff, and the thing I'll write will remain contained in a couple files, and callers won't have to know about it.
It is absolutely not a prerequisite to know all extensions to earn one's living doing Haskell, just like one doesn't have to know the JVM or the dozens of design patterns inside and out to program in Java.
Which ones are a prerequisite? OverloadedStrings? TemplateHaskell? How many others?
If the answers to that list were enabled by default in the language, and most Haskellers agreed on them, there really wouldn't be much complaint – you'd reach for these fancy macros when you need them, like you say, and that'd be that. A newcomer wouldn't know that some of them are required reading and others aren't and not know which is which.
OverloadedStrings is one of the few extensions you'll see everywhere, but there's nothing to remember about it. It's really the kind of thing you'll understand in 10s and find obvious forever after.
> If the answers to that list were enabled by default in the language, and most Haskellers agreed on them, there really wouldn't be much complaint
Here is the thing: just moving all the extensions into the language and removing the pragma would basically mean there would still be the same amount of things in the language, but with no convenient way to know what's used in a specific file, and it would be hard to research that topic which you don't know.
Extensions are great in that they force devs to label files with searchable names for the concepts they use in the code.
> A newcomer wouldn't know that some of them are required reading and others aren't and not know which is which.
A newcomer, just like a seasoned haskeller, shouldn't bother learning new extensions if they're not about to use them. That's the way to go forward.
On the other side, ScopedTypeVariables is the way the language should have always worked, and should probably just be enabled globally.
OverloadedStrings is a trivial syntactic convenience, where every use of a string literal is replaced by (fromString "the string literal"). This makes code substantially more readable when you are wanting literal values of (typically) Text. The downside is that the compiler can't always know what type fromString is expected to return, so you sometimes need to add more type annotations, which gets messy again. I don't care much which way it's set at a project level - I toggle it per file if the project default is too much of a pain locally.
The main reason that they are not enabled by default is that Haskell has a standard. It's like gcc having -std=c++11, except the opposite way around.
Right, that's exactly it! Almost nobody uses Haskell without un-disabling many features; in doing so, they discover "Real Haskell" and leave behind the less-coherent "Plain Haskell".
However, there is no central understanding (that I've ever seen) of what "Real Haskell" is. Everyone defines their own.
Beauty being in the eye of the beholder, it's thus no wonder that wizened Haskellers come back from the wilderness speaking of a thing of magnificence – for the Haskell they have seen is a Haskell they wrought. They found the extensions that suited them, and unlocked the "Real Haskell" of their dreams.
But for a new adventurer setting out, there is no one charted path to Nirvana; only swirling rumors, each as different as the next. Perhaps all travelers agree that a good bit of String is necessary to carry along, but it ends there.
This isn't to say that Haskell fails – there are many who love it, and its curiosities inspire much exploration in plainer realms – but.... well, you can see that it wouldn't be appealing to someone who wants to understand a system instead of playing a choose-your-own-adventure mystery game ;)
Sometimes I try to do something that seems sensible. The compiler says "you can't do this, you need to turn on -XExtension". I do so. The end. It's really not terribly mysterious or complicated.
Ehh... except the times it thinks I could do something with an extension, but it's not actually something I want to do.
Still, it's very nice to have the starting point, and I agree it's not a bad experience overall.
Edited to add an example:
Prelude> 1 2
<interactive>:1:1: error:
• Non type-variable argument in the constraint: Num (t1 -> t2)
(Use FlexibleContexts to permit this)
• When checking the inferred type
it :: forall t1 t2. (Num t1, Num (t1 -> t2)) => t2
Prelude> :set -XFlexibleContexts
Prelude> 1 2
<interactive>:3:1: error:
• Could not deduce (Num t0)
from the context: (Num t, Num (t -> t1))
bound by the inferred type for ‘it’:
forall t t1. (Num t, Num (t -> t1)) => t1
at <interactive>:3:1-3
The type variable ‘t0’ is ambiguous
• In the ambiguity check for the inferred type for ‘it’
To defer the ambiguity check to use sites, enable AllowAmbiguousTypes
When checking the inferred type
it :: forall t1 t2. (Num t1, Num (t1 -> t2)) => t2
Prelude> :set -XAllowAmbiguousTypes
Prelude> 1 2
<interactive>:5:1: error:
• No instance for (Num (Integer -> ())) arising from a use of ‘it’
(maybe you haven't applied a function to enough arguments?)
• In the first argument of ‘print’, namely ‘it’
In a stmt of an interactive GHCi command: print it
There's nothing wrong with enabling FlexibleContexts, but it's not going to solve your problem. You likely don't want AllowAmbiguousTypes on globally (though it's not as dangerous as it might sound). The final error message you get is the most useful, so going on this little journey isn't terrible, but it could have been short circuited if you took a beat to understand the extension it was asking for, and why it's being asked for in this context.Edited to add:
The problem with the above code, of course, is that I'm trying to apply the function 1 to the value 2, but 1 doesn't represent a function. But it could! Because the way number literals in Haskell are handled is much the same way as OverloadedStrings works - 5 is treated as (fromInteger 5) - and they have type (Num a => a), so that the context gets to pick which numeric type they're interpreted as.
As far as the language is concerned, I could make integer literals represent functions that (say) add that number to an input:
instance Num (Integer -> Integer) where
fromInteger n = (+ n)
...
... but that's not actually what I wanted. Almost certainly, it was a typo.