Type classes vs interfaces

What are they missing?

2 Likes

Interfaces should allow interfacing both input parameters and output parameters.

myFunc(input: IContraption): IResult

Type classes basically can’t do this. They simply don’t have a runtime semantics an interface mechanism should have.

I guess you’d want existentials like this:

myFunc :: forall a. Contraption a => a -> exists b. Result b /\ b

(using ASCII-ified notation from An Existential Crisis Resolved)

Isn’t interface an OOP concept which should only be used as a last resort ?

I really don’t. I’ve researched this topic including existentials, and everything I found about it was just inappropritate

No. Interfaces is a universal concept of engineering (not only in software).

1 Like

I’m just trying to understand what you mean. I don’t know enough about interfaces to understand what the difference is between your example and my example.

I’m afraid, any explanation will be just words (as it happened in the topic here: C++ 20’s concepts are typeclasses and make object-based polymorphism obsolete - shockingly!).

My general advice to all haskellers is to practice more languages, OOP included, and broaden their horizons. The notion of a programming interface is super important.

1 Like

Ah, I see, because you want to be able to do something like

handleError :: ExceptT String App a -> App a

which doesn’t translate directly to MTL style because if you try something like

handleError :: (App m, Error String m, App m') => m a -> m' a

then it doesn’t work, and if you try something like

handleError :: App m => ExceptT String m a -> m a

then it’s too concrete?

That’s what I thought too, that’s why I suggested first-class existentials:

handleError :: (App m, Error String m) => m a -> exists m'. App m' /\ m' a

Or with some more impractical manual work you can do it in Haskell today:

data SomeApp a = forall m. App m => SomeApp (m a)

handleError :: (App m, Error String m) => m a -> SomeApp a

But apparently, that’s not it.

This is the right direction of reasoning. However, the problem is even more serious. I want to encapsulate the result completely so that the client code could work with it abstractly, and I don’t really want to carry all those type-level bits here and there (because it breaks the encapsulation and brings a lot of boilerplate).

So interfaces allow to have runtime polymorphism for input and output values. Type classes won’t allow that. Existentials do something, but yet, it is a crutch that doesn’t work as needed

1 Like

I think it would help us if you could elaborate on why. I have not yet found a programming interface that polymorphic lambda calculus cannot represent so I would welcome being enlightened by you!

1 Like

I fully agree with this. Type classes introduce a lot of complexity in the types. I’ve also seen this argument in Edward Yang’s thesis on Backpack:

From a code perspective, type class parametric code is often harder to use than monomorphic code. For an inexperienced Haskeller, the proliferation of constraints and type parameters in the type signatures of functions can make an otherwise straightforward API impenetrable:

(=~) :: (RegexMaker Regex CompOption ExecOption source,
         RegexContext Regex source1 target)
     => source1 -> source -> target -- from regex-posix-0.95.2

Furthermore, type classes work best when exactly a single type parameter is involved in instance resolution. If there aren’t any type parameters, you must introduce a proxy type to drive instance resolution; if there are multiple parameters [5], you often need to resolve ambiguity by introducing functional dependencies [12] or replacing parameters with associated types [24].

Edit: It was Yang’s thesis, not Kilpatrick’s.

1 Like

I’ll see what I can do; it’s just not easy to explain. I have an article in Russian about why type classes and existentials can’t represent the notion of interface, but it’s outdated (year 2012?). Maybe I’ll write another one.

For now, I have an example that I feel should lead to the answer (but not completely sure - this needs to be researched). In my article Functional Declarative Design: A Comprehensive Methodology for Statically-Typed Functional Programming Languages, I have this task:

  • There are two recipe eDSLs for making pizzas and sandwiches
  • There is a cooking machine that can cook pizzas and sandwiches using these recipes. You give it a recipe and get a meal back.
  • The cooking machine should be also able to cook a random meal not known upfront (pizza or sandwich). The meal type should encapsulate what you’re getting.

This task requires a return type polymorphism. It’s pretty easy (and clean!) to do it with Free monads. But FT/mtl/existentials will definitely require a lot of unwanted bits if even be able to solve this.

data CookingMethod next
 = MakePizza (PizzaRecipe Pizza) (Pizza -> next)
 | MakeSandwich (SandwichRecipe Sandwich) (Sandwich -> next)
 | MakeRandomMeal (CookingMachine Meal -> next)

type CookingMachine a = Free CookingMethod a

sampleCookingMachine :: CookingMachine [Meal]
sampleCookingMachine = do
 sandwich   <- makeSandwich mySandwich
 randomMeal <- makeRandomMeal
 pure [sandwich, randomMeal]

That’ look for me like OOP shoe-horned into FP.

I see, so the key element here is something to do with CookingMethod and CookingMachine depending on each other?

It is not. It is a usual approach of information hiding and encapsulation.

Look, this anti-OOP bias makes you lack a lot of design tools and ideas. OOP is just another implementation of these tools and ideas, but the ideas themselves are more high-level, general, and universal.

I think your comment would be more helpful if you could explain why it’s bad to want (this aspect of) OOP. After all, if OOP is a good thing then shoehorning into FP is a good thing! And the fact that it needs to be shoehorned is a weakness of FP.

I’ll carefully answer “yes”, but again, I would first to develop my position better to answer definitely

1 Like

I’m curious. What difference does it make using

data CookingMethod next
 = MakePizza (PizzaRecipe Pizza) (Pizza -> next)
 | MakeSandwich (SandwichRecipe Sandwich) (Sandwich -> next)
 | MakeRandomMeal (CookingMachine Meal -> next)

type CookingMachine a = Free CookingMethod a

in preference to

data CookingMethod' next
 = MakePizza (PizzaRecipe Pizza) (Pizza -> next)
 | MakeSandwich (SandwichRecipe Sandwich) (Sandwich -> next)

type CookingMachine' a = Free CookingMethod' a

data CookingMethod next
 = MakePizza (PizzaRecipe Pizza) (Pizza -> next)
 | MakeSandwich (SandwichRecipe Sandwich) (Sandwich -> next)
 | MakeRandomMeal (CookingMachine' Meal -> next)

type CookingMachine a = Free CookingMethod a

Does the former have some important property that the latter does not have?