15
$\begingroup$

I'm playing with creating a language after doing the LLVM Kaleidoscope tutorial. I've added some basic type checking, and now I want to add some functions that write things to standard output, as must languages do.

Let's say I create functions like (pseudocode):

func print(x: Int) { ... }
func print(x: String) { ... }
func print(x: Double) { ... }

Which I can call like:

print(123)
print("Hello world")
==>
123
Hello world

In languages like C++ that have "overloading", I can simply define the different functions with the same name and different argument types, and it's as if the symbol print is bound to to a function group – an "overload set". And you have to decide during type checking which exact function to call based on the types of the arguments.

The other style of solution seems to be to create a central generic function and then specify in some way the implementations of it, like:

func print(x: T) where T is Printable
Int is Printable { ... }
String is Printable { ... }

There are different strategies here but basically you're saying the type T can be anything that's a member of this defined group of types.

Is this six of one, half dozen of the other? Or are there some advantages to one way or the other?

It seems like the second way is more modern, and I see it in Rust and Haskell, and others. In C++ too, in a way, with templates and concepts.

$\endgroup$
0

1 Answer 1

9
$\begingroup$

These do not need to be exclusive options: Scala has both typeclasses and overloads. Here are two ways to implement println:

def println(x: Int) = ???
def println(x: String) = ???
trait Printable[+A]:
  def println(a: A): Unit
given Printable[Int] with
  def println(i: Int) = ???
given Printable[String] with
  def println(s: String) = ???
def println[A](a: A)(using p: Printable[A]) = p.println(a)
// or:
def println[A: Printable](a: A) = summon[Printable[A]].println(a)

There are advantages and disadvantages to both options.

Competing standards

Every type class which is not in the standard library will be subject to competing standards. foo[A: mylib.Foo] and foo[A: otherlib.Foo] can't exist in the same place unless your language does also have overloads and type resolution strong enough to decide which to use, and provided no type exists that has both mylib.Foo[T] and otherlib.Foo[T] instances. This is somewhat unlikely, assuming those Foos do the same things and some third library might want to provide users of either library the instances for their types.

Locality

Many languages with overloads do not allow third party code to add overloads to existing code. This might mean that library code needs to use different names than the standard library for doing similar things. Typeclasses, by design, avoid such problems.

$\endgroup$
4
  • $\begingroup$ This is helpful, but I'm having trouble understanding the paragraph "Competing Standards", because I'm not sure what the code aaa[B: C] means. I think a little elaboration with a more concrete example of a conflict would help. $\endgroup$
    – Rob N
    May 28, 2023 at 16:54
  • 1
    $\begingroup$ right, sorry, that's the same syntax as the last line of the second Scala example, it basically means that aaa is generic in B, which is required to also have a C instance $\endgroup$
    – RubenVerg
    May 28, 2023 at 19:15
  • 1
    $\begingroup$ @RobN def printable[A: Printable](a: A) = ... is sugar for def printable[A](a: A)(using p: Printable) = p.println(a), where p is basically an instance of the typeclass Printable. The A: Printable thing is a context bound. It's kinda like saying (Printable a) => in Haskell. $\endgroup$
    – user
    May 29, 2023 at 19:57
  • $\begingroup$ I think you meant (using p: Printable[A]) but I get the drift. Thank you to both of you. $\endgroup$
    – Rob N
    May 30, 2023 at 14:30

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .