A confession of my OOP baggage: using type classes where (G)ADTs suit better

Like many software engineers who started in the early 2000s, learning OOP was the mainstream thing to do. In my specific case, I started as a C++ developer and had years of professional experience. A few years ago, I rediscovered Haskell and rekindled my passion for programming. I want to do my new projects mainly in Haskell.

However, I noticed a repeated and likely anti-pattern in how I do Haskell. And I suspect it is from OOP. I want to share it here and see if it resonates with anyone.

The impetus of software engineering is the maintainability of the code base. OOP design patterns are often based on a design interface in a specific arrangement or through an elaborated class hierarchy, touting that such a code base is more maintainable for reasons A, B, or C.

Such a habit directly influenced me to choose type classes (which are not the same as OOP classes, especially on sub-typings!) more often than needed. Here is my current intuition:

case Q "Are you releasing a library?" of
  False -> A "Use ADT or GADT for your logic"
  True  ->  case Q "Do you expect your users to extend the library's behavior?" of
    False -> A "Use ADT or GADT for your logic"
    True  -> A "Pass a function as a parameter, or use type classes if more convenient"

The main reason is that being able to do pattern matching within your code base makes things way more simpler, and the opposite of using type classes for unknowns can evoke all sorts of demons such as OverlappingInstances, UndecidableIntances, etc.

If you are a library writer, and type classes or some sort of callback by passing “functions” as parameters is inevitable, that’s where I think even OOP is moving away from class hierarchy and subtyping. And I admit that because of the lack of subtyping, I thought it was a limitation when using Haskell.But now I rather think it’s a blessing.

I throw it out here for a discussion. I am happy to hear about your experience, too!

4 Likes

My intuition, I’d say, is the following:

  • Are the functions you’re making for a specific data type that the user can just pick up and use?
    • Yes: Just use existing / make new data types the user can use and functions to work with them. (which could even be higher-kinded to accommodate some custom user-defined types in some cases)
    • No: Then you are probably making functions that will have to work with the user’s own data types?
      • In this case, you probably need type classes to use “stubbed” functions the user can define themselves for the types they’re using.

But as you say, even in the last case, sometimes adding a “function as a parameter” would also be enough. (which is kind of like an anonymous type class method in certain situations, now that I think about it :thinking:)

1 Like