
Haskell's Type Classes: We Can Do Better - andars
http://degoes.net/articles/principled-typeclasses/
======
dwenzek
An alternative approach is under development [1] for OCaml : modular implicits
[2]. This is a mix of ideas from SML/OCaml modules, Scala implicits and
Haskell type classes.

More than one alternative module implementation may be provided for a module
signature, and the actual implementation to be used is chosen on the implicit
modules available in the current context. This results into an error if the
implicit module cannot be chosen without ambiguity.

[1] [https://github.com/ocamllabs/ocaml-modular-
implicits](https://github.com/ocamllabs/ocaml-modular-implicits)

[2] [http://www.lpw25.net/ml2014.pdf](http://www.lpw25.net/ml2014.pdf)

~~~
Kutta
(very similar systems are already implemented in Agda and Coq)

------
jules
The requirement to be globally unambiguous is the #1 wart of type classes.
Rather than enforcing it with the compiler it would be better to make that
restriction unnecessary. It should be possible to define a type class instance
local to a module. Locally unambiguous yes, globally unambiguous no.

~~~
Kutta
That's not good. We can put an instance into a runtime box, pass it to another
module, and now there are potentially two different instances to choose from
in the other module. We'd have to ban moving around instances in runtime data
if we are to preserve class coherence, which is unsatisfactory.

~~~
jules
> and now there are potentially two different instances to choose from in the
> other module [...] to preserve _class coherence_

I know the standard arguments for class coherence (Set etc.), but I haven't
found any of them convincing. Relying on two values coming from two different
places to be the same indicates that the design of the code is wrong. It's
like writing a function f x y but you must only ever call it with identical
arguments x=y. The right thing to do then is to give f only one argument to
begin with.

~~~
Kutta
I consider coherence seriously mainly because Edward Kmett supports it, and he
has immense experience regarding the relative merits of coherent vs.
incoherent classes in practical development. One core Kmett argument that I
subscribe to is that 95% percent of the time there is no newtype noise needed,
and in that case coherence is very nice, since one doesn't have to think about
class hierarchies (which can get very complex, with many alternate instance
derivations) as far as the code typechecks.

But aside of that, I'm not all that convinced of the importance of coherence
in a dependent setting either! My intuition is that types should encode all
the properties that we'd like to be able to __use __in instances. In other
words, even if we don 't get coherence, we can use types to constrain the
implementation of instances as much as we like.

Highly ambiguous instance resolution (like a bunch of Monoid Int instances
lying around) is a bit of a pain though, and there could be some "using
instance" feature/syntax that locally throws out all instances except one.

Additionally, as of now there are no large production code bases that use Coq
style type classes in a proper dependent language. There are very few sizable
applications in dependent languages to begin with. So we don't know a lot
about the practical caveats of that design, while there is a lot of experience
and know-how about Haskell-style classes, which is one reason why we should at
least consider salvaging coherence in future languages.

Generally, I think that type classes are a rather ugly and haphazard
construction compared to the succinct elegance of the type theories of core
languages; still, eventually type classes worm their way into languages (Agda,
Coq) because people really like them and like to use them, so a language
designer should consider them. I wish there was an elegant/general formal
treatment that captures the essential features, in any case.

------
dkarapetyan
Has anyone here shipped anything production worthy with dependently typed
languages? I'm serious. There are like 10 PhDs in France working on a
certified compiler toolchain for C and that's it. Meanwhile Go and JavaScript
are taking over the world and they're anything but dependently typed. Maybe
there's a reason for that? Maybe the reason is that after a certain point the
validation overhead of advanced type systems is just not worth it.

Yes, I know the article isn't about dependent types per se but many of the
features he wants get pretty close to dependent types since that is pretty
much the state of the art when it comes to advanced type systems with lawful
abstractions and whatnot.

~~~
incepted
Dependently type languages are wonderful in theory and a royal pain in the
butt in practice. It's a great example of "devil in the details" kind of
thing.

For example, let's say you define a type of list of size n. Great. You can
only assign list of the same size and you can only append an element of the
type "n" to a type "n-1" or the compiler will complain. Now you can even prove
that your program is formally correct. What's not to like?

And then, reality knocks at your door.

How do you instantiate such a type? It needs to be instantiated right away
with n elements or your program won't compile. But these elements are coming
from the runtime, say, a database. How the hell does this work?

Dependently typed languages that go just a step beyond prototypes quickly
realize how crippling this approach is and they will give you all kinds of
escape hatches to weaken the type system just so you can use the darn thing.
But it's futile, because at the end of the day, a program is a living thing
that relies on side effects and external inputs, and sophisticated systems are
just not designed to cope with human fuzziness.

The reason why dependently typed systems never became popular is not because
the world hasn't realized yet how awesome they are: it's because they just
aren't a good fit to solve real problems.

~~~
clhodapp
Your "I-can't-work-with-this-unsized-list-because-I-need-to-pass-it-to-
something-that-takes-a-sized-list" problem seems like something that one might
_think_ would be a problem if one understood advanced types on a conceptual or
theoretical level but not how to use them in real-world code. It's actually
pretty easy to deal with cases like this... At the end of the day, you just
end up having to code for both the case where the list matches the size
requirements of the code you want to call and the case where it doesn't. For
example, you could pass the unsized list to a function which performs
validation of its actual runtime size, returning, e.g., an `Option[List[Int]
@@ MinSize[_5]]` (if I may use Scala-speak). You'd then have to handle both
the case where the `Option` is a `Some` and the case where it's a `None`.
Ultimately, it ends up being extremely similar to checked exceptions: you are
simply forced to write code to handle all cases, rather than writing code that
allows some of them to blow up at runtime (while telling yourself "that case
could never happen in the real world" or "that's an edge case, we'll just
ignore it").

~~~
incepted
> At the end of the day, you just end up having to code for both the case
> where the list matches the size requirements of the code you want to call
> and the case where it doesn't.

I don't follow. If your list is not the right size, your program doesn't
compile, period.

If it does, you're not using a dependently typed language.

Am I missing something?

~~~
pathsjs
It depends on the language. For instance in Scala you can simulate collections
of fixed size as in
[https://github.com/milessabin/shapeless/blob/master/core/src...](https://github.com/milessabin/shapeless/blob/master/core/src/main/scala/shapeless/sized.scala)

Then, at runtime, you can cast an unsized list to a sized list giving - say -
a function List[A] => Option[Sized[List[A], N5]] that will fail if the list is
not of size 5. From then on you can go on letting the compiler keep track of
lengths.

Of course, this does not work at all if you have no clue what size your input
will have

~~~
clhodapp
I believe that you don't need to know what size your _input_ will have.
Rather, you need to know what size _the thing you will be calling_ will need,
right?

------
joseraul
Note the weakness of the Java solution. Since its interfaces offer no support
for virtual contructors (hence the design pattern), you have to instanciate a
factory before you can get an empty value.

~~~
kushti
Possible now with Java 8 default methods.

------
pcwalton
It's interesting to contrast this with the attitude in the D community (or at
least Andrei and Walter), which has also come to the conclusion that
typeclasses are a bad idea–but because they're _too_ strongly typed rather
than not strongly typed enough, and the solution is to throw them out in favor
of dynamic "typing" at the type level through ad-hoc templates.

My two cents is that this is just like every other static vs. dynamic typing
debate, and it's a deep tradeoff. I've been pretty pleased with typeclasses as
an easy-to-use sweet spot between complexity/mental overhead and safety. But
I'm certainly willing to believe we can do better, of course.

------
jonsterling
There are a number of good points here, but I am surprised by the fact that
John wants a global coherence check, since in our conversations he has always
insisted on the primacy of _local reasoning_.

------
wyager
I think the article mixes up "type" and "newtype". "newtype Email = String"
doesn't make sense.

~~~
gizmo686
"type" wouldn't work in this context, as it is literally just an alias, so the
compiler cannot use to determine what instance to use. In contrast, newtype
actually creates a new type, that acts as a wrapper around the old type. I
believe what the author meant to say was "newtype Email = Email String".

------
Kutta
This post engages in plenty of armchair speculation, and is overall horribly
vague, or just nonsense ("addOneTwo works on any type that’s provably
isomorphic to Integer"). It's more like a plea for all things that seem
intuitively nice and good, mashed together without any deep consideration for
whether they can be realized consistently. Type classes are still an open
research topic, especially when combined with dependent types. From the top of
my head:

Class coherence is the property that any two instances of the same types are
equal. It's a strong guarantee that boosts the robustness and usability of
type classes significantly. Scala doesn't have it, instead it has a arcane set
of rules that determine the resolution of implicits in the face of ambiguity,
and it's a huge pain for more complicated use cases.

Named instances are incompatible with coherence. We could just put one named
instance into a runtime object, and voila, it's indistinguishable from any
other instance. We could remedy this by indexing the type of an instance with
a label, which isn't better than choosing instances with newtype wrappers.

Similarly, local instances and explicit instance application imply
incoherence.

Also, laws for classes are an already existing feature in Idris, Agda and Coq.
The problem is that laws for anything are by far best realized via dependent
types, and dependent types have a nontrivial interaction with classes that is
an open problem. With dependent types, checking whether two instance heads are
different could be just undecidable, depending on what constructs one allows
there.

It's a possibility to neuter the language fragment that goes into instance
heads, so that propositional equality becomes decidable over it - in a way
Haskell already does this, by disallowing type families in instances. But in a
dependent language it could be rather awkward that a fair amount of useful
types cannot be made instances.

Alternatively, one could require proofs from the __programmer __in each
instance that the instance head is propositionally different from all other
instance heads in scope. But this would also require much thought, and I haven
't given it yet.

Also, if our classes are actually coherent, we would like to have an internal
proof of this property (as opposed to in current Haskell where we just sort of
know that this is the case), but I have no idea how to do this best. Maybe the
instance resolution procedure could be modeled internally in a strong enough
language, and coherence could be given as a theorem, and there could be some
syntactic sugar for code with classes.

Or we could have both coherent and incoherent classes, each explicitly marked
as such. Or we could have coherent classes by __proving __that a certain class
is necessarily coherent, independently from any instance resolution procedure.
There are many such classes, but not nearly all that we frequently use, and
some classes are coherent but we can 't prove it in currently implemented type
theories because the proof relies on parametricity (Functor is such a class).

~~~
oggy
Thanks for the informative post. Could you please clarify a couple of things
for me?

> Named instances are incompatible with coherence. We could > just put one
> named instance into a runtime object

Could you give an example of what you mean in, e.g., Haskell (assuming some
fictional facility for named instances existed)?

> With dependent types, checking whether two instance heads > are different

What are instance heads?

~~~
Kutta
> instance heads

Instance heads are the types in the instance declaration that the compiler
dispatches on, for example in `instance Show Int` the head is `Int`.

> named instances

You can see an example for named instances in Idris (very Haskell-like syntax)
here
([http://idris.readthedocs.org/en/latest/tutorial/classes.html...](http://idris.readthedocs.org/en/latest/tutorial/classes.html#named-
instances)). We could put named instances into runtime objects (example in
Haskell: `data ShowInstance a = Show a => Instance` which does exactly this),
thereby forgetting the name, which could cause ambiguities down the line. But
even without instances in runtime boxes, different instances for the same type
necessitate that there are some rules for instance resolution that a
programmer has to consider. For example, if we have two monoid instances for
`Int`, one with addition and one with multiplication, we somehow need to make
sure that the monoid operations mean what we want them to mean, so that we
don't get unexpectedly addition instead of multiplication.

------
eru
There's some template Haskell magic that generates QuickCheck properties for
the typeclass laws for your instances for you. That's taking care of some of
the article's worries.

~~~
nicolast
No need for TH, actually. Write a function which takes a `proxy a` and returns
e.g. a Tasty `TestTree`: `testMyClassLaws :: proxy a -> TestTree`. Then in
your test module, add something like `testGroup "MyClassLaws for MyType" $
testMyClassLaws (Proxy :: Proxy MyType)` somewhere in your list of tests.

------
joostdevries
John de Goes mentions at the start of his article how Scala implements type
classes without an extra language construct. But he doesn't show what his
example would look like in Scala. So I wrote that out for my own amusement.

You can find the gist here. [https://gist.github.com/joost-de-
vries/949fbea6345fec1ac4ef](https://gist.github.com/joost-de-
vries/949fbea6345fec1ac4ef)

