Type classes and multi-methods

#1

From reading the blog post, I see more or less how to define what seems syntactically and semantically equivalent to adding a method to a class I don’t own. I.e., if class Foo is defined elsewhere and f is an instance of Foo, then I can use a type class to define semantics for f.mymethod.

My next question is whether this is limited to the left hand side of the dot? Can I also use a type class to define a binary function such as the following works?

def Foo(a:Int, b:MyClass):MyClass = ...
def Foo(b:MyClass, a:Int): MyClass = ...
def Foo(a1:Int, a2:Int): MyClass = ...
def Foo(b1:MyClass,b2:MyClass): MyClass = ...

such that the following works?

List(1, MyClass(2), 3, MyClass(4), 5, 6).fold(MyClass(0))(Foo)

Or similarly, could I define a var-args version of Foo which I could call as follows:

Foo(1,2,MyClass(3),Foo(1,2),Foo(4,Foo(MyClass(3),4,5,MyClass(6),7,8)))

#2

Scala doesn’t have multi-methods. There’s also no easy and efficient method to emulate them properly (that I know of, at least). The code you’ve presented, i.e.:

def Foo(a:Int, b:MyClass):MyClass = ...
def Foo(b:MyClass, a:Int): MyClass = ...
def Foo(a1:Int, a2:Int): MyClass = ...
def Foo(b1:MyClass,b2:MyClass): MyClass = ...

is an example of overloading and overloading in Scala is static, i.e. it’s done using the statically known type. Therefore following things happen:

object Main extends App {
  def foo(v: AnyRef): Unit =
    println("I've got AnyRef")
  
  def foo(v: String): Unit =
    println("I've got String")

  val value = "abc"

  foo(value: AnyRef) // prints: I've got AnyRef
  foo(value: String) // prints: I've got String
}

Instead of multi-methods you can use pattern matching, fluent interfaces (i.e. overloaded single parameter apply method in a builder) or something else.

#3

@tarsa is right, but to help clarify, when you say something like this:

List(1, MyClass(2), 3, MyClass(4), 5, 6).fold(MyClass(0))(Foo)

the types being passed to Foo are only known at runtime – at compile time, all you have is a List[Any], which generally isn’t useful. Typeclasses work at compile time: all the type information needs to be crystal-clear, so that the compiler knows exactly which version of the typeclass to use at any given point.

#4

As I understand, in Scala 3, the type of this will be List[Int | MyClass] rather than List[Any]

I tried to test this using Scastie, but I couldn’t figure out the format for Scala 3.

How do I write this code in Scastie, Scala 3, so it will display the type in the worksheet?

31

When I select dotty and press Save, I get an error Expected a top-level definition. But when I give a top level definition, it loses worksheet semantics.

#5

Could be – that’s possible, but I have no idea whether the type inference is good enough to puzzle that out, or if that’s even a goal. Explicit union types certainly are, but I don’t know how far they are trying to infer them.

Regardless, though, that doesn’t change my point – any given invocation of Foo in your original example is being presented with a type that isn’t known, at that spot, at compile time. Even if it’s restricted to only valid choices, the compiler still doesn’t know which choice you want. It won’t work.

Keep in mind, when I say that typeclasses help many erasure problems, I mean that it does so by enabling very different programming patterns. But it’s not magic: it works if and only if the compiler knows precisely what type is in use at a given spot. When you smoosh a bunch of differently-typed values together like this, you lose a great deal of information, and that eliminates the compiler’s ability to help you. As @tarsa says, at that point you basically need to use pattern matching to rediscover the types.

I don’t use Scastie much – somebody else will have to speak to how to make that work.

#6

In the mean time, I had an interesting talk with Guillaume Martres (@smarter) about what dotty can and can’t do. It turns out (if I understand correctly) that if List(1,2,MyClass(3),4,5) is used as an argument of a function whose type is declared as List[Int|MyClass], then the type will be inferred as such. However, it will usually be inferred as `List[Any]’, This is because dotty widens types to the lub which is NOT a union.

1 Like
#7

Union types make more sense in explicit declaration instead of inferring.

#8

I’m glad to hear that union types are not inferred, because my guess is that most inferred union types would be the result of bugs. In the same way as inference of Any or AnyRef is usually a bug, except that with Any/AnyRef, it is usually more obvious that it is a bug.

#9

Wow I thought this would work

var i: Int | Long = 5L
println(i+3)

[error] – [E007] Type Mismatch Error: /Users/kanwals/code/skala/src/main/scala/Main.scala:11:14
[error] 11 | println(i+3)
[error] | ^
[error] | Found: Int(3)
[error] | Required: String
[error] one error found
[error] (Compile / compileIncremental) Compilation failed

#10

Would you want this to work:

class Party { def launch: Unit = println(“Let’s have some fun!”) }

class NuclearMissile { def launch: Unit = println(“Doom!”) }

val myParty: Party | NuclearMissile = new NuclearMissile

myParty.launch

#11

you could still make something like:

trait Launcher{def launch:Unit}
class NuclearMissile extends Launcher
class Party extends Launcher
func(new NuclearMissile)

we still have this feature because we trust devs?