Is there an idiom to achieve something like Haskell's type classes or Elixir's protocol

It’s probably an overused example, but let’s say I’m writing a unit testing framework. I’d like to have

test("arithmetic")
-> check(3+4)
-> equals(8)

There are two polymorphic calls, which take integers in this example.

Behind the scenes, though, there is more polymorphism going on.

First, the equals check will likely want to delegate to a type-specific equality checker. Second, it equals fails (as it will here), it will need to convert the actual and expected values to strings in order to generate the failure message.

This is achievable for built-in primitive types. But say I want to write tests for my parser, where the AST is represented by a tree of variants.

I’d like to able to say "when unit testing does an equals on an AST node, it ignores the line and column number fields. I’d also like to be able to say “here’s how to format the AST node in a failure message.”

rescript-test does this by having you write your own helpers for each test:

let intEqual = (~message=?, a: int, b: int) =>
  assertion(~message?, ~operator="intEqual", (a, b) => a === b, a, b)

let stringEqual = (~message=?, a: string, b: string) =>
  assertion(~message?, ~operator="stringEqual", (a, b) => a == b, a, b)

This obviously works, but I feel that the level of abstraction is wrong: all I’d really want to do is to define what == means.

In Haskell the test framework would use type classes, and in Elixir it would use protocols. Users of the framework would then just provide implementations of functions such as toStrring and eq, which the framework would apply when needed.

So… is there an idiom for doing this in RS?

Dave

Kind of. ReScript is more explicit than Haskell. One way to do it would be to use module functors. You would end up with code that looks like,

module CheckInt = Check({
  type t = int
  let toString = ...
  let \"==" = ...
})

module CheckString = ...
...
let test1 = {
  open CheckInt
  "arithmetic"
  ->test
  ->check(3 + 4)
  ->equals(8)
}
...
let test2 = {
  open CheckString
  "string ops"
  ->check("a" ++ "")
  ->equals("a")
}

Usually a library that provided this kind of abstraction would provide the convenience modules CheckInt, CheckString ready-made, and let users define their own modules for custom types.

There’s an example of this style in ReScript’s Belt library, check Belt.Set | ReScript API

3 Likes

Thanks for this. I’ve been playing with functors, but I’m not keen on the fact that there doesn’t seem to be any type inference taking place.

Is there any way to make this generic? It has the type information it needs to decide which function to invoke, so is there any way that check(5) could automatically dispatch to CheckInt?

Not really :slight_smile: The type system is inherited from OCaml and this implicit resolution is basically an open research area there right now. There is the modular implicits proposal but that is years away from shipping.

1 Like