I am putting this in a separate thread because it is my personal proposal. I think this is probably the simplest way to enable dynamic tensors with infix operators in userland. I believe that static tensors would require larger type system changes to be ergonomic. As such, I am not considering them for this proposal. Also, please ignore names, they can all be flexible and solidified later.
The base idea has been has already be touched upon. Enable infix math operators by adding abilities to the numeric functions.
Num.add will become a function of type a, a -> a where a has Arithmetic.
This would happen for all of the common math operations. They would all get added to the same ability.
In userland, anyone can define types that support Arithmetic and they will get access to a full set of infix operators related to that ability.
This is all that is needed to enable dynamically shaped tensors to work. It is agnostic to the underlying data storage technique. This also enables thing like complex numbers.
We may also want to enable more infix operators for types that want it. For example, not all types support remainder/modulus. So maybe some of the still common but not super common arithmetic operations could be put in a separate ability. I don't think this should be granular to the individual op (to avoid abuse and non-math use), but maybe we can have 2 or 3 abilities total.
If we want to give slightly nicer support specifically to matrices, I would advise adding a custom ability with infix operators for common matrix operations. Off of the top of my head, I can only think of matrix multiplication and division, but maybe there are more. Those operations would benefit from a totally separate infix operator leaving the default * and / for pervasive multiplication and division.
To deal with the case of mat * 2, we could explicitly add an ability. This ability will automatically be applied when when have a number and a matrix trying to be used with the same infix operator. It will essentially be used to force mat and 2 to have the same type, just like we would get when making matrices a builtin.
I am not 100% convinced on exactly how this should work, but probably could be used anytime we have an unconstrained number type and type that support from num. This probably should not work automatically in general with numbers that have a defined type already that is not a matrix. That should probably require an explicit broadcast request, but I am not really fully sure.
hm, I don't think the third one can work with the current type system but I might be missing something! What would be the types of the functions in question?
So how I imagine it. We have Num.mul: a, a -> a where a has Arithmetic. Then we have the mat which is a Tensor F32. We have 2 which instead of being a Num a is an a where a has FromNumber . Tensor F32 has FromNumber. So the types merge and a is defined as a Tensor F32. We then automatically add in the call to Tensor.fromNumber to get a Tensor F32 from the 2.
Something roughly like that
That or on typechecking failure, check for FromNumber and instead of failing insert Tensor.fromNumber
I do believe that fromNumber may require some compiler magic to work, but I am also fine with that if it is pretty simple and enables this. (though this is also an addon cause I don't think it is needed for the base proposal)
hm, but if 2 has that type, what's the type of something like Num.div which accepts fractions and not any number?
(we can open up this rabbit hole if desired, but I looked into what it would mean if we used abilities to represent integers, fractions, and numbers, and it had a lot of messy consequences)
Let's not open the rabbit hole then. For addon 3, I have 2 other ideas then. Either:
mat * (m 2)mat * $2both are interesting!
what I like best about $2 (which I'd prefer not to do) is that the idea itself makes it easier to try out the "just don't offer it and see if it's ok" strategy, because there's a known alternative if it doesn't work out
regarding infix ops just for matrices: what operators specifically would they be? I'd suggested ** previously for matrix multiplication, but // is already in use for integer division
also, it might be weird (but also might be fine) to have a builtin ability that's never implemented anywhere in the builtins - namely, the "is a matrix" ability for matrix infix ops
knowing that several different use cases want several different representations for dynamic matrices makes a strong case that this is the way to go if we want infix arithmetic ops to work with matrices
Didn't think about // being integer division. I'm not sure what to use for matrices. Maybe others have ideas (or examples from other languages).
I'm also open to making integer division be something different (I just used that because it's what Elm and Python use). Main thing is that it's different from / because silently truncating integer division when you don't realize that's what is going to happen is a huge footgun
speaking of which, that's another tricky thing here: if we want Num.div to accept matrices while also accepting fractions but not accepting integers, what does its type signature become?
I guess div should only work on Matrix (Fractional a) and not Matrix (Num a) due to these exact same issues with truncations...hmm
Maybe it means division needs to be separate form other numberic abilities. Like Maybe it would fall into an extension that enables fraction operations? Though is division the only special fractional operation?
the same consideration would apply to any function involving either Frac or Int (which runs into the same issue)
any function that we want to work with matrices, that is
it might require opening up the rabbit hole after all haha...the only way I can think of to make it work is if int and frac come from abilities, e.g.
Matrix a := { rows : List (List a) }
implements
MatOps { mulMat, divMat, divMatInt },
FracOps if a implements FracOps { div },
IntOps if a implements IntOps { divInt }
I thought this already implicitly worked. you define the div implementation and then you add a a where a has FracOps to it. I feel like we hit something similar with sets and it just worked, but maybe that was just in using mixed abilities and not implementing mixed abilities.
today it's div : Frac a, Frac a -> Frac a
so there's no ability constraint in there
But today I could write Mat.div : Mat (Frac a), Mat (Frac a) -> Mat (Frac a) and Mat.divInt : Mat (Int a), Mat (Int a) -> Mat (Int a). Those function would just work.
The question is if I could add them to an ability, I guess
yeah the problem is that - for example, if we want + to be overloadable, then that means the type of Num.add (which + desugars to) has to change from:
add : Num a, Num a -> Num a
to something like:
add : a, a -> a
where a implements Add
(or whatever)
so the question is, if you want to do that same change but for Num.div, how do you do it in a way where it still works for matrices but also still restricts it to just fractions?
one option is to say div : a, a -> a where a implements FracDiv
and then you give that to all the fraction types but not all the integer types
(like, F32 gets it, but I32 doesn't get it)
that works, but it means functions that accept Frac a can no longer do division on them
because Frac a is a type alias for something that doesn't have that ability (unless maybe there's some way we can change it to do that?)
I'm actually not sure what's possible there; I guess that's an Ayaz question :big_smile:
as an aside, part of the rabbit hole of redefining Num a := a implements NumOps, IntOps, FracOps (for example) is that it means you have to actually define what those abilities are
so like for IntOps you have to list every primitive function that's necessary for something to be an integer
and then people can of course define their own integers, which on the one hand can be beneficial if someone wants to do their own arbitrary int (or whatever) implementation, but on the other hand as soon as people do that, if we ever need to add a new primitive int op (e.g. because CPUs introduce a new feature we want to support), now that's an unavoidable breaking language change for the whole ecosystem
bc the IntOps ability has to get a new member
I think you are over complicating this. Conditional abilities already work today
I think this does all we need and it already works today
app "helloWorld"
packages { pf: "https://github.com/roc-lang/basic-cli/releases/download/0.5.0/Cufzl36_SnJ4QbOoEmiJ5dIpUxBvdB3NEySvuH82Wio.tar.br" }
imports [pf.Stdout]
provides [main] to pf
FracDiv implements
div : a, a -> a where a implements FracDiv
IntDiv implements
divInt : a, a -> a where a implements IntDiv
MyF32 := F32 implements [
FracDiv {
div: myF32Div
}
]
myF32Div = \@MyF32 x, @MyF32 y -> @MyF32 (x / y)
MyI32 := I32 implements [
IntDiv {
divInt: myI32Div
}
]
myI32Div = \@MyI32 x, @MyI32 y -> @MyI32 (x // y)
Vec a := List a implements [
FracDiv {
div: vecFracDiv
},
IntDiv {
divInt: vecIntDiv
}
]
vecFracDiv : Vec a, Vec a -> Vec a where a implements FracDiv
vecFracDiv = \@Vec x, @Vec y ->
vecFracDivHelper = \out, i ->
when (List.get x i, List.get y i) is
(Ok a, Ok b) ->
List.append out (div a b)
|> vecFracDivHelper (i+1)
_ ->
out
vecFracDivHelper [] 0
|> @Vec
vecIntDiv : Vec a, Vec a -> Vec a where a implements IntDiv
vecIntDiv = \@Vec x, @Vec y ->
vecIntDivHelper = \out, i ->
when (List.get x i, List.get y i) is
(Ok a, Ok b) ->
List.append out (divInt a b)
|> vecIntDivHelper (i+1)
_ ->
out
vecIntDivHelper [] 0
|> @Vec
main =
@Vec a = divInt (@Vec (List.map [4, 5, 6] @MyI32)) (@Vec (List.map [1, 2, 3] @MyI32))
@Vec b = div (@Vec (List.map [4, 5, 6] @MyF32)) (@Vec (List.map [1, 2, 3] @MyF32))
aStr = a
|> List.map \@MyI32 x -> x
|> List.map Num.toStr
|> Str.joinWith ", "
bStr = b
|> List.map \@MyF32 x -> x
|> List.map Num.toStr
|> Str.joinWith ", "
Stdout.line "A: \(aStr)\nB: \(bStr)"
If we can do this with wrappers, I think we can do this with builtin numbers just fine.
Can we not define Frac a as something that enforces certain abilities? I mean all of the types with Frac a are forced to have all of the fractional ops. Kinda like how Dict k v guarantees that k has Hash.
Also, yeah, definitely no suggesting this: Num a := a implements NumOps, IntOps, FracOps. Just want the subtypes of num to implement the correct ablities.
Brendan Hansknecht said:
Can we not define
Frac aas something that enforces certain abilities? I mean all of the types withFrac aare forced to have all of the fractional ops. Kinda like howDict k vguarantees thatkhasHash.
I think so, but I'm not positive - @Ayaz Hafiz would know best
but on the other hand as soon as people do that, if we ever need to add a new primitive int op (e.g. because CPUs introduce a new feature we want to support), now that's an unavoidable breaking language change for the whole ecosystem
Yeah, I think we should scope this to just a reasonable subset of math ops. Something that likely will stay static forever. If we add something new to integers, it would not affect this. It just wouldn't be accessible from matrices, complex numbers, and the like
it's different from Dict because Dict is an opaque type, whereas Frac is a type alias for the Num opaque type with particular type parameters
specifically, Frac a : Num (Fraction a)
so if Num were an ordinary opaque type, we'd have to do something like Num a := { ...whatever... } implements FracDiv if a implements FracDiv - and then we'd need Fraction a := { ...whatever else... } implements FracDiv
which I guess would actually work because you can't construct a Fraction in userspace anyway :big_smile:
so I guess that sounds like it would work?
Roc does let this work:
FracVec a := Vec a where a implements FracDiv
implements [
FracDiv {
div: fracVecDiv
}
]
fracVecDiv = \@FracVec x, @FracVec y -> @FracVec (div x y)
# Do I need to specify the ability?
someDivFunc : FracVec a, FracVec a -> FracVec a
someDivFunc = \x, y -> div x y
But yeah, types always get complex, don't they...
you can put the ability in the type alias
to me the bigger concern with the IntOps / FracOps design is what happens when you want to add a new one and it's a major breaking change
I don't think the goal is to list out all IntOps or FracOps
I think it should just be to put a reasonable list to give infix operators to more types
So we can still add new ops to a regular integer without updating those abilities
yeah I agree, and that does seem in theory to be possible :thumbs_up:
So maybe they need a different name, but something to keep them limited and focused if possible.
I do agree that it is quite inconvenient that there are subtle differences (divsion being the main one cause technically fmod exists)
fortunately we recently decided that mod and rem shouldn't have infix ops after all :big_smile:
so the infix op abilities in question would be something like:
DivFracDivIntAddSubMulNeg...is that it?
I think so...oh, pow is infix as well
yeah that one has a frac and int version
so we should probably either give it two infix ops or zero, like we do with div
having a ^^ operator sounds kinda silly though :stuck_out_tongue:
Why does pow need two version?
semantically, I think they are the same, just different underlying impl, all numbers should support it
I think that is something we should be able to hide from the users? though maybe I am missing something
Martin Stewart said:
I thought Num.powInt was separated because it needs to produce an err if you write Num.powInt 2 -1 (otherwise you have Elms problem where this function says it returns an int but actually it’s a float)
I think it may just return zero or panic currently
Or it just returns 1 currently....
Also, I think the infix ^ is always pow and never powInt currently
but these all may just be other inconsistencies rather than a comment on what we should design here
yeah
Should powInt just a take a Nat as the second arg?
some other unsigned value I think, because we're going to get rid of Nat
I think we determined anything bigger than U8 would overflow anyway, right?
Brendan Hansknecht said:
Didn't think about
//being integer division. I'm not sure what to use for matrices. Maybe others have ideas (or examples from other languages).
Just a small comment following up on the notation. One thing to keep in mind is that there is both left and right matrix division. This is because matrix division is really multiplying with the inverse matrix, and this can be done from both left and right. I.e. we might say , and . (It is when constructing the inverse of a matrix that you need to take fractions of numbers). And in fact when solving linear equations it is very common to multiply with the inverse from the left. Here's Julia's matrix division operators.
(Usually inverses are only defined for square matrices, but there are generalizations to non-square. Then only one of the divisions make sense, depending on the shapes of the matrices involved...)
heh, we actually could make a \\ operator
but not \ because that already means lambda
so if we wanted to have // and \\ for matrix division, that would be a nice symmetry (although then we'd need to find something else for integer division of course)
not to bikeshed too much on it, but since it's desugaring to Num.divTrunc, I could see an argument for /- or /_ or /!
This is where full unicode is often awesome even though it doesn't makes sense to add here: /⌊
Also look strange but makes a lot of sense /_
Though also a bit weird cause _ is don't care.
yeah
definitely looks weird to me haha
could also do / for integer division and /. for frac division, but if either of them should get / it should be frac division :sweat_smile:
I guess you could argue /. makes sense for int division because it's truncating everything after the .
but OCaml programmers would be like :scream:
I'd say /- is the frontrunner to me, because truncation is kinda like doing accurate division and then subtracting away the part after the decimal point
I also don't mind (/) or /~. You know /~ you divide, then you get lazy and give a roughly correct answer. it is ~7
I guess that would be divRound not divTrunc
I strongly want to avoid 3-character infix operators
partly for aesthetic reasons but also then we can't use efficient SIMD operations to parse them
/~ is interesting!
or even ~/
anyway, I think we have enough viable options there that the matrix infix ops could reasonably be **, //, and \\
are there others people would want?
an important question to ask here is: where do we draw the line on how many (and which) infix ops to add to builtins for these use cases?
like how many does Julia have?
Based on the doc here: https://docs.julialang.org/en/v1/stdlib/LinearAlgebra
Julia has *, /, \, and^ as special linalg ops. Also presumably the standard + and -, but it isn't listed there, or I missed it.
oh yeah, how would we distinguish between frac and int division with matrices?
...is it even useful to have int division of matrices?
probably not useful. I think it is fine to leave that as explicit
to be honest, I now wonder the same thing about int division in general :thinking:
Also, rest of the ops are here: https://docs.julialang.org/en/v1/base/math/
For math specifically, they have +, -, /, and //. They then also have multiple bitshifts and a few other things
like maybe you should need to call Num.divTrunc or Num.divCeil or whichever you want
yeah I assume nobody wants to do bitshifts on matrices
That said, julia super embraces custom operators, you can just write ⊗(x,y) = kron(x,y) to get the new infix operator ⊗. They have a super long precedence table in that link...crazy
Richard Feldman said:
like maybe you should need to call
Num.divTruncorNum.divCeilor whichever you want
I get the sentiment, but would be really inconvenient for a lot of things with indices.
fair
interestingly, I think in this design game devs might choose not to use the infix matrix multiplication because it requires dynamic
unless it's specifically like a 4x4 matrix
because for smaller matrices they might not want to pay for storing the dimensions dynamically
at runtime
and instead have like a Mat4x4 := (...nested F32 tuples...)
and maybe another for 4x3, 3x4, etc.
They also can use infix with static size matrices, just with more limited shape possibilities.
Have to be the same shape for anything they define so only works with square matrices
right, exactly
zooming out, I think an important question to ask at some point (maybe not yet, maybe when it's more concrete) is: assuming we do all the implementation work, will people actually use it?
or will they just use Python anyway? :sweat_smile:
because I've heard that (for example) the main barrier to Julia adoption is Python inertia seeming impossible to overcome, see for example Mojo's design decisions
I will definitely use this functionality, and be very happy that it exists.
sweet! For graphics?
(please don't say "for making a bunch of DSLs using all the arithmetic operators at once")
or will they just use Python anyway? :sweat_smile:
the main barrier to Julia adoption is Python inertia seeming impossible to overcome, see for example Mojo's design decisions
I think we have to be careful when we say this. While this is true, Julia is has found itself a nice niche in part of the scientific computing community.
On top of that, Roc is not really trying to compete in this space. We just want to enable users that happen to overlap with this space to be happier. So I think our metric of success really shouldn't relate to this much at all.
sure, my main concern is ecosystem
like as I understand it, domains like scientific computing need significant library support for people to be able to do things in them effectively
and we won't have that unless there's a certain minimum level of interest
the specific thing I'm worried about is that we don't (and won't) have direct C FFI, so you can't just grab LAPACK off the FFI shelf unless you want to do it through a platform and have Tasks for math ops
so there has to be sufficient interest for people to actually implement (or at least port) a chunk of linear algebra libraries over, right? (I'm mainly worried about this because I don't know the domain, but I've heard things, and so I don't really have relevant intuition here about how things might go)
so a concrete outcome I would be worried about is that we make the infix ops happen, but still nobody is using Roc for scientific computing (for example) and then we're back to #ideas talking about how to get LAPACK access into Roc to unblock the scientific computing use case for a second time :sweat_smile:
(for context, I am currently leaning toward turning this idea into a proposal and accepting it, so I'm drifting into poking-holes mode)
past thread for context: https://roc.zulipchat.com/#narrow/stream/304641-ideas/topic/FFI
The comment on LAPACK/BLAS/etc is quite pertinent. I hadn't thought of that. It does mean that everything probably has to be reimplemented in roc and will not be as performant....yeah, very good point
how much less performant would it be? :thinking:
I kind of assume we could get it to be equivalent performance, as long as we have simd (which separately we do want to have anyway)
is it just bounds checks?
because we support all the fastest arithmetic CPU ops
and if we also have SIMD on top of that, I'd assume the only thing any implementation could use where the perf couldn't be matched by pure Roc code would be something making use of memory unsafety, because we require bounds checks
I think some of those libraries are hand optimized assembly and they have a number of clever representation tricks. So I just assumed no one would put in the work to fully reproduce their perf in roc. So more a limit in terms of what someone would do than what could be done was my main assumption.
ahh gotcha
yeah like trying to get LLVM to optimize it into assembly that's at least that fast
I could use this feature for graphics and drawing things. I would use this for a state space controller for e.g. a robot. I haven't got a burning desire to do scientific computing with Roc, but I definitely feel like it has the potential to mature to the point where you can do a lot, with all the benefits of using roc entails. If I was a professor teaching electrical engineering, I could use Roc to teach the mathematical principals, run simulations, etc - not to compete with Matlab or Python but I could see a future where it is possible and desirable to do these things. There are significant benefits with some of Roc's design choices that I think make a compelling argument to use it in many critical areas too. Not just performance, but things I really care about like maintainability, security, reliability, reproducability, verifiability, validation, testing, simulation, ease of use/learning, etc.
I'm halfway through reading this and something just hit me like lightning - why not just make matrices matrix-valued functions? I mean, if that doesn't completely wreck the type system. That's what a matrix literally is, a matrix valued operator. You wouldn't even need an infix, it'd just be a space between matrices. Likewise a tensor, acting on any other class of tensors of fixed shape, can be seen as a function into a different tensor space. I don't think it works as neatly there (a tensor is basically many functions at once depending on how they're used), and I'm not even confident about it working even constrained to matrices (let alone what it might do to nums!), but I needed to get it out of my head.
And yeah, I don't think everything should end up in the builtins, all those diagonals and tridiagonals and so on, I brought them up for largely the reason you've been discussing - where should the limits of the builtins end and the userspace begin? And what changes would be needed to let userspace tensors be ergonomic-ish? Bringing up einsum wasn't necessarily about bloating the requirements as capping the bloat - if you can make this work everything else probably can. @Richard Feldman said earlier he didn't have much experience with the area (nor do I in language design), so I thought I might info dump about it to give him enough clues to sniff out the right response.
Brendan Hansknecht said:
Richard Feldman said:
like maybe you should need to call
Num.divTruncorNum.divCeilor whichever you wantI get the sentiment, but would be really inconvenient for a lot of things with indices.
Can you give an example here @Brendan Hansknecht?
why not just make matrices matrix-valued functions? I mean, if that doesn't completely wreck the type system. That's what a matrix literally is, a matrix valued operator. You wouldn't even need an infix, it'd just be a space between matrices.
Can you explain this further @Declan Joseph Maguire?
I don't know if this would remotely work with the type system, but you could have as functions any matrices A: x -> y, B: y -> z, and thus B A: x -> z. The type variables are vector spaces, or just lists of nums. More generally, any tensor is characterised by the vector spaces it spans over, which itself has a lot of nuances to do with dual spaces but I don't think it needs us to go there. In any case, any tensor is simultaneously an object and a sort of "multifunction" able to embody multiple mappings, one for every binary partition of its vector spaces.
I could dig into the general tensor case, but I want to lay out the simpler matrix case just to see if this has legs.
Some more detail on why we can't support indexing syntax in general: suppose we had bracket syntax that desugared to List.get, e.g. myList[index] like Python has.
If we wanted to generalize that beyond List to userspace types, it wouldn't be implementable because the ability function would need to know not only the type of the collection (which it does know, so no problem there) but also the type of the element being returned (which it does not know, and which it couldn't know unless we introduced something like associated types to abilities, which I don't think we should).
So this same problem applies to any syntax you might choose, including functions. Actually functions have several additional problems, like how functions aren't comparable for equality, can't unify to userspace opaque types, and probably others I'm not thinking of right away. :big_smile:
Yeah, had a sense it'd be too good to be true. Sucks about the indices too, and maybe there's an argument to bake indices into tensors too (unless I misunderstood the depths of the issues preventing it), but we've got bigger fish to fry design-wise.
it can't work with the design in this thread
Ah well. I'm pretty excited about the rest of the thread though.
Anton said:
Can you give an example here Brendan Hansknecht?
Super simple example, in a binary search you want to write m = l + (r - l) // 2.
I would 100% use this for any teaching I do. If we have a reasonably efficient graphics output, I could see Roc like apps becoming very useful for teaching concepts like animation, where the predictable nature of an ergonomic statically typed functional language reap huge rewards.
I haven't been following these conversations as much as I want. But I would use it for many things. I'm currently in s scenario where I have a bunch of shitty C++ code that attempts to follow reactive functional UI principles (AKA elm architecture) and it works pretty good, but it's still C++ so the errors are shite, and every once in while you blow your leg off with something incredibly difficult to spot that would be completely avoided in a language like roc. So I would switch to it for a bunch of the stuff I do for research and potentially even professionally
Especially if the cross-compilations works as well and as easily as you intend for it to.
Would other people use it? I have no idea. I've long ago learned to distrust my ability to predict how the average programmer will react. :stuck_out_tongue:
Super simple example, in a binary search you want to write
m = l + (r - l) // 2.
I'd have to do a bunch of examples to get a better feel for it, but in this case, I do like this kind of formatting:
m =
Num.divTrunc
l + (r - l)
2
Luke Boswell said:
I could use this feature for graphics and drawing things ... If I was a professor teaching electrical engineering, I could use Roc to teach the mathematical principals, run simulations, etc - ... but things I really care about like maintainability, security, reliability, reproducability, verifiability, validation, testing, simulation, ease of use/learning, etc.
I wholeheartedly agree with all of this.
@Anton Would be:
m = l + (Num.divTrunc (r - l) 2)
oops, haha, as I said, editor plugins to render math are the way to go :p
I'm not sure I agree, mostly cause source is plain text. You will end up seeing code in plain text in many locations, and we want that to be readable. Sure, an editor plugin helps, but there will always being people using different editor, viewing code on github, etc.
Yeah, I've thought about that as well, it seems all possible solutions have significant shortcomings.
what about [*], [/], etc, as the matrix operators? By bracketing them, they're visually distinguished as being related to matrices
It's not clear if we need left vs right division when we could just reorder the operands
I'd like to avoid operators with more than 2 characters, partially for aesthetic reasons but also because 1-char and 2-char operators can be detected using a single SIMD classification operation, whereas 3+ have to use extra logic.
Last updated: Jun 16 2026 at 16:19 UTC