Stream: beginners

Topic: Type based pattern matching for operator overloading


view this post on Zulip Pax (Jun 13 2026 at 22:40):

Hey everyone, I just picked up Roc the other day to try it out. I was trying to implement a little Vector2 type today and got stuck trying to implement its plus operator so that I could add vectors together as well as add a scalar to a vector.

I'm trying to use pattern matching to define this behavior, but I'm getting a lot of errors from the compiler and I haven't managed to get a working version yet. Anyone here have tips on how to get this sort of thing to work? Or am I trying to do something that isn't advised?

Vector2 := {
    x : I64,
    y : I64,
}.{
    new : I64, I64 -> Vector2
    new = |x, y| { x: x, y: y }

    zero : Vector2
    zero = { x: 0, y: 0 }

    plus : Vector2, [Vector2, I64] -> Vector2
    plus = |a, b|
        match b {
            { x, y } => { x: a.x + x, y: a.y + y }
            I64 => { x: a.x + b, y: a.y + b }
        }
        # I also tried this
        # match b {
        #   Vector2 => { x: a.x + b.x, y: a.y + b.y }
        #   I64 => { x: a.x + b, y: a.y + b }
        # }
}

# Succeeds
expect Vector2.zero == { x: 0, y: 0 }
expect Vector2.new(1, 2) == { x: 1, y: 2 }

# Fails
expect Vector2.zero + 1 == { x: 1, y: 1 }
expect Vector2.zero + Vector2.new(1, 2) == { x: 1, y: 2 }

Thanks for any help you can give

view this post on Zulip Richard Feldman (Jun 13 2026 at 22:54):

great question!

view this post on Zulip Richard Feldman (Jun 13 2026 at 22:57):

so for this particular test case, what you can do is give Vector2 a from_numeral method - there's a (WIP) langref entry on this https://github.com/roc-lang/roc/blob/f081a87b5fd74ae8de3a1df952da4e0439236499/docs/langref/numbers.md#custom-number-types

view this post on Zulip Richard Feldman (Jun 13 2026 at 22:57):

so when you do that, 1 can become a Vector2, and then Vector2.zero + 1 can Just Work because 1 can become a vector

view this post on Zulip Richard Feldman (Jun 13 2026 at 22:59):

unfortunately that doesn't work in the general case

view this post on Zulip Richard Feldman (Jun 13 2026 at 23:00):

because if you implemented from_numeral in a way that would work for addition/subtraction, it wouldn't work the way you wanted for multiplication, and vice versa

view this post on Zulip Richard Feldman (Jun 13 2026 at 23:04):

there's the same problem with something like this:

plus : Vector2, other -> Vector2 where [
    other.to_vec2 : other -> Vector2
]

so that's a valid Roc type, but even if you gave I64 a to_vec2 (which it doesn't have, and there's no way to add methods to a type after its original definition) it would still run into the problem of "one size does not fit all"

view this post on Zulip Richard Feldman (Jun 13 2026 at 23:07):

you could go a step further and do something like this:

plus : Vector2, other -> Vector2 where [
    other.plus_vec2 : other, Vector2 -> Vector2
]

basically "if the other type knows how to add itself to a Vector2, then we can just go ask it how to do that" - that would actually work, with the problem that you can't extend I64 to have a plus_vec2 method

view this post on Zulip Richard Feldman (Jun 13 2026 at 23:11):

so that would work, but "you can add methods to something after the fact" is intentionally something I don't want to add to the language because it causes all sorts of problems

view this post on Zulip Richard Feldman (Jun 13 2026 at 23:12):

all that said, I think vector-scalar operations are a case I'd like to figure out how to make work nicely, possibly by having builtin vector types

view this post on Zulip Richard Feldman (Jun 13 2026 at 23:12):

there are similar questions with matrices

view this post on Zulip Richard Feldman (Jun 13 2026 at 23:13):

however, I don't know what the right design would be for those yet :sweat_smile:

view this post on Zulip Richard Feldman (Jun 13 2026 at 23:14):

I'm curious what other languages you've used this sort of thing in!

view this post on Zulip Pax (Jun 13 2026 at 23:18):

Thanks for the detail! I'm used to doing this sort of thing in python using dunder methods or in lua with metatables

view this post on Zulip Pax (Jun 13 2026 at 23:21):

Thought I'd try doing something similar here in Roc, but from what I gather it seems like the pattern matching I'm trying here is entirely the wrong direction to go in? It is making me pretty curious about how to go about generics given what you've mentioned here

view this post on Zulip Pax (Jun 13 2026 at 23:22):

I'm having a lot of fun reading up on Roc and playing around, but definitely still wrapping my head around things

view this post on Zulip Richard Feldman (Jun 13 2026 at 23:24):

yeah so "either this concrete type or this other type" is sometimes called a union type in other language, and by design Roc doesn't support union types (they have nonobvious downsides!)

view this post on Zulip Richard Feldman (Jun 13 2026 at 23:24):

so the short version is that there are lots of things you can do in Roc, but "this argument is exactly this one type or exactly this other type" is not one of them :smile:

view this post on Zulip Richard Feldman (Jun 13 2026 at 23:25):

that said, you _can_ do things like "this argument can be any type that has this particular method"

view this post on Zulip Richard Feldman (Jun 13 2026 at 23:25):

or set of methods

view this post on Zulip Richard Feldman (Jun 13 2026 at 23:25):

