Generalizing number types seems harder than it should be

I ask an LLM how I would solve this problem using type classes, and it spits out this

trait Addable[A]:
  def add(a: A, b: A): A

object Addable:
  // Summoner method to easily retrieve instances
  def apply[A](using instance: Addable[A]): Addable[A] = instance

  // Type class instances for primitive numeric types
  given Addable[Int] with
    def add(a: Int, b: Int): Int = a + b

  given Addable[Double] with
    def add(a: Double, b: Double): Double = a + b

  given Addable[Long] with
    def add(a: Long, b: Long): Long = a + b

  given Addable[Float] with
    def add(a: Float, b: Float): Float = a + b

  given Addable[BigInt] with
    def add(a: BigInt, b: BigInt): BigInt = a + b

  given Addable[BigDecimal] with
    def add(a: BigDecimal, b: BigDecimal): BigDecimal = a + b

// Generic add function
def add[A: Addable](a: A, b: A): A =
  Addable[A].add(a, b)

Not going to lie, my initial impression is that this is pretty bad. So much work for something so simple and consistent. Hope the LLM is wrong.

in it’s basic form, scala’s trait is something similar to java’s interface or class, i.e. you can inherit from scala’s trait just like you can inherit from java’s interface or class. scala’s trait allows for more than java’s interface, i.e. it can have fields and (in scala3) a constructor. that makes it more powerful as you can inherit from multiple scala’s traits at the same time.

if you combine scala’s trait or class with scala’s given or implicit, then you get the type class machinery.

let’s make a simple introduction to type classes, assuming you come from oop:

  • in java there are type java.lang.Comparable and java.util.Comparator. the comparable interface is strictly oop construct, but the comparator is close to a type class.
  • what separates java’s comparator from a real type class is that java compiler doesn’t have the implicit arguments machinery. if you have to explicitly spell out the comparator argument everywhere then it’s not a type class. in scala, you can make the implicits machinery work in variery of ways.

in rust lang, you can turn the rust’s trait into an oop construct if you use Trait object types - The Rust Reference but that’s usualy discouraged (rust lang is striving to use as little oop as possible).

1 Like

it’s mostly right, i.e. this should work and is a correct implementation, but if your goal is to minimize the amount of code, then it could probably be massaged somewhat to reduce the amount of code by half or so.

what you cannot avoid in typeclasses is having separate typeclass instance for every type that doesn’t have an useful supertype. in this case, having supertype java.lang.Number or something like that doesn’t help, so you need separate typeclass instances for every primitive type.

1 Like

i gave a quick shot at trying to make a solution based on reflection, but that doesn’t work.

import scala.language.reflectiveCalls

def add[T <: { def +(other: T): T }: Manifest](a: T, b: T): T =
  a + b

println(add(5, 6))
println(add(2f, 3f))

that doesn’t work and quick googling points to this answer why: Structural types: Structural access not allowed … because it has a parameter type with an unstable erasure - #3 by Odersky

i’ve tried to make metaprogramming based alternative, but i’m still too weak in that regard :slight_smile:

I see, thanks for the explanation.

Where I currently stand now then, - if anyone can correct me please do - is that if Scala supported a way to say “this generic parameter T will be only one of the following types” e.g.

def add[T = Int | Double | Float](a: T, b: T): T =

or “logic or” as @spamegg1 said, it would perfectly solve this problem immediately, once-and-for all at arbitrary scale (you never need to change it if you add more types which implement homogeneous +, whereas the typeclass approach requires a new instance declaration for every new type in your codebase, very exhausting)

So, that’s sort of what I was meaning by “type classes are a workaround for an insufficiency in the language”. The “=” operator I’m describing for Generics is easy to understand, plausible for a language to implement, but apparently not possible to express in the scala. And it would avoid the need for typeclasses entirely.

(I am sure type classes have other huge advantages and are required in other contexts. But this simple scenario of making a math function work with any type of number doesn’t seem like one of them)

Match types is the only one I’m aware of in Scala. But it’s quite limited (as I linked above). (Maybe it could be done with path-dependent types as well? :thinking: )

It’s a much weaker version of the same feature from fully dependently-typed languages like Lean, Idris, Agda etc. But these languages also use type classes (or similar) in one way or another to do it… (and they don’t have subtyping to worry about)

Scala 3 DOT system is a very special (if not unique) one to my knowledge, that tries to mix both worlds and still remain provably sound, but it has to make compromises (deliberate design decision). Keep in mind type inference in general is undecidable, and it’s quite costly performance-wise. (Lean has to do a ton of special tricks to improve type computation performance, even then it can be quite slow)

