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
great question!
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
so when you do that, 1 can become a Vector2, and then Vector2.zero + 1 can Just Work because 1 can become a vector
unfortunately that doesn't work in the general case
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
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"
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
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
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
there are similar questions with matrices
however, I don't know what the right design would be for those yet :sweat_smile:
I'm curious what other languages you've used this sort of thing in!
Thanks for the detail! I'm used to doing this sort of thing in python using dunder methods or in lua with metatables
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
I'm having a lot of fun reading up on Roc and playing around, but definitely still wrapping my head around things
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!)
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:
that said, you _can_ do things like "this argument can be any type that has this particular method"
or set of methods
or "any type at all"
just not "exactly this type or exactly this other type"
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 }
which obviously is not ideal :smile:
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
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.
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
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
which I think is totally reasonable
one tricky one is matrix multiplication - I think that one would need to have a different operator than *
is that a big deal ergonomically?
Python uses @ for matrix multiplication. I like it.
Julia uses * for matrix multiplication and .* for item-wise multiplication
yeah we could do the reverse
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
otherwise you can write x * 2 but you couldn't write 2 * x, which would be ridiculous :stuck_out_tongue:
(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)
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
but if we pick a different operator besides *, we can give it different rules
I like @ because it's visually obvious, for example:
W = W - lr * 2/n * X.T @ (X@W - Y)
Julia style isn't as clear to me:
W = W - lr * 2/n * X.T * (X * W - Y)
yeah, although I don't love that:
@ for this purposeData 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)
Not a hill I'd die on, just my 2 cents
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