or "any type at all"

view this post on Zulip Richard Feldman (Jun 13 2026 at 23:25):

just not "exactly this type or exactly this other type"

view this post on Zulip Richard Feldman (Jun 13 2026 at 23:26):

now you could do

plus : Vector2, [Vec(Vector2), Scalar(I64)] -> Vector2

...but then you'd have to label which was which when calling it:

expect Vector2.zero + Scalar(1) == { x: 1, y: 1 }
expect Vector2.zero + Vec(Vector2.new(1, 2)) == { x: 1, y: 2 }

view this post on Zulip Richard Feldman (Jun 13 2026 at 23:27):

which obviously is not ideal :smile:

view this post on Zulip Pax (Jun 13 2026 at 23:33):

Haha yeah I had thought about that but like you say it's not very ergonomic...

I had initially tried specifying b to be any type, but I wasn't really able to work that one out either, and looking back I'm not sure it would end up being all that great either

view this post on Zulip Aurélien Geron (Jun 14 2026 at 00:58):

A few thoughts. I quite like Zig's "no magic" stance. In C++, a simple-looking expressing like v + 1 can hide automatic conversions, constructions, etc., and sometimes it's not the one you expect, it can be hard to maintain and debug. There's a lot of value in keeping things simple, even if it means having a bit more verbose code sometimes.

On the other hand, when implemented correctly, C++ can offer very expressive and powerful code (especially in conjunction with templates): for example adding three matrices A + B + C can be implemented in C++ in a way that avoids materializing the intermediate matrix A + B. TensorFlow achieves a similar result very differently: when you call A + B + C, the code is traced (or parsed) and it builds a computation graph which it then compiles into optimized GPU code.

Side-note: in TensorFlow you can add a TF tensor t and a NumPy array a, in either direction (t + a or a + t), and the result will be a TF tensor, even though NumPy doesn't know about TensorFlow. This is achieved through a priority level: a + t calls a.__add__(t) which sees that t.__array_priority__ is defined and is higher than a.__array_priority__ so it calls t.__radd__(a) to delegate the addition to TensorFlow.

view this post on Zulip Pax (Jun 14 2026 at 01:38):

Yeah that's largely where my experience with this sort of thing is coming from. I use Pytorch quite a bit and really enjoy its ergonomics. It makes complex math far easier to describe in code than it would be without the overloads

view this post on Zulip Richard Feldman (Jun 14 2026 at 02:12):

I have some design sketches for how things like this could work, but they'd realistically require having vector and probably matrix in the stdlib so the builtin number types can be aware of them

view this post on Zulip Richard Feldman (Jun 14 2026 at 02:12):

which I think is totally reasonable

view this post on Zulip Richard Feldman (Jun 14 2026 at 02:13):

one tricky one is matrix multiplication - I think that one would need to have a different operator than *

view this post on Zulip Richard Feldman (Jun 14 2026 at 02:13):

is that a big deal ergonomically?

view this post on Zulip Aurélien Geron (Jun 14 2026 at 02:24):

Python uses @ for matrix multiplication. I like it.

view this post on Zulip Aurélien Geron (Jun 14 2026 at 02:25):

Julia uses * for matrix multiplication and .* for item-wise multiplication

view this post on Zulip Richard Feldman (Jun 14 2026 at 02:25):

yeah we could do the reverse

view this post on Zulip Richard Feldman (Jun 14 2026 at 02:26):

the problem is that right now we rely on * having a constraint where the type of the value before the * and the return type have to be the same

view this post on Zulip Richard Feldman (Jun 14 2026 at 02:26):

otherwise you can write x * 2 but you couldn't write 2 * x, which would be ridiculous :stuck_out_tongue:

view this post on Zulip Richard Feldman (Jun 14 2026 at 02:26):

(other languages have different ways of solving this, but we use this way because it's the way that makes sense in our type system)

view this post on Zulip Richard Feldman (Jun 14 2026 at 02:27):

so * can't possibly work with matrix multiplication unless we're ok with x * 2 working but 2 * x not working, which does not sound like the best design

view this post on Zulip Richard Feldman (Jun 14 2026 at 02:27):

but if we pick a different operator besides *, we can give it different rules

view this post on Zulip Aurélien Geron (Jun 14 2026 at 02:32):

I like @ because it's visually obvious, for example:
W = W - lr * 2/n * X.T @ (X@W - Y)

view this post on Zulip Aurélien Geron (Jun 14 2026 at 02:34):

Julia style isn't as clear to me:
W = W - lr * 2/n * X.T * (X * W - Y)

view this post on Zulip Richard Feldman (Jun 14 2026 at 02:34):

yeah, although I don't love that:

view this post on Zulip Aurélien Geron (Jun 14 2026 at 02:35):

view this post on Zulip Aurélien Geron (Jun 14 2026 at 02:39):

Data science is dominated by Python these days, so sticking to @ for matrices might be less friction. Also, a single-character operator is more readable, IMHO. For example, here's *. :
W = W - lr * 2/n * X.T *. (X *. W - Y)

view this post on Zulip Aurélien Geron (Jun 14 2026 at 02:40):

Not a hill I'd die on, just my 2 cents

view this post on Zulip Notification Bot (Jun 15 2026 at 14:02):

A message was moved from this topic to #beginners > Performance costs - multiple dispatch for operators by Jonathan.


Last updated: Jun 16 2026 at 16:19 UTC