overall, i think what you want is to achieve something like c++ templates, but the only language that i know that has something like c++ templates is c++ :slight_smile:

c++ templates work by blindly expanding the templates for every parametrization and only then trying to compute the static types for the expanded implementation. that’s not how generics work.

5 Likes

Here it’s working as you wished, in Lean, but it uses the HAdd type class instance. Keep in mind Lean’s type system is way more powerful than Scala’s. It synthesizes, or as people here would call it, “derives”, the type class instance automagically, depending on whatever T is.

Hopefully you begin to understand how difficult this is to do. Without the type class instance, it amounts to mind reading… generic type parameters don’t work the way we intuitively think, because we have human “super powers” of pattern recognition and sensing intention (just by looking at that plus sign).

1 Like

Yes, that is how a solution from scratch would look.
Nothing there is complex, is just long.

In any case, you don’t need to define Addable yourself, it already exists both as Scala Numeric or as cats Semigroup / Monoid.

This means the whole long part of defining the instances was already spelled out for you.
All you need to do is use it.

Typeclasses are really not complex, and are very different from the Monads with regard to “taking forever to understand”.
They are essentially just the adaptor pattern from OOP just with extra help from the compiler.

3 Likes

Static duck typing! That does seem to be a “feature” that other languages haven’t followed the lead of C++ with.

I feel the C++ example is good because it helps to think about what is going on here on the machine with whatever a compiler spits out. a+b might have the same syntax for int, doubles, etc. But they are very different when you think about the machine/assembly language level. You have different instructions that operate on different register banks. This is even more clear if you consider types like Complex that don’t fit into single registers. The static duck typing in C++ means the compiler will try to see if it can make things work, and it spits out different machine code for each operand type. The requirements on the operands are implicit in C++, so the syntactic similarity is all that is needed. In any language where the requirements on T are explicit, something has to define the meaning of the operation.

@SJohn, it seems you find the type class code you gave to be verbose because all the types you provided happen to share the exact same syntax of a+b. But that’s only true for a very limited number of types. How do you expect the compiler to know what Addable means for things like Complex or Vec3? That has to be written somewhere. You happen to be picking types where the definition is at the language/compiler level, but that isn’t true in general.

5 Likes

For some reason, no one has punned on “addled” for the original example.

This sort of dispatch is “normal”, at least in Stockholm.

Scala 2 has “specialization” for a different look at signatures:

➜  ~ scala -J--enable-preview -J--add-exports -Jjdk.jdeps/com.sun.tools.javap=ALL-UNNAMED -nobootcp
Welcome to Scala 2.13.15 (OpenJDK 64-Bit Server VM, Java 23.0.1).
Type in expressions for evaluation. Or try :help.

scala> abstract class C[@specialized A] { def add(x: A, y: A): A }
class C

scala> class D extends C[Int] { override def add(x: Int, y: Int): Int = x + y }
class D

scala> :javap -public D
Compiled from "<console>"
public class $line4.$read$$iw$D extends $line3.$read$$iw$C$mcI$sp {
  public final $line4.$read$$iw $outer;
  public int add(int, int);
  public int add$mcI$sp(int, int);
  public $line4.$read$$iw $line4$$read$$iw$D$$$outer();
  public java.lang.Object add(java.lang.Object, java.lang.Object);
  public $line4.$read$$iw$D($line4.$read$$iw);
}

scala>

Edit: by coincidence, the word of the day is “addlepated”. I didn’t know that eggs can be addled as well as scrambled, just like brains. (via m-w.com)

In this hectic, often confusing world of ours, it’s probably safe to say that even the sharpest thinkers—the wonks and eggheads among us—get a little addlepated from time to time. In fact, the idea of an addlepated egghead makes some etymological sense. Addlepated combines the words addle and pate. While the meaning of the somewhat rare noun pate (“head”) is straightforward, cracking open the adjective addle is where things get interesting. In Old English, the noun adela referred to filth, or to a filthy or foul-smelling place. In Middle English, adela came to be used as an adjective in the term adel eye, meaning “putrid egg.” For its first few centuries of adjectival use, and with various spellings, addle was used strictly for eggs, but in the 16th century it gained a figurative sense that, when applied disparagingly to people’s heads or brains, suggested the diminished or rotten condition of an addle (or addled) egg. Today, addle is often found in combination with words referring to one’s noggin, addlebrained, and addle-headed, and most common of all, addlepated.

1 Like