As much as I want to use Haskell for real, instead of just reading about and toying with it, I think the examples in this article are a perfect illustration of why I find learning Haskell borderline obnoxious at times.
data S a
data R a
newCaps :: (R a ⊸ S a ⊸ IO ()) ⊸ IO ()
send :: S a ⊸ a ⊸ IO ()
receive :: R a ⊸ (a ⊸ IO ()) ⊸ IO ()
In this API, S is a send capability: it gives the ability to send a message. R is a receive capability...
At least here the author reasonably explains the snippet (this article is actually just fine generally), but so much of the Haskell examples online are just like this. Single letters for every type/variable. Why aren't S and R just called SendCapability and ReceiveCapability? It's almost like the Haskell community takes pride in being as obscure as possible, which I think is very possibly true.
It's uncommon to see single-letter datatype names; the general idea being that designing your programs with a data-type orientation should provide the kind of description you're looking for; type variables are very often single letter since they're not very important to understanding (the exception being exceptionally unusual types).
Single letter bindings are not common in production haskell code though there are exceptions where it's very clear using single letter binding names is clear enough and not easily confused, like this for instance:
-- | Produce @Nothing@ if the first argument is @False@; produce
-- @Just a@ if the first argument is @True@.
consMaybe :: Bool -> a -> Maybe a
consMaybe b v = guard b *> pure v
but usually, most production Haskell (at least where I work and write it) looks like this (minus the top-level function documentation and all the surrounding types and imports, which are important for context):
makeKey :: FormatTime t
=> t -- Formattable timestamp, usually obtained with getCurrentTime
-> Text -- AWS region, e.g: us-east-1
-> Text -- AWS service, e.g: s3
-> Text -- AWS signing key protocol, e.g: aws4_request
-> Text -- AWS secret access key
-> ScopedSigningKey
makeKey ctime region service protocol key = ScopedSigningKey scope' sigkey
where
dateStamp = formatTime defaultTimeLocale "%Y%m%d" ctime -- YYYYMMDD
scope' = Data.Text.intercalate "/" [ pack dateStamp, region, service, protocol ]
sign :: ByteArrayAccess ba => ba -> C8.ByteString -> Digest SHA256
sign k m = hmacGetDigest $ hmac k m
kDate = sign (encodeUtf8 $ "AWS4" <> key) (C8.pack dateStamp)
kRegion = sign kDate (encodeUtf8 region)
kService = sign kRegion (encodeUtf8 service)
kSigning = sign kService (encodeUtf8 protocol)
sigkey = decodeUtf8 $ convertToBase Base64 kSigning
To be fair, this article is really pretty "cutting edge" as far as Haskell goes. It describing a new feature they're planning on adding to the Haskell compiler, one that significantly extends the type system in an interesting way. I think it's fair for them to assume comfort and familiarity with Haskell syntax and code when writing this article; their audience is folks who are likely to know about and use somewhat esoteric GHC extensions...
> Why aren't S and R just called SendCapability and ReceiveCapability?
Let's see how it reads if they were?
data SendCapability a
data ReceiveCapability a
newCaps :: (ReceiveCapability a ⊸ SendCapability a ⊸ IO ()) ⊸ IO ()
send :: SendCapability a ⊸ a ⊸ IO ()
receive :: ReceiveCapability a ⊸ (a ⊸ IO ()) ⊸ IO ()
Personally, I find that less readable - most of the space of the line is taken up with the two names, it's harder to keep track of the shape of it (in particular, matching parens), and it doesn't communicate anything more when you'll need to read the accompanying paragraph anyway.
That's not to say S and R are good names in a real program - name length should track scope, and type names have pretty broad scope (global but module-constrained), so they should usually be comparatively long. `SendCap` and `RecvCap` might be a good compromise, in that context. Type variables (like the `a` here) are local to the individual type signature (and, with -XScopedTypeVariables, possibly the accompanying definition) and so should be shorter. One letter may be too extreme - `a` does a fine job conveying "whatever you care to be dealing with" but `msg` might be even better in this particular case.
While I agree that if the names are too long, it becomes difficult to discern the "shape" of the function types, in my mind it's more important to be able to tell what the symbols mean. One-letter names are the bane of my existence...
I agree that a good compromise is to shorten the names as long as they are still easily understandable, exactly like `SendCap` and `RecvCap`.
> Why aren't S and R just called SendCapability and ReceiveCapability
Because each one is used only 3 times, in the same code snippet. Having long JavaStyleNames would make the provided code harder to read, not easier. Additionally, S and R are just placeholder types for this snippet; the whole point is you're supposed to replace them with some "actual" type.
> It's almost like the Haskell community takes pride in being as obscure as possible
Production Haskell code doesn't use 1-letter names like tutorials often do. Tutorials expect you to have a non-zero attention span so you can keep track of a short name across a few lines, but I've never seen this sort of thing used for type constructors in an actual library.
People with a weakly typed/untyped background also often make the mistake of confusing specificity with obscurity; Haskellers use "obscure" terms like "Functor" not because they're trying to confuse you, but because it would be wrong to use a "simpler" term. From the Haskeller perspective, it's better to spend a bit more time learning the correct concept than to save a bit of time by using a crutch intuition that will ultimately fail you.
Specific examples are easier to understand than abstract rules - you need a mathematical mindset to get abstractions at all.
The best way to get a concept across is to list enough examples that the reader can do their own generalization. Just tell me the abstract rule, and I will be mystified.
In math, this is why it's important to do the problem set. But you don't get to assign homework to readers in most places; you just gotta give a lot of memorable examples in the text.
Frankly, just saying that you expect people to pay attention and that's why the names aren't typed out seems lazy. If the snippets are as simple as people state then the names won't ever reach the Java style AbstractionFactoryGeneratorSingletonInterface that are difficult to parse due to the name. If they did end up getting that long that'd be more of an indication that the pieces of your code are doing far too many things, or are too complicated of an abstraction anyway for people to be easily able to understand the snippet. Not specifying what you mean when teaching/demonstrating has no benefits in my eyes
I agree this wouldn't be ok if you were reading implementation from an actual production system - there the semantics matter more - but in this example, I think what the author did is fine. It forces you on not focusing on the semantics but on the shape of types and functions, which is what matters here.
Haskell I've seen often looks a lot like mathematical equations to me, where the tradition is that the notation is terse, with single letter variables (possibly with primes or other modifiers) is the norm.
I've thought that Haskell should consider resurrecting the old convention of putting a commented header at the beginning of their code that explains the single-letter notation in a more friendly way. Then they can have their terse syntax but make things a little friendlier for newcomers.
Javadoc-like comments ("Haddocks") are pretty widely used, and Hackage (the Haskell library index) let's you browse them and so on.
See [0] for a good example of well-commented but idiomatic Haskell (a snippet from the popular Megaparsec parser library), which includes things like:
data ParsecT e s m a
ParsecT e s m a is a parser with
custom data component of error e,
stream type s, underlying monad m
and return type a.
Admitted, that is a monad transformer, but one-character type variables with human-readable docs are both good for newcomers as well as low-friction for people who are used to the library in question.
(Strawman alert.) It is significantly more work to parse (ha) something like
ExceptT fooException (StateT fooWorldState underlyingMonad) a
It's also worth noting that there are conventions around type variable names. If you see an `m`, it's probably a Monad; if you see an `s`, it's probably a stream or a struct or state; `i` is likely being indexed over; the code probably doesn't care what `a` is - the surrounding code can pick it freely. If you need another, advance to the next letter (`b`, `c`, less often `n`, `o`).
Of course, Edward Kmett had a story about winding up with his code telling him `i m a s t a b u`...
f for a functor is also very common, as is k for "continuations", very generally speaking (I think this is a borrowing from Lisps?): one often sees monadic bind defined as
m >>= k = ...
In a somewhat more esoteric vein, I love the use of w for comonads: the rationale is apparently that it looks like an upside-down m.
> Of course, Edward Kmett had a story about winding up with his code telling him `i m a s t a b u`...
Is that an indexed lens ... parameterized over a monadic type? I'd like a link to the video/article.
Certainly `f` for functor. `k` is more for values than types (you usually know a continuation is some kind of (->)) but they'll often return `r`.
> I love the use of w for comonads: the rationale is apparently that it looks like an upside-down m.
That's my understanding, yeah.
> Is that an indexed lens ... parameterized over a monadic type?
Presumably further parameterized by the focus, the source structure, the target structure, the focus again, the transformed focus, and some third structure.
> I'd like a link to the video/article.
Me too :-P I see a couple references in a google search but I'm not sure where I first heard it.
I want to disagree a lot - the first example is way easier to parse then the second. The names tell me approximately the scope the function applies to, what I'm looking for in the code as "relevant", and which bits are doing what.
The second tells me nothing - how is this intended to be used? What scope is it intended to be used in? Why was it created? Where does it fit in the program flow?
There is a certain amount of familiarity expected, sure. But anyone who has read, say, the Monads chapters in Learn You A Haskell (which is about as nonintimidating as a book can get!) should be able to guess what those types - s and so on - mean. It's akin to the mClassMember/THIS_IS_STATIC_AND_PROBABLY_FINAL conventions in Java (don't quote me on that).
In other words, my thesis is that if you aren't familiar with State, both State s and State fooAppStateType are fairly opaque.
In any case, most of the time one works with a specialized instance, for instance, one might have something likr
newtype AppState a =
AppState {
runAppState ::
ReaderT Config
(StateT AppState IO)
a
}
and then there's no problem at all.
If you're trying to understand how a library works when you're not very fluent in the language, that's what the documentation is there for! Any good library (and this is an area where's lots of room for improvement) should document the types it uses and the idioms that it lends itself to, and as one accrues experience, the whole "learning to see common patterns" takes over. The great thing about a helpful compiler of the Rust/Haskell ilk is that you make fewer mistakes in between (modulo occasionally horrific error messages).
And I'm willing to vouch for this not being expert (ha) blindness - I have only recently become comfortable enough with these things to talk about them.
Obviously the solution is to encode success into the type system, such that functions which increase the chance Haskell will become a mainstream programming language fail to type-check.
People keep saying this as if it were meant to be parsed "(avoid success) (at all costs)", but in fact it's supposed to be "avoid (success at all costs)", in other words "principles > popularity".
It certainly inherits a lot from that culture but there's a fast-growing number of industry programmers using Haskell that are bringing more approachable styles to the language; most notably: using partial functions as little as possible, using let bindings more often, using applicative-do, making variables names a bit more descriptive when it makes the waters clearer, using descriptive data type names, documenting the fields of records, documenting the arguments to a function if the types don't make it crystal clear, etc.
I always thought it's the same reason as when doing math on paper starting in elementary school you'd call things x, y, z, h, w, w2... etc. instead of triangle_width... etc.
In math'ish problems naming/algorithm (noise ratio) needs to be kept low, otherwise you can't read through it.
You want to elevate symbols between type/vars in reader's attention (what's happening) as opposed to being detailed in naming (which is less of a problem to recall).
It's the same in other languages, just look at SHA calculation in C or anything else - it's the same, math'ish style (and it's good).
ps. another answer may be that writing code this way uses less ink! :)