spebbe / dartz

Functional programming in Dart
MIT License
749 stars 60 forks source link

[Breaking change] Making TypeClasses a mixin instead of a class #83

Closed KristianBalaj closed 2 years ago

KristianBalaj commented 2 years ago

Let's have a class A that is a Monoid:

class A extends Monoid<A> {
  @override
  A append(A a1, A a2) {
    ...
  }

  @override
  A zero() {
    ...
  }
}

Problem

Imagine for example that class A is a huge data class and I want to subclass it with Equatable to solve the equality logic for me. After that I can't make the class A to be a Monoid. I could make class A extends Equatable implements Monoid<A> but after that I need to override 3 methods, the appendC that shouldn't be necessary to override for Monoid.

Making a Monoid a mixin would solve this problem as following class A extends Equatable with Monoid<A> and then there are the 2 Monoid methods to override as expected.

This should be done for all the other typeclasses as well.

Notice

However, this would be a breaking change, breaking all the existing code, where developers would need to replace extends with the with keyword.

spebbe commented 2 years ago

Hi @KristianBalaj!

Interesting proposal! Could you give an example of what kind of code you would want to be able to write? I'm not sure I understand what you are looking for... Using the methods on Monoid as instance methods on custom types would probably be a bit awkward:

  // odd to have to instantiate an A just to be able to get to empty()
  final anEmptyA = (A()).empty();

  // creates a new value based on secondA and thirdA, but the value of firstA is irrelevant and ignored?
  final fourthA = firstA.append(secondA, thirdA);

The API for Monoid is designed for "standalone" use ("type X forms a monoid" rather than "type X is a monoid") and also allows for types to form monoids (and semigroups) in several ways, like in:

  final nums = ilist<num>([1,2,3,4]);

  print(nums.concatenate(NumSumMi));     // => 10
  print(nums.concatenate(NumProductMi)); // => 24
  print(nums.concatenateO(NumMaxSi));    // => Some(4)

Several type classes in dartz have accompanying Ops types (Foldable/FoldableOps, Functor/FunctorOps, etc) that are intended to provide "type X is a ..." semantics to other types. These provide OO-friendly versions of the "standalone" methods provided by their respective type classes. Most of them are sadly not usable as mixins, since Dart mixins are currently restricted to inheriting from Object only. "Being" MonadOps implies also "being" ApplicativeOps and FunctorOps. Similarly, "being" MonoidOps would naturally imply also "being" SemigroupOps. No MonoidOps currently exists and I'm not sure how useful it would be in practice, but maybe that's what you are looking for?

KristianBalaj commented 2 years ago

Example

Well a simple example would be having a class BankAccount that has fields like balance, transactions, etc. Inheriting BankAccount from any other class makes sense, e.g. Equatable.

Making the BankAccount a monoid makes sense here, where the append has semantics of BankAccount merge for example.

And let's take an example where I want to compose a general function taking Lists of Monoids and the result of this function would be a Monoid.

Sumup

This is one of the many possible examples why making a Monoid by inheritance is not a good solution in my opinion.

But since you said that mixins can't be extended what I didn't know initially, then this issue doesn't probably have a proper solution in Dart language.

Also, other problem is that you cannot call the Monoid.empty method without an instance of the object what makes it impossible to compose such a general function as mentioned in the Example (in case the parameter of the function is an empty List. In that case, you have just the generic parameter T extends Monoid<T> and an empty List<T> but you can't return the empty since u don't have an instance of T. Maybe it would work with some rape using reflection - instantiating object from type and calling empty on it, but it is nasty).