Hi all, I finally found a convincing argument and a differentiating property.
Firstly, there are two sorts of abstractions: genericity-like abstractions and interface-like ones. The differences between the two are the following (cite from my materials):
-
Interface-like abstractions. Describe the common behavior of similar domain notions and allow the client code to treat each of them uniformly. Implementations can be substituted in runtime transparently for the client code. Does information hiding and encapsulation. Examples: Java and C# interfaces, Haskell’s Free monads, Service Handle pattern, and usual first-class functions.
-
Genericity-like abstractions. Handle the essence of a domain notion with generic type-level declarations. Providing an implementation means specifying the generic type with a specific one at design time and compile time. Examples: generics, type classes, templates in C++, Haskell’s Foldable, Traversable, Monoid, Semigroup, and so on.
So type classes are mostly genericity-like abstractions, but can somewhat mimic interfaces.
The borderline here lies in the information hiding property. Type classes don’t have this property, at least without additional magic (such as existentials).
Here is the modified code of the cooking machine. I added a possibility for the machine to create random recipes for pizza. Notice that it returns another Free monadic language as a result (PizzaRecipe
):
data CookingMethod next
= MakePizza (PizzaRecipe Pizza) (Pizza -> next)
| MakeSandwich (SandwichRecipe Sandwich) (Sandwich -> next)
| MakeRandomPizzaRecipe (PizzaRecipe Pizza -> next)
Now, we can ask for a random recipe and run it without leaving the client function:
sampleCookingMachine :: CookingMachine [Meal]
sampleCookingMachine = do
pizza <- makePizza myPizza
rndPizzaRecipe <- makeRandomPizzaRecipe -- asking for rnd recipe
rndPizza <- makePizza rndPizzaRecipe -- evaluating
pure [pizza, rndPizza]
The type class based solution needs type classes for recipes that are lacking in the previous examples:
class Monad m => CPizzaRecipe m where
-- TODO
class Monad m => CCookingMachine m where
cmakePizza :: CPizzaRecipe r => r Pizza -> m Pizza
cmakeSandwitch :: CSandwichRecipe r => r Sandwich -> m Sandwich
cmakeRandomPizzaRceipe :: CPizzaRecipe mm => m (mm Pizza)
csampleCookingMachine
:: forall m mm
. CPizzaRecipe mm -- information leaking
=> CCookingMachine m
=> m [Meal]
csampleCookingMachine = do
-- pizza <- cmakePizza (cMakeCirclePizza ThickCrust []) -- how to make this compile?
rndPizzaRecipe :: mm Pizza <- cmakeRandomPizzaRceipe
rndPizza <- cmakePizza rndPizzaRecipe
pure [{-PreparedPizza pizza,-} PreparedPizza rndPizza]
Now we have two problems:
- private information leaking
- that commented-out routine that doesn’t compile
I believe this demonstrates why type classes are not interfaces although they exhibit some of the needed properties.
There are actually additional differences when it comes to substituting the implementations at runtime, and this is where the differences start being significant. One cannot easily substitute instances of type classes at runtime because it’s a type-level mechanism only.
Full example of code