I’m a little bit concerned about the direction we’re taking with respect to class and instance declarations. It is mainly about readability and intention of the code. Let me first summarise two current issues and state my concerns, after which I’ll introduce a possible way to ease the pain.
Kind annotations and class inheritance
First, since PureScript 0.14, we can add kind annotations to datatypes, newtypes and classes.
When browsing some source code, I stumbled upon this (there are more examples):
class Category :: forall k. (k -> k -> Type) -> Constraint
class Semigroupoid a <= Category a where
identity :: forall t. a t t
I don’t know if I’m alone in this, but reading this code from top to bottom boggles my mind. It goes like this:
- “Ah, we’re declaring a
Category
class.” - “Oh wait! It’s the
Semigroupoid
class!” - “Oh sorry.
Semigroupoid
is the superclass of theCategory
class.”
I know, writing down the superclass before the new class is the way we always did it,
is the way Haskell does it for some decades, but…
- Now that we have kind annotations, they don’t align with the declaration.
- It makes me wonder what we are declaring here.
- It remembers me of reading C, where type annatations read backwards.
Instance declarations and forall
Second, a PR has been merged which removes of explicit instance names. The compiler can generate them. Wonderful! We’re also discussion the addition of forall
to instance
declarations, which helps avoiding problems with type variable scoping. Hurray! I guess the end result will be something like this:
instance forall a b. (Show a, Show b) => Show (Tuple a b) where ...
Some observations:
-
We’re repeating ourselves. We have to state that we’re declaring an instance for all
a
andb
, but the part after the=>
implicitly states the same thing. -
Just as with class declarations, it reads backwards. We’re declaring an instance of
Show
onTuple
s, and for that we need two otherShow
constraints. (But again, we did this, and Haskell did this, for decades now…) -
The introduction of an explicit
forall
for instances, also raises the question: Shouldn’t we treat class declarations the same and add an explicitforall
there too to aid type variable scoping?class Category :: forall k. (k -> k -> Type) -> Constraint class forall a. Semigroupoid a <= Category a where ...
Consistency with type
and data
declarations
For type
and data
declarations we do not need an explicit forall
, as the type variables are introduced together with the data declaration. When, for example, we write data Tuple a b = ...
, it is clear that we are introducing type variables a
and b
, and we’re only allowed to use a
and b
after the equals sign. We don’t have to repeat ourselves! Why shouldn’t we do the same thing with class
and instance
declarations?
So, what if we make the experience of entity declaration simpler and more uniform by starting every declaration with the name we are declaring, together with the variables that can be used in that declaration.
So:
-
type
anddata
declarations stay untouched; -
class
declarations start with the class we’re declaring together with their type variables, followed by their superclasses which can use the type variables introduced earlier
(as is custom in I think every language with some kind of oo-class or interface/protocol/trait inheritance except Haskell and Idris); - do the same for
instance
declarations; - drop the explicit
forall
.
data Tuple a b = ...
type Usually a = ...
class Category :: forall k. (k -> k -> Type) -> Constraint
class Category a | Semigroupoid a where ...
instance Show (Tuple a b) | Show a, Show b where ...
I’m curious if other people’s minds have to do the same yoga when reading current class and instance declarations, and if above proposal would enhance DX.