Is there some way we can make functions assignable to infix operators, like %, +, -, etc? Unless it unavoidably breaks everything, this seems like a feature most general purpose language should have. There's so many functions that are most elegantly and conventionally represented by them, so limiting it to just Num seems like a big sacrifice.
I'd imagine if we support overriding an operator's function, it would need to be scoped to a single module and set per type. It'd generally be unreasonable to change the behavior of _other_ modules with something as fundamental and surprising as changing the meaning of their use of operators.
That said, changing these anywhere in any scope greatly harms the ability to read and reason about code. It's the kind of thing that happens when one person thinks % should be redefined to "multiply by the given percentage," and that happens while you're on vacation, and then you come back, write a little more code in the same module and then spend 8 hours debugging something because you didn't think to check your assumption about trusting that the operators do what they're supposed to do.
Why should we expect problems of new types being given a + operation affecting the interpretation of prior uses of + elsewhere? While it might expand the scope of some types and thus trigger a type error, it shouldn't change existing functionality when actually implemented, right? And I guess the usecases I had in mind were more mathematical and numerical, like defining matrix addition and multiplication.
For reference, here is an earlier discussion: https://roc.zulipchat.com/#narrow/stream/304641-ideas/topic/Num.2Emod.20is.20a.20trap.3F
There's also further discussion about mod removal in this PR: https://github.com/roc-lang/roc/pull/2883#issuecomment-1101531371
It was essentially removed due to lack of hardware support (we would have needed to emulate the operation), and our discussion about what the precise meaning of mod should be, or how it should be named, was never resolved.
I believe the best outcome would be getting rid of the mod and rem naming distinction, and perhaps leaving rem for what the hardware does, while giving descriptive names for at least any other variants (e.g. remTowardsZero, remTowardsNegInf, etc).
"floor" seems risky as a term because people seem to not think about its implication regarding negative numbers (unless they're specifically dealing with negatives at the moment), and thus they may mix up towards-zero and towards-negative-infinity.
Declan Joseph Maguire said:
I guess the usecases I had in mind were more mathematical and numerical, like defining matrix addition and multiplication.
Ah, I misunderstood. I thought you discussing a mechanism to allow Roc code to _redefine_ the meaning of an existing operator on types it was already defined on, e.g. "Roc % means rem, but I want it to mean mod"
I don't recall if _abilities_ supports defining operators or not, but that may be what you're looking for. There will certainly be Zulip discussion, and probably a design doc, indicating why it does or does not support operator overloading
Oh yeah, god, that would be insane. Redefining on already supporting types would be utterly ludicrous. I'll do a little search for operator overloading - I hope it isn't futile.
7 messages were moved here from #ideas > Add mod to the builtins by Richard Feldman.
some relevant context about customizing infix operators:
|+|), but not redefine builtin ones (such as +) but then Elm removed that capability after the way they were used seemed like a net negative. I think that was a good decision and I don't want to introduce arbitrary new operators to Roc.+) and James Gosling intentionally removed that capability for Java based on how he saw it used in C++ (e.g. repurposing the bit shift operator for things like cout << "Hello, World!")+ and << do, but I haven't really seen it used in the same way it seems to be in C++. I'm not sure if that's a difference of cultural norms or something else.+ would require some sort of compiler change, and potentially a change to the design of the type system. So there's a significant bar to clear to justify adding that to the language.we haven't really talked about this idea before, but here are some of my thoughts on it:
(matrix1 + matrix2) / matrix3 such that it's a big inconvenience to have to write matrix1 |> Matrix.add matrix2 |> Matrix.div matrix3 instead? (Maybe this comes up a lot and is a major inconvenience! I don't do much with matrices, so I'm genuinely curious to learn.)Num rather than making changes to the type system.cout << "Hello, World!" in C++ or like using / in a DSL involving URLs or paths. I think it's best if those operators stay for use with numbers only.I think it's very reasonable to restrict it to num, or perhaps some type enclosing Num. I think my physics background is betraying me here, because we often summed matrices as a direct translation of the mathematics. So this perhaps isn't the most generalisable example. However, it certainly is common for vectors in graphics, needing to add, subtract, and scale vectors. In fact, it gets thornier, because you often want to multiply a vector by a num.
that's easy to do if there are separate functions for it, but off the top of my head I think it's almost certainly impossible in Roc to make + do that without changing to a completely different type system
because + needs to have the same type on both sides
and it's that way at a very fundamental level (well, unless we wanted to have it work in a hacky and likely surprising way involving silent numeric type coercion, which I don't think we should do)
another use case that comes to mind is units of measure; we've talked about having a first-class language feature for those although I think it's less clear that it's a good idea today than it was back when we first talked talked about it, because the language has gotten more complex in other ways in the intervening time, and I always want to be mindful of overall complexity in the language (nobody sets out to design a big bloated language; it gets that way by accepting a bunch of small individual feature requests!)
without the first-class units of measure language feature, a Roc equivalent of something like elm-units would only be able to use + if somehow that were built into Num, or else if there were a way to customize +
I think there's a strong case to investigate some of those possibilities. If we have to restrict to any individual invocation of an operator having the same type both sides, that's good enough.
As long as the workaround isn't too bad. You could probably get away with a single function any time you needed to make the types work out.
I personally am a big fan of array languages and being able to just do math of Nd shapes. It is really awesome. Something with a numpy like api is totally useable, but way more clunky
Obviously something like numpy is fine at the same point, just look at its success in python
Almost the exact same api could be make in roc
what are some specific examples?
what specifically are you looking for when you ask that? examples of what?
doing math on n-dimensional shapes - like what are some specific use cases where this has come up and been nice in array languages?
Ubiquitous in all of science and engineering, and a decent chunk of 3D code
I of course never get to use it in production just for hobby. I have seen lots of talks where peopel do a lot of math, simulation, and engineering with it. The only project I have seen in person was laser point cloud data processing for car panel alignment quality control.
I'm looking for specific examples because I don't work with those domains :big_smile:
the specifics matter a lot!
this is specific, but I'm not sure how exactly it will help you:
laser point cloud data processing for car panel alignment quality control
er sorry, I meant as in code examples
like I want to look at how a particular use case is handled nicely in another language, try to see how nicely we could implement it in today's Roc, and then see how far off we are
like in what specific ways the ergonomics are different etc
Many physical quantities are naturally represented by vectors (1d data of dimension N), matrices (2d data with dimension numbers N), and higher order tensors (D dimensional data of dimensional number N). So this is what is used to represent all forces, velocities, momenta, pressures, shear forces, general force transfer through a 3D solid, the core of any fluid simulation, electromagnetic simulation, finite element simulation, most concepts in statistics, and all of quantum mechanics
Ah, I see, I misunderstood what you meant by specific examples
It's super late here, but maybe tomorrow I'll dig out some old matlab code from my undergrad days, for whatever relevance you think that has, and post it.
sure, thanks!
This is my own code, but following a real problem related to ray tracing. Calculates collision times for N rays (represented by to 3 by N arrays` with a sphere (of course would be done in a similar way for trianges, also doesn't calculate for M spheres at once cause memory constraints, but it is easy to modify for that as well):
HitTimes ← {
center‿origins‿dirs‿radius ← 𝕩
oc ← origins-center
a ← +˝dirs×dirs
hb ← +˝oc×dirs
c ← (טradius)-˜+˝×˜oc
disc ← (טhb)-a×c
⌊˝(⊢+0.001√∘-∘≥⊢)÷⟜a˘(-hb)(+≍-)√disc
}
If interested, I can spell out exactly how it works and the shapes.
otherwise, we can find less exotic example ,
wow, yeah that's quite array language-y :sweat_smile:
is there an example that's more syntactically similar to Roc?
This is a simple numpy example:
import numpy as np
# Create two vectors
vector1 = np.array([1, 2, 3])
vector2 = np.array([4, 5, 6])
# Perform elementwise addition
result = vector1 + vector2
print(result) # [5 7 9]
Here the + can thus be used to add vectors (1D arrays) .
I kind of think a named function could be better here like vAdd or vecAdd, because it's so common to use single letter variable names in math that it's nice to know you're dealing with vectors.
yeah, so that example I think would be totally fine in Roc today - just have a named function with the type List (num a), List (num a) -> List (num a)
alternatively, you could do Vec3.add : Vec3 a, Vec3 a -> Vec3 a assuming Vec3 : (Num a, Num a, Num a)
that would be more memory-efficient, enforces at compile-time that the lengths are the same, but only works for a fixed N hardcoded at compile time
and we already know that Vec2, Vec3, Vec4, as well as e.g. Vec2x2, Vec3x3, Vec3x4, Vec4x3, Vec4x4 are all reasonable options
all of those I think work fine in Roc today as long as you're okay with named functions for operating on them
I think this desire for operator overloading in part comes from mathematicians/physicists/... who want to recreate the formula they are familiar with in code and thus have it look as similar as possible.
so maybe a more specific question is: what's a real-world (not necessarily "professional production code" but at least something where someone needed it in order to get something working where they didn't know what the output was going to be in advance and cared about the output being correct) example of something that wouldn't work with the above list of hardcoded Vec shapes?
I have implemented physics simulations involving rank-4 tensors whose compnent dimensions were determined by computer capacity
how big could those get?
in practice, I mean
I too am a physicist with opinions here haha. One example is when solving differential equations, i.e. if you have a spatial grid with many lattice points, say N. Then you can represent the solution you are looking for as a vector of size N, and represent differential operations as N by N matrices. Then you solve a matrix equation. In this case N will be chosen big enough to give you accurate enough numerics, and is often quite large!
I have lately been using Julia for these things and it is fantastic in this regard.
so "quite large" - like what's an example number there? like, a thousand? a million?
In my code, I have a 810,000 by 3 matrix
ok cool, so that's clearly heap-allocated haha
Yes
so the next question I wonder is: how bad is it if that's just a List of numbers?
and then there's a matrix library built on top of that, using primitives already available in the language today?
Totally fine (with minor caveat that I don't think belongs on roc)
As for matrix library, it is just a reality thing
Oh, and a duck typing thing
hm, I don't quite follow (although there may be typos in that response? haha)
Readability
ah gotcha :thumbs_up:
a + b works as long as a and b have compatible shapes.
right
So could be vectors, matrices, scalars, or any combination with compatible shapes. Scalar plus matrix for example. That is convenient shape based duck typing and makes it so you don't need many variants of functions
so then the question becomes, how big of a concern is the ergonomics gap between a + b and (for example) Mat2d.add a b
Exactly. That is the core question.
I'm an array language fan, so I would say a very big difference.
At the same time, I would argue that it isn't something roc is really trying to solve. Roc is not an array or mathematics language. I think the named form with a good library is enough for basic support and any deeper support requirements probably should use a better suited language.
Think about all the extra words, parens, and verbosity. Really hides the point of the code.
Made worse when you have clear equations that would normally be written out by hand (like in physics and math). Being able to mirror them in code is amazing for verifying code is written correctly. You lose that with named function
I agree with this. Probably best to leave all that to the Julias and MATLABs of this world.
Would a reasonable outcome be to allow definition of math operators when wrapped in parens, e.g. 5 + 6 can work for numbers, but not anything that isn't language-defined, while an a (+) b or a '+ b or whatever can only be used for non-builtin types
This has the benefit that it's clear to the reader that these are not the normal operators (and to knowledgeable readers, they know that these may have custom behavior), while still being fairly readable
Elm had that pre-0.19, you could use backticks to call a function infix, like
5 `mod` 3
I'm not 100% sure why it was removed, but my guess would be when the parser/compiler got rewritten for 0.19 it was easier to just cut that feature for performance. It's kind of neat, but not really essential.
Would a compromise be that infix operators need to explicitly imported like other functions for a type, with some limitations to ensure this is only possible if the library had already supported it? For extra clarity, the functions being imported as + might have more explicit names so its clear you're importing a new function as +. However I'm not sure if imposing sensible limitations would require introducing undesirable inconsistencies.
That way any non-Num uses of the operators would be isolated to the place using it, and it would also be explicit what its meaning is. This also makes it just inconvenient enough that people would only use it if it gave a meaningful egonomic benefit, so that should disincentivise people trying to add operators to all their libraries just because they can.
I'm perfectly fine with the marked operator idea, if that's what it takes. But could I sugges using +' instead of '+? It feels ever so slightly more readable, plus I reckon you could set up the second character to autofill in your personal IDE if you really want save the keystroke, without that bothering anyone else using the language.
Brendan Hansknecht said:
a + bworks as long asaandbhave compatible shapes.
To give a second example, if a is a matrix of m x n, and b a matrix p x q, then ab is only defined when n == p, and ab will have shape m x q if so. To even attempt matrix multiplication otherwise is effectively a type error. You could just have it be a regular error and make the user handle it, but this would make writing any sort of arithmetic statement a nightmare
I do think there are limits to how much to expect from Roc in its support for this stuff though, given it's not an array language. I feel that asking for mat + 0.5 to work the way it does in matrix languages (adding the given value to all matrix entries) would be too much. Except, of course, that it's extremely common to write something like 2.5*mat, which is indispensible, although it would at least be sensible to forbid mat*2.5 if that makes the problem slightly less bad.
To make things tanglier, in the case of 1 x 1 matrices, you basically recover your original numbers with all arithmetic operators recovering their original meanings. Stuff like quaternions (now ubiquitous in game engines), complex numbers (engineering, especially anything to do with electrical engineering, physics, some niche graphics applications), or really anything "numberlike" have representations as matrices, so if the syntax and dimension-correctness of matrix arithmetic is supported then all those use cases are as well. Not that you'd represent those as matrices under the hood.
Richard Feldman said:
ok cool, so that's clearly heap-allocated haha
Is that purely a matter of size, or is there a second consideration? Once you have the matrix, you tend to do operations that allow in-place mutation, if that is relevant. If array shape can be properly part of types, then this should be determinable at compile time. Forgive me, I don't have a brilliant understanding of when and how objects should be stack/heap allocated or not.
I'm not quite sure if all matrices should be heap allocated though, in graphics applications it's very common to have lists of small vectors/matrices of the same size, like lists of offsets or 3x3 rotation matrices (or quaternions, increasingly). Hell, any sort of 3D software like a game engine needs these mandatorily, though they tend to cap out at 4x4 matrices, and usually 4x3 or 3x4, so just hardcoding their type relationships would work, though it'd be a lot of tedious duplication.
If heap allocation is mandatory for big chonky bois, I think it's reasonable to allow the end user to be in charge of that. Anyone making these big matrices will already be accutely aware of performance considerations, or at least memory considerations. Of course if there's a known size beyond which objects ought to be on the stack, then maybe the library writer could handle that automatically, stack allocating any sufficiently large arrays. But, I'm not sure if that's a good or bad idea as I haven't had to deeply engage with this side of Roc.
Why are all my messages always so goddamned big. Need to learn brevity.
Yeah, I think there are two core use cases here:
1) is place where array languages, Matlab, and the like would be used. They tend to have larger arrays and tend to just heap allocate everything.
2) places like games where they have small approximately 4 dimension generally 2d arrays. These would be stack allocated but probably also most suited for a different library.
Yeah, it's very likely they'd have different use libraries. Then again, it's not uncommon to have physics simulations within 3D software, and you can sometimes end up with 3x3x3 shaped arrays to represent certain tensors, so there's an overlap. However even ignoring this overlap, I think the accomodations Roc would need to have to best support these two use cases are very similar.
For sure
Plus I think in general giving the type system a notion of array shape, or at least size, would be a good idea anyway. So many operations act on arrays to produce new arrays of the same size, but that fact is not always obvious if your idea is just "we have lists and a function that yields a list's length".
Also I'm pretty sure any solution that makes array shape part of the type system would instantly allow unit types. Any unit of measure can be expressed as a vector, whose components are the exponents of their corresponding base unit, so the units of 9.8 m/s/s = 9.8 ms^-2are expressible in SI units as a tuple (-2, 1, 0, 0, 0, 0, 0). Types can be computed very easily then, the only difference is that shapes are described by tuples of Nats, whereas SI unit exponents are usually integers, nearly always rationals, and very very rarely reals.
I don't think that should be in the type system as a first class citizen.
For one, most large multidimensional array probably want to store the shape info as runtime data. That way you can have dynamic shapes depending on input and things of that nature.
For small multidimensional arrays, I think it would make more sense, but I think it isn't a priority that will fit roc's type system well just for that use case. Someone can manually define said types but doing it generically with type variables that need to do math to calculate new sizes is a ton of complexity for the type system.
Also, just curious, does any language actually do this in the type system currently? Like encode array shapes and mathematically define how different operations change said shapes.
True, but I think in the case of arrays you could get away with treating the elements of the shape abstractly, so concatenating two vectors of lengths n1 and n2 wouldn't require knowing n1 or n2 at compile time, nor to compute n1 + n2. Instead it would be algebraic, taking abstract expressions and checking for equivalence. You would end up with an implicitly defined algebraic polynomial, and those can easily be put into canonical forms that can be checked for exact equality. Only at runtime would these dimensions be populated, unless you explicitly specified a shape literal (probably a record or list of Nats).
Brendan Hansknecht said:
Also, just curious, does any language actually do this in the type system currently? Like encode array shapes and mathematically define how different operations change said shapes.
I swear for the life of me I have, but I can't remember where. Excuse me while I do some digging. I really hope this isn't something my brain confabulated.
I'm pretty sure that type systems that track array length exist, at a minimum
there are languages that support tracking units of measure which is equivalent, for example F#, and most proof languages, including Coq. We have talked about such a feature in Roc before and it is not overly invasive, but I don't think it's something the language needs to prioritize right now. I don't think Roc can really compete with a Mathematica/MatLab/Julia or even Python in this regard.
Ayaz Hafiz said:
there are languages that support tracking units of measure which is equivalent, for example F#, and most proof languages, including Coq. We have talked about such a feature in Roc before and it is not overly invasive, but I don't think it's something the language needs to prioritize right now. I don't think Roc can really compete with a Mathematica/MatLab/Julia or even Python in this regard.
I agree it's a low priority, not urgent at all. I think there's still value in talking about these things early though, just to make sure we're aware of the design space needed so we don't close it off accidentally. And I don't think Roc is going to displace Mathematica/MatLab/Julia/Python, but I still think it's a use case worth some consideration for a general purpose language. Plus I think there's a much stronger argument for Roc being deployed in game engines and other 3D applications, so all the use cases that flow from there remain pertinent.
Also, just curious, does any language actually do this in the type system currently? Like encode array shapes
That's really interesting, I hadn't seen that language before. Given it's a functional programming language designed for extreme multithreading and GPU compute, it almost sounds like a complement for Roc in the domains it's not competing in.
Dear god this thing looks adictively brainmelting
Hehe, yeah, I also like to check on it regularly. It would be cool to make a futhark platform for roc.
Richard also just did a podcast with the co-creator of Futhark https://podcasts.apple.com/us/podcast/designing-compilers-for-speed-with-troels-henriksen/id1602572955?i=1000631159511
It could be synergistic. Ooh I'll listen to that later.
You'll probably also find this interesting: https://github.com/hbcbh1999/hvm-core/tree/main
Anton said:
I should have been more clear, just encoding the size is not the important part. Even c++ does that. The important part is encoding as symbolic variables that can be manipulated. concat is a function that takes array of n and m and generates an array of the n + m`. But that isn't even fully it. It needs to be able to do this for complex functions with multidimension arrays. Be able to encode rotation, broadcasting, etc, all in symbolic variables in the types.
Anton said:
Hehe, yeah, I also like to check on it regularly. It would be cool to make a futhark platform for roc.
Is that even possible?
Is that even possible?
At the very least we should be able to frankenstein something together :p
Futhark can be compiled to a C library
Ah, ok. Was thinking it would require roc compiling to cuda or opengl or similar
I guess that is really no different that roc interacting with a c platform that happens to use cuda
Uhu, I had something like that in mind.
Anton said:
Also, just curious, does any language actually do this in the type system currently? Like encode array shapes
Finally got around to researching, I think it was Go I was thinking of. I don't think they do full shapes as types, but they do encode array length as part of the type of the array. I haven't used Go so that's why I forgot which one it was.
Count me as someone that would love to see customized infix operators for the use cases where I'd want to use Roc (combination 3D game like rendering/engine + scientific computing to provide the data).
I won't press the point, because you all are far better suited than I to evaluate the pros and cons, but not having it would be a bummer for me.
what are some specific examples of code you'd like to write in Roc that isn't possible today?
Richard Feldman said:
so then the question becomes, how big of a concern is the ergonomics gap between
a + band (for example)Mat2d.add a b
I would expect it to be possible, just not ergonomic. I don't think my use cases are that much different from what's been mentioned already. But when making demos, you sometimes want to pull in some random technique to use for this demo that the underlying platform may not provide directly.
The above quoted example is overly simplistic in my opinion, if I'm just doing addition, then I agree, it's fine. I may also be misunderstanding what's possible today, apologies if that's the case.
However, let's take your example slightly further with something like a fourth order integration Runge Kutta and show the differences between your method based syntax and something like C++.
Equation:
My imagined Roc syntax:
y_n1 = Vec3d.add y_n (Vec3d.mul (Vec3d.div h 6) (Vec3d.add (Vec3d.add (Vec3d.add k_1 k_2) (Vec3d.mul 2.0 k_3)) k_4))
What I want:
y_n1 = y_n + (h / 6.) * (k_1 + (2. * k_2) + (2. * k_3) + k_4)
I can super easily verify the correctness of the last one. But the syntax for Roc is quite difficult for me verify. In fact, there is an omission in there, which was not intentional, and I didn't notice until I was typing out the syntax I want.
Hrmm, is the zulip latex/math support not working here? (edit: it is working, I just managed to mess it up somehow here, Edit 2: \o/)
For the record, I'm a fan of the scala infix idea, even if it comes with its own problems, and is probably not at all applicable here.
fantastic, this is exactly what I was hoping to see! :grinning:
which Scala infix idea? :sweat_smile:
Richard Feldman said:
which Scala infix idea? :sweat_smile:
In scala:
T + Y
Is just syntax sugar for
T.+(Y)
ah I don't think that would help in Roc
I don't either.
infix operators are all just sugar in Roc anyway; the question is what they desugar to :big_smile:
I also chose the example I chose, because the types are not the same type throughout. There are scalars in there and Vec3d's. I don't know if Roc has function overloading based on types. If they don't, then I should update my example to distinguish when I'm using a scalar vs Vec3d
Yeah, roc doesnt
I think it would be interesting to explore what the minimal change to the language today would be that allowed support for the "what I want" example above
like what would the type of the Num.add function have to be?
like for example, this wouldn't work with that example:
add : a, a -> a where a implements Add
because mixing scalars and vectors wouldn't work
although that's true today...is there some world where we could have scalars unify to vectors?
I think I have something to say here. In a sense, you can consider scalars to be merely a special case of vectors, or more accurately, matrices.
We would need implicit broadcasting or similar
So there is a perspective where all numbers are just 1x1 matrices. You end up with all your standard operations working as expected, and even more complicated functions like cos or exp
So while I expect it might be considered a bit obtuse or impractical for a type system that wants to be simple, you can consider nums a subtype of mats
Also, I think you would need either overloading, higher order abilities, or a generic and less performant wrapper type for this to work.
yeah an example that basically can't work in Roc's current type system is if you give Num.add two concrete types, one of which is a scalar like U64 and the other of which is a concrete vector type
they're just not type compatible
You would have to do something terrible like this in current roc:
Num.add a, b -> NumericArray where a has BroadcastToNumericArray, b has BroadcastToNumericArray
Which is also still pretty terrible cause everything is just being broadcast to to the same generic type. Also, it is missing the sub numeric type (int, float, etc)
I don't think it's realistic to consider introducing subtyping to the language; that's practically equivalent to changing type systems
yeah that add type would be unpleasant to use in all use cases :sweat_smile:
That's not exactly true. That is exactly how most duck typed languages work. So it would be as usable as python, matlab, array languages, etc
Also, the NumericArray type would still have a tag in it with info on the underlying type, shape, etc
at runtime? That would destroy numeric operation perf, wouldn't it?
like how big is a U8 in memory in this design?
oh yeah, for sure. That is the case in most of these languages (though they have some smarts to try and minimize that a bit)
yeah let's rule that out haha
Totally agree, just pointing out what it would take in current roc (no type system support)
To do something nicer, you would need a lot in the type system
the only way I can think of to make the a, a -> a where... design work with both scalars and matrices for different arguments is if scalars always have unbound type variables
This is helpful to look at: https://nalgebra.org/docs/user_guide/vectors_and_matrices
like it's U16 a rather than U16
What it takes to do in rust
which sounds painful
Can see lots of quite complex traits that check dimensions and lots of operator overloading
You need abilities like this if you want to handle shapes statically at compile time: https://docs.rs/nalgebra/latest/nalgebra/base/constraint/struct.ShapeConstraint.html
ok that said, number literals are actually a different story - those do have a type variable, and I believe it would be possible to get those to unify with something as long as they had the same base type (e.g. if a vector type was a builtin and was a type alias of Num)
at which point if you could write myMatrix * 2
but you couldn't multiply it by a named variable that had a concrete scalar type like I64
you'd have to convert that to a matrix/vector first explicitly
Wouldn't that be like saying you can get an F32 to multiply with an U64. Doesn't matter they are both subtypes of num, you still need an explicit cast.
it wouldn't affect concrete types at all
it's like how today you can do 42f32 + 1 but not 42f32 + 1u64
it's the same idea - you could do myMatrix + 1 but not myMatrix + 1u64
ah. So implict broadcast if not typed.
I wouldn't call it broadcast, it's just the same unification we do today
Though if you are creating a full matrix in memory for 1 when doing myMatrix + 1, you have a big memory problem.
basically it'd be putting matrices and vectors into the same Num hierarchy we have today
yeah that's what would happen there
good point!
there'd probably be a way we could optimize that out
Also, is this assuming only dynamic matrices in the type system, or both static and dynamic?
Please correct me if I'm wrong, but it seems like we're mostly discussing how to mix scalars (based on types which already exist in the language) with vecs/matrices/tensors. Would these techniques also allow customing infix operators for other types?
they're separate questions really
I'm still on the "what's the minimum change that could get us to the 'what I want' example above?"
is there a separate use case from that where you'd want to overload operators for a different purpose?
@Brendan Hansknecht I think both static and dynamic could work with that idea
If we did that, would we also add other numeric types to make them more useful, like complex numbers?
Note, not exactly a serious question, trying to note that there is a slope here with a lot of potential requests
I'm open to it in principle, depends a lot on the specifics I think
like I don't want the html docs for the Num module to be an 80mb download, but if there can be major ergonomics improvements to certain use cases that only require making some changes to Num that largely don't affect current use cases, that seems worth exploring
a good example of where to draw that line which we've talked about in the past is arbitrary sized integers
For sure. Given Rocs limited focus in this area, I definitely feel this would be best if we could enable it in userland.
That said, I think it is easiest to get kinda okish support in the biultins. but it will be limited for what many many users want.
the question I posed there was: what's a use case where all of the following are true?
I128 is too small because I'll get overflow if I'm limited to undecillionsso I'd want to ask similar questions of other use cases that could be considered for builtins
like how else could it be done, etc.
I'm open to the possibility that user-defined operator overloading is the right design, but it's low on my preference list
yeah, makes sense
And I agree that it isn't a clean problem.
Just saying that if we can find a userspace solution even if not as ergonomic as builtins, I think that it would expand support and flexibility enough that it is actually more useful.
it wouldn't work for adding matrices and scalars though
for that to work, without changing type systems altogether, I can't think of another way to do it besides matrices being part of the Num hierarchy
at which point I'm not sure what the remaining use cases are for operator overloading besides complex numbers
they wouldn't work well for userspace units of measure because of things like "6m / 2s" - without that being done at the language level, + couldn't give "3 m/s" as an answer
so maybe it's something where there's a long trail of candidates, but I kinda doubt it. Seems like there would be a small and finite number of them (e.g. if complex numbers are in, do you need a separate "Ratio" type for fractions?)
One other note, which may or may not help, I'm fine if all of the customization of the operators comes from a platform package. So long as it can provide infix operators
So what does a * b do? pervasive multiplication, matrix multiplication, dot product, other?
Do we handle shapes and all of type checking for matrix multiplication?
If this isn't matrix multiplication, how is that done infix? Can it be assigned to a symbol?
This are the kind of things we have to all for tons of operations
@Lakin Wecker do you have other use cases in mind for operator customization besides matrices and vectors?
just to be clear - so far the only viable design idea that can handle adding scalars and vectors would involve making matrices and vectors part of the language builtins, and not introducing any userspace capabilities for customizing operators
(incidentally, I don't think operator behavior should be coupled to platforms as a concept)
good question regarding matrix multiplication - how could that work in terms of types if the rows and columns are different? :thinking:
You need a type system that can do more complex dimension matching and such
This:
pub fn tr_mul_to<R2: Dim, C2: Dim, SB, R3: Dim, C3: Dim, SC>(
&self,
rhs: &Matrix<T, R2, C2, SB>,
out: &mut Matrix<T, R3, C3, SC>
)
where
SB: Storage<T, R2, C2>,
SC: StorageMut<T, R3, C3>,
ShapeConstraint: SameNumberOfRows<R1, R2> + DimEq<C1, R3> + DimEq<C2, C3>,
So yeah, lots of complexity in the type system or giving up and just doing dynamic tensors
yeah I can't think of a way to make that nice without type level arithmetic
er wait, nm
If we have a builtin type that is a static tensor, even if * is pervasive multiplication, how would an end user define matrix multiplication in general (can it be done generically or would it need specific types?)
you could theoretically have mulMat2d : Mat a b Mat b c -> Mat a c right?
Also, simplist example of needing type level arithmetic is joining two matrices
Where the outermost dimension merges
(or something like that; I forget how the rows and columns line up, but the point is that the answer's number of rows and columns always appears in the arguments)
yeah so multiplication could get away with type level numbers but not type level arithmetic
another possibility: the dimensions aren't specified in the type, but you have like Mat2d a, Mat2d a -> Mat2d a for dynamic matrices, and then a ** operator which desugars to that
so then you'd still have an infix operator for matrix multiplication, it just wouldn't be *
Yeah, this is just where you lose perf on all small array types of ops because things are dynamic. Maybe not a problem, but definitely would push end users who need speed to define static versions. Also, no need to specify it is 2d technically (could just be generic fully).
Richard Feldman said:
Lakin Wecker do you have other use cases in mind for operator customization besides matrices and vectors?
just to be clear - so far the only viable design idea that can handle adding scalars and vectors would involve making matrices and vectors part of the language builtins, and not introducing any userspace capabilities for customizing operators
I would expect so, but I'll need to think about it. I can come up with some hypothetical ones (vector spaces are often defined over a scalar field, but that scalar is not always a real number AFAIK), but it's unclear to me if I would be able to give you such a clear example of how I would use it off the top of my head.
fair enough! :+1:
yeah regarding losing speed due to specifying dimensions at runtime - I don't really see a way around that
but then again, we did note that for games there's already a way to get maximum perf using tuples today
for small matrices that you'd want to be on the stack
so one possibility is that someone makes a separate package for stack-allocated matrices which doesn't support infix operators
ok
yeah, mostly asking questions to verify the ramifications
I think if the goal is to make roc mostly useable, but maybe not max ergonomic or perf, adding a dynamic tensor type to roc directly that allows all of the number infix operations as pervasive operations. Maybe adds a few extra common operations and then also enables an end user to make a full featured library for all the other lin alg ops (just not infix) probably will satisfy most users enough without adding much complexity to roc. Also, if the tensor are dynamic, we can avoid the allocating a full matix for the 1 in m + 1. It can just note that it is a scalar in the shape and then handle broadcasting dynamically.
the other example I can think of is Clifford Algebras as an alternative to the typical linalg stuff
I'm not familiar with those...what would that look like in code? :sweat_smile:
I forget the details, but they're an alternative to linear algebra that solves the same set of problems. But where linear algebra has a few hacks in it that we just "get used to" to solve certain things in projective spaces, clifford algebra solves them more elegantly. But it's slightly more complicated. There are more primitives and more base operations and there is an explicit way to move between dimensions with some of the operators. Regardles, it's probably fine to just make this a user space library. However, without infix operators I don't know if anyone will. But again, I agree that this probably isn't Roc's primary use case
I think it's often also called Geometric Algebra
It's probably a side tangent so I'll move any further links to it to the #off-topic stream
So Richard, as a pivot of conversation, what about the alternative idea of enabling arbitrary infix ops with something like the 'func' syntax or similar
Cause that is probably the best way to enabling keeping everything out of builtins without tons of type system complexity
Of course with some hit to ergonomics for non-builtin numerics
back in the day I was a major advocate of removing that syntax from Elm :big_smile:
in general I'd rather add things to builtins than add that feature, I'm really not a fan
Interesting. Ok. Cause I think that syntax enables more generality for end user libraries, but I get how much it can be misused or hurt readability if you aren't used to it.
it's not just misuse, it's how the design interacts with things like |>
Does it need to?
e.g. if we added it and used the Haskell backtick syntax, any of these would become valid:
Str.endsWith name "iko"
name |> Str.endsWith "iko"
name `Str.endsWith` "iko"
to me, the latter two are very similar in terms of how they read, so how much value is it adding to have a whole separate language feature? And then what about situations like this?
foo
|> bar `blah` baz
basically if you already have |> it seems like mainly what having infix syntax does is force you to ask the question over and over "which should I use here?" when the difference is very small
fair, |> is a more verbose infix, why have both
how is + implemented now? It's syntactic sugar for what?
Num.add
How does it dispatch between the various numerical types?
Also, I found this right after asking. :man_facepalming:
The signiature is add: Num a, Num a -> Num a. No sharing between types. But you can pretend that Add is an ability
That is basically how it works
Just for comparision:
y_n1 = Vec3d.add y_n (Vec3d.mul (Vec3d.div h 6) (Vec3d.add (Vec3d.add (Vec3d.add k_1 (Vec3d.mul 2.0 k_2)) (Vec3d.mul 2.0 k_3)) k_4))
With |>, correct type conversion, and importing all functions unqualified:
y_n1 = (k1 |> add ((w 2.0) |> mul k_2) |> add ((w 2.0) |> mul k_3) |> add k_4) |> mul (h |> div (w 6)) |> add y_n
And with proper infix and builtin to deal with types, but same ordering:
y_n1 = (k_1 + (2 * k_2) + (2 * k_3) + k_4) * (h / 6) + y_n
Only the second 1 could be written today in roc. The top one, would hit type issues due to not wrapping numeric constants.
Finally, with a infix syntax:
y_n1 = (k_1 `add` ((w 2) `mul` k_2) `add` ((w 2) `mul` k_3) `add` k_4) `mul` (h `div` (w 6)) `add` y_n
With custom infix that can use symbols as mentioned above:
y_n1 = (k_1 `+` ((w 2) `*` k_2) `+` ((w 2) `*` k_3) `+` k_4) `*` (h `/` (w 6)) `+` y_n
Personally, I think all without symbols are pretty terrible. Even infix without symbols.
So yeah, I think if we want to make this readable, we need one of:
Add etc abiltiesI think actually now, I am leaning towards just defining + as Num.add a, a -> a where a has Add. Then leaving everything else to userland.
This just means that you have to define a wrapping function. You would get userland matix code that looks like this:
y_n1 = (k_1 + ((w 2) * k_2) + ((w 2) * k_3) + k_4) * (h / (w 6)) + y_n
Note: this still has limitations around more custom matrix ops which would still need names like dot product. But at least for all of the basic math, you get really nice operators and everything is pretty much in userland and applicable to any type including complex numbers.
Note, in all of the above, w is my wrap function. defined as w : a -> Tensor a. It just turns a scalar into a tensor with 1 element. From that point forward, all the dynamic shape stuff and broadcasting can automatically be handled and all the types will match.
I guess you would still miss out on things like m < 0 which is a pervasive less than that that goes from Tensor a -> Tensor Bool. Cause even if we added a LessThan ability, it would return a Bool and not a Tensor Bool. So definitely still has limitations that may be annoying. But at least you get basic math
one idea I had a long time ago was "what if you could customize operators but if you did one, you had to do all of them"
in other words you don't have an Add ability, but rather a Numeric ability which obliges you to implement add, subtract, multiply, maybe others
basically to discourage using them for DSLs
and to make it clearer that they're intended to be used for math only
another thing I could see people asking for if Add exists: giving Add to Str and List
Assume you have a: Vec2d and b: Vec2D, what's a ^ b ?
yeah I don't think pow or div could be in that list
Richard Feldman said:
another thing I could see people asking for if
Addexists: givingAddtoStrandList
From categorical theoretical (or abstract algebra) point of view, this is a perfectly normal thing to ask for.
If I'm not helping the convo, feel free to tell me so. I'm thick skinned. I do want to be practical.
it's a reasonable request, although if we did that, are we going to call it Num.Add at that point? It'd be weird for Str to implement the Num.Add ability.
if it goes in its own Add module, and is used in builtins for other things besides numeric types, then builtins are sending a strong signal that using them for DSLs is fine
oh your comments are great so far! It's great to have a different perspective :hearts:
Yeah, once you go down a certain path of generalizing the operations you give people for syntactic sugar, people will make DSLs.
Is that something Roc is trying to avoid?
category theory aside, plenty of languages support + for string concatenation, and possibly also for combining arrays.
historically Roc hasn't, and it feels like allowing that would change the feel of the language somewhat, but I don't think it's out of the question
When I first saw:
print("a" * 80)
in python I was very confused, but now that I understand where it comes from it's quite nice. So I think it's quite nice to have the ability to implement certain types of operations in a general way for certain types. It also raises the question though: do you support competing instances of an implementation of a same typeclass? I'm assuming Roc does not right now. They're useful in some scenarios.
my experience with DSLs that use infix operators is that if you say "try really hard to come up with an alternative design that doesn't use infix operators," it's consistently (but not always, to be fair) my experience that the DSL ends up having a nicer design if you don't use infix operators
hm, what's an example of competing instances?
There are multiple ways to implement addition/concatenation (if you see it as a semigroup) for strings. Even for numbers. For example, always taking the maximum/minimum element is a valid semigroup, in addition to the usual definition.
Whether or not you want that operation represented by the infix + is certainly debatable. I think I'd be on the side of not wanting it to potentially mean different things in different contexts. Scala 2 allowed for this with it's implicits and it was kind of a mess and they rolled it back a bit in Scala 3
oh, so in Roc you have to define the abilities that go with a type right when you define the type, so yeah in that sense competing implementations aren't possible
I'm asking chatGPT about how operator overloading is used in other languages for non numeric purposes and I'm learning about all sorts of uses of + I didn't know about! :big_smile:
e.g. in Python it combines two dictionaries
also * in Python not only repeats strings, but lists too
If you consider * to be many applications of a +, then it makes sense that it would work wherever + is defined for two types
AKA, if you have a semigroup implementation, then you get a * with a integer, which just represents multiple applications of + to the same object with itself, or basically a repeat combined with reduce/fold (depending on which version of those terms you use)
what should "foo" * "foo" evaluate to? :big_smile:
["ff", "fo", "fo", "of", "oo", "oo", "of", "oo", "oo"]
I think it's almost certain a type mismatch would be more helpful there, although you never know... :laughing:
I agree.
I was going to try and put together a simple demo that uses the above example I gave in a nice way, both to try out Roc and to give you a working example to use as motivation, but with the platform stuff being more work right now, I dunno when I'll get to it. Would something like that still be useful?
sure! Would the basic-cli platform the tutorial uses work for the demo?
I'd like to be able to draw filled in circles and lines, so I would expect not. But then again, I haven't looked at it in detail.
I could probably come up with an example that does work with that, but not the one I was thinking of.
Lakin Wecker said:
Assume you have
a: Vec2dandb: Vec2D, what'sa ^ b?
This should work if the shadows match also, matrix power can be super useful
:star: Shapes
wouldn't matrix power need a scalar and a matrix? :thinking:
Yes
But it is quite useful at times
Could use a scalar wrapped in a matrix if needed for types to wrok
But also points towards how many ops have multiple overloads that would be wanted in different contexts which makes things even harder to be a built-in of the language
huh, something interesting I just realized about the Add idea: we could automatically infer Add for any tuple where every element has Add
Same for records
true!
fp-ts has this for its typeclasses.
and same for other operators, although I'm not sure that's actually what you want in those cases
(I'm sure haskell does too), It's very convenient in certain situations. For example, merging duplicate records in a database can be viewed as a semigroup operation
And it's a great place where you might want different semigroup instances for a given type, depending on your use case, rather than one for the type.
for a specific example, if your records is a User, and they have both a createdAt and modifiedAt attribute which is a DateTime field, you'd want the minimum one for createdAt and the maximum one for the modifiedAt
you definitely don't want this on infix operators like + though, probably best to have that explicitly defined some other way
that sounds more error prone than convenient to me overall :sweat_smile:
It's done in a very explicit manner. You construct the operation directly. It's not at all implicit
hm, maybe I'm not thinking of what you have in mind then
In this case I'm just pointing out that being able to prove that some type T satisfies a set of requirements/laws/typeclass in multiple ways is useful. And if you have an Algebraic Data Type that's fully composed from type T's that satisfy those requirements, you should be able to derive the proof for the ADT as well.
a concrete problem with driving * automatically is that if you're doing it with two vectors of the same size (and representing them with tuples) it'll do pairwise multiplication and work the way you'd expect, but if you're doing it with matrices (also represented as tuples) it's not gonna do the right thing unless maybe they happen to have the same number of rows and columns
But it's probably a bit of a tangent, because I don't think all of this should be done with infix operators
Richard Feldman said:
a concrete problem with driving
*automatically is that if you're doing it with two vectors of the same size (and representing them with tuples) it'll do pairwise multiplication and work the way you'd expect, but if you're doing it with matrices (also represented as tuples) it's not gonna do the right thing unless maybe they happen to have the same number of rows and columns
Yes, this is why libraries like Eigen3 have both an Array and a Matrix view. Sometimes you do want the array level version, and it's convenient to use the operators to do it. But most of the time you want the matrix definition, not the automatic one.
Lakin Wecker said:
But it's probably a bit of a tangent, because I don't think all of this should be done with infix operators
It's still a tangent, and this tutorial is out of date, but this is where that idea comes from: https://dev.to/gcanti/getting-started-with-fp-ts-semigroup-2mf7
In that case the getStructSemigroup is you requesting it to be constructed, but you're also giving it explicit instructions. So it's not quite a "derivation".
I'm not sure how we could get * to work with a tuple-based matrix and have the return type work as expected
Honestly, matrix muplication is an odd one. We (myself, and other people used to C++ or similar) expect it to work like a multiplication, but matrix multiplication is not the same as scalar multiplication
If you have typed shapes, then it's definitely not a semigroup
anyways, the details of all of this are where my understanding starts to fall down.
It's why I tend to just use new languages rather than design new ones. :eyes:
I'd much rather have user-space ability to customize operators than automatically derived operators
Could we do something like this:
At the top of a file where you want to do matrix multiplication you import a Matrix package and then write
* = Matrix.mul
and then every occurrence of * in that file means matrix multiplication.
And if you don't do that, you get the default implementation which is equivalent to
* = Num.mul
Although I suppose that's similar to what Richard argued against with C++ << for streams.
Where is the argument against << with C++ streams?
I'd like to read it
I saw Gosling talk about it in an interview, although it's been years and I don't remember which interview :sweat_smile:
he didn't talk about << specifically, just the general "the way I saw it used in C++ made me want to remove it for Java"
although worth noting that for dynamically sized matrices, * could be implemented on those opaque types with no problem
the issue would be tuples and auto inference
or I guess in general matrix types with static dimensions baked into the type, as opposed to dynamic ones
because that's what breaks the type of the mul function (so, not really related to infix operators at that point)
Richard Feldman said:
he didn't talk about << specifically, just the general "the way I saw it used in C++ made me want to remove it for Java"
Interesting. I like them in C++. :shrug:
They're not perfect, but I'd rather have them than have whatever java turned into. Edit: This wasn't a useful comment.
Java is fine, for the record. I just would never use it over C++, personally. And one of the reasons is due to the lack of user-space customizable operators. Maybe they have that in newer versions? I dunno.
It's why I skipped to Scala and one of the reasons why I like scala
I thought of a way to articulate something that bothers me about overloading arithmetic operators for non-numeric use cases. I'll use + as an example because overloading it for strings and such is used in several popular languages.
if I look at Roc code today, almost all function calls are one of the following:
Str.concatblah, not Foo.blah) where the function is defined in the current scopethe primary exception to this is DSLs, where I'm importing (for example) a bunch of functions from the Parser module so I can use them in an unqualified way when building parsers.
in all of these cases, I can tell quickly which function is being called. However, if the function is part of an ability (e.g. Bool.isEq, or the much more common == operator that desugars to it) then I no longer know what the implementation of that function is. Now I need more context: I need to know what the types are of the things I'm looking it just to know what code is being run at all
with the current builtin abilities, that's not a real problem in practice. It's not a big mystery to me what == does on all the builtin types, and if someone implements equals on a new type, they're almost certain to implement the expected equals semantics for it. There might be bugs, sure, but they're not going to implement the Eq ability on something in a weird way because they happen to want a function that takes 2 of their type and returns a Bool and == will look nice in their DSL, so they implement Eq in a way that has nothing to do with equality. That's extremely unlikely to happen because they'd know how many unintentional bugs it would cause.
as a consequence, when I look at == in Roc code, even if I don't know the specific implementation being used by the type on either sides of it, I still know what semantics to expect: equality
so this is a really nice property for being able to glance at code and quickly build an accurate mental model of what it's doing
today, I also have that with the + operator: it's doing mathematical addition, done. It's not customizable.
supposing it becomes customizable, and it gets used for non-numeric types, I can no longer look at foo + bar and necessarily know what its semantics are.
to be fair, the examples of how I've seen this used in practice in other languages are generally pretty easy to understand
like str1 + str2, ok sure, it concatenates the strings
or list1 + list2, again, concatenation, sure
then in Python, dict1 + dict2 - ok, it unions the dictionaries...but now I do have some questions, like: what if both dictionaries have the same key, and different values? Assuming the dictionaries don't allow duplicate keys (which they don't in Roc, although some languages do allow that), you have to pick which one "wins." I assume it's the second argument, but now we have three pretty distinct uses of what + means, semantically:
and that's just builtins; the more uses people come up for it in userspace, the less and less confidence I can have of the semantics of what foo + bar is doing without knowing more context surrounding it
this gets into DSLs and my concern about operators in them
suppose I'm reading someone else's code, and it says like prefix / suffix
today, I'm like "cool, division"
but in a future where that's allowed, I think a very plausible thing that might mean is "this is a URL fragment or a file system path, and someone made a library where as a convenience they define / to mean 'concatenate these two together, and as a convenience, join them with a '/' if they otherwise wouldn't have a slash between them after concatenation"
personally, I wouldn't do that in my URL or Path implementations, but the thing that bothers me about allowing this is that my knowledge that someone else might have done it means my brain always has to be more cautious around all operators
because I can no longer jump instantly to "I know what the semantics of this are" like I feel I can today
anyway, that's not to say I view this as a flat-out dealbreaker or anything, I just thought of a way to articulate part of my discomfort with the idea, and wanted to add it to the discussion :big_smile:
I think this really is the general topic of tradeoffs of DSL. I often find that DSLs are great when they are clearly boxed off. That way, the "weird" behaviour is limited to a very small area of code (preferably smaller than a full file most of the time). I think a well scoped DSL is awesome in these case.
Think of a rust macro that does html templating or other small scoped rust macros that give you a totally different synctax for a small area of code.
I think this area may be more work exploration for roc. Some way to clearly signify that some small area of code can have custom infix operators to make it nice for a DSL. Then any end user or library author could define "MatrixMode" or geometric algebra mode or etc. They can then define infix x as matrix multiplication and infix ⋅ as the dot product, etc. It would be super clear to end users that they are in a special mode and with a simple go to defintion, they could see exactly how each infix operator is defined within that special block.
This is just a rough idea, but I think it has a lot more promise that has both ergonomic and could fit roc nicely
Richard Feldman said:
yeah I don't think pow or div could be in that list
Dear god you guys talked a lot while I was asleep and at work. And regarding this (if you haven't abandoned this all by the time I get to the bottom of the thread), maybe div and pow could have additional abilities that require a Numeric ability to be already implemented?
Richard Feldman said:
huh, something interesting I just realized about the
Addidea: we could automatically inferAddfor any tuple where every element hasAdd
This generalises for other operations. Be careful though, consider what 3*[7, 5, 2] will do. Is it [7, 5, 2, 7, 5, 2, 7, 5, 2], or [21, 15, 6]? I prefer the later for generalisation's sake, but it does mean we shouldn't do anything like the former.
Also, regarding non-Numeric types using mathematical operators, I agree that random types shouldn't have it. However, matrices basically already are numbers mathematically speaking. They have addition, subtraction, and if limited to square matrices of fixed size, multiplication, division (with the caveat that you can't "divide" by a matrix of determinant 0, equivalent to dividing by 0 ), even exponentiation, logs, etc. I like the idea of restricting their use to some sort of type that forces you to implement them as a system, however the issue with that is there's many objects that naturally only have addition, or only multiplication, or addition and subtraction, or addition and multiplication, or addition multiplication and subtractions but not division, etc. There's still a structure there (subtraction is nearly always derived from addition as its inverse), but it requires more thought. The looser the rules, the more structures it can represent, but the more risk there is of someone misusing them.
Also, @Richard Feldman what are the issues with matrix size being known at runtime/compile time? Basically all the operations I can think of on matrices (and tensors) involves dimensions being added, removed, multiplied, and subtracted. The output shape should always be determined by the input shapes, and those dimensions should either be explicitly calculable at compile or expressible as an abstract polynomial and thus still compile-time checkable. Is it purely an issue of runtime memory allocation? I'd love to know a little more about the problems you'd need to solve/bypass to make this all work, from a type perspective.
BTW you can include quaternions to list of algebraic objects that are common in software that basically need infix operators to not suck. They get used to represent 3D rotations in a very compact and effective way, their use in 3D software has sort of catapulted them to renoun. Like complex numbers, but 4D.
Having a nice editor plugin to render (and eventually edit) math in roc may be the most ergonomic, best looking, and least complex solution.
With the way you feel about DSLs, you probably won't like Scala much :joy:. as they have a ton of them. Regardless, in Roc, you must still need to know what the types are to understand what functions do, even now? x / y is not defined between all integers. But I'm assuming it's imlemented for integers. So does it round or ceil or floor? / alone is not enough to tell me this, I need to know what the types are. Same goes for addition or subtraction. x - 5 <- will this produce a negative number or not?
So you still need context, the only difference is that it's language provided context which can't change. So you memorize it and move on. In my experience with Scala, I do end up needing to understand the types to know what the semantics of a function are, but I also know exactly what it desugars to in each instance, and my editor treats them all as function calls. So I can use my editor's tools to bring up the docs for that operator to understand what it's doing.
However, I'm fully in agreement about the implicit nature of it. I want to know where these operators came from. So either explicit imports of the operators, or some way to opt in. I'd be fine with a localized rust-like macro system where in side of it you can use customized operators for user space types, or just a way to explicitly import them at the top of the file in an unqualified way.
Interestingly, your views on customized infix operators is very similar (or at least reminded me of) zig which advertises No hidden control flow. and do not have customized infix operators as a result.
I only bring it up as it might be a source of more information as to why they made this decision, or looking at how people reacted to it might be a source of motivation for why to pursue or avoid this design decision. For example, this reddit thread mentions a language called Odin which apparently has built ins for matrices, and apparently does component wise mathematical operations for arrays for vecs
Anyways, I also need to get back to work, lol. So I'll stop filling your inboxes while you all work.
I didn't expect this, but I was listening to an interview and encountered a case of a game programmer who puts a surprisingly strong value on operator overloading for vectors as well as SIMD: https://www.youtube.com/watch?v=566I_Soio5I&t=3600s
summary of the clip: he prefers C to C++ in general, but uses a C++ compiler specifically in order to get operator overloading for vectors and SIMD in games, and he's not interested in a language that doesn't have operator overloading for those use cases
I have definitely heard that argument before. Would use C, but C++ has a few features that make it worthwhile. Generally the features I hear are:
i guess that makes sense why odin has it then.
Yeah, it's really absurd just how much infix operators help with vectors maths and its kin. Back in uni I had to build a crappy linear algebra engine in C++ over a course on physical simulations, and in spite of how little I knew about the language I immediately dove into figuring out how to do operator overloading (with a bunch of very annoying edge cases) because otherwise it was miserable.
The chance of errors without it skyrockets because you need to do a bunch of mental transformations and decompositions of mathematical formulae just to write straightforward expressions. And yeah, SIMD makes vector ops go vroom, and most things can be vectors if you try hard enough. Representing even simple equations without infix operators can be so clunky and awkward as to be straightforwardly the wrongway to do it, or as close as you can get in the world of function notation.
And those use cases contain basically everything that has to talk to a 3D engine (for a sense of how frequent that is, just the games industry is bigger than basically all legacy media combined, by revenue and users), or any sort of maths/physics/engineering code (see: any company, firm, gov department, or university department that could reasonably be categorised under STEM). This is very far from a niche application and if Roc is successful it will probably need to talk to these systems, even if not used within them.
Brendan Hansknecht said:
Having read this whole thread and having some skin in the game as someone who is a big fan of well designed domain specific dsls. I think this is a great choice. A generic solution here might be something a little like ocaml's let open X in syntax as a way to open the entire contents of a module in a certain scope. That way you just don't allow importing those infix operators in the global imports list but you have a way of dumping them all into a local scope
Could be handy for other use cases as well like html dsls that don't use infix ops but do require a stupid number of imports that really clutter the global scope
@Eli Dowling How does that differ from something like from package import *? Because I know that's a pattern Roc explicitly won't support with its import structure by design.
@Declan Joseph Maguire It would be in effect allowing import *, but only within a local scope and never at the top level.
So you'd never have a problem where you're using "import *" and consequently import all the things that got "import *'d" by the thing you're importing? If so, that certainly sounds like it'd fix many of the problems with wildcard import, especially regarding linker speed. I started a different thread about wishing there was some intermediate between wildcard and no wildcard, and this sounds like it'd do that.
However, I don't know enough to say whether it'd still retain other issues I'm less familiar with.
D'ya reckon it's worth bringing this up with the BDFN? Like it's a pretty decently big change, but it definitely seems worth discussing, @Eli Dowling @Brendan Hansknecht you seem to understand this pretty well.
Operator overloading is similarly useful for scientific programming, where datastructures with natural definitions for +,-,* etc are often encountered, like matrices or polynomials. As with game programming, having some form of operator overloading is super necessary to make the expressions remotely readable. I would really love to use roc for scientific applications one day, but without operator overloading this is simply not possible.
I actually did not like the way Haskell's typeclasses implements operator overloading, since it makes too many assumptions about the underlying datastructure. Suppose I wanted to use the Num typeclass to represent matrices. Then I am required to implement abs and signum, but there is no sensible definition for these.
Interesting. I guess I come from ml where those have sensible element wise definitions
Right, for general tensors of numbers, you can always make elementwise definitions. But if you want to think of * in terms of matrix multiplication then this gets not so easy. If your matrix has an eigendecomposition then maybe you could have a diagonal matrix containing the signs of the eigenvalues? But if your matrix is not normal then this doesn't make sense. Perhaps a better example could be polynomials, which don't really have a notion of signum.
Looking around on the Zulip, it seems that custom infix operators have already been discussed extensively. I recognize that this is a challenging language feature to design, but leaving it out would alienate a big part of the scientific and mathematical community. I would be so excited to benefit from the speed and reliability of a strongly typed functional language that languages like python/matlab/mathematica simply don't provide.
This is also where you get the split between * and @. For many libraries * is matrix multiplication and @ is matmul
But it isn't conaistent
And yeah, clearly infix operations are important to some groups
Personally, I think I'd we were to support them, the most likely would be some sort of generic numeric ability. Maybe broken into a few subgroups, but not too granular. We don't want it to easily be used for arbitrary things, so would try to restrict it if possible. Making one large numeric group that has to be implemented a unit might help clarify which types are reasonable to implement it.
Another potential (though I think much less likely to succeed) is adding tensor types (and I guess complex numbers) to the standard library.
If either of these are added, I think they would be added in quite a lagging cadence. First we would probably want to see the roc userland libraries and clear want for the use case. Beyond a handful of users, but a larger community building things and hitting friction. There is a strong case that other reasons might restrict roc from these domains. If that is the case, it may not be worth customizing for these domains.
I see. I guess I personally disagree with the idea that algebraic operations should always follow the Num pattern, or some other combination. There are a lot of algebraic structures other than rings out there, some of which are very minimal, and it won't be possible to anticipate them all as a language designer.
I am also pessimistic about the idea that the scientific community will ever communicate a "clear want for the use case". They are usually not so vocal, and they are just not going the language if it doesn't have the features they need.
Patrick Rall said:
There are a lot of algebraic structures other than rings out there, some of which are very minimal
what are some specific ones that get used in scientific computing?
I understand the general sentiment, but this is an area where adding flexibility for one audience has downsides for everyone else, so I want to understand what the specific benefits would be that would outweigh the downsides of that flexibility :big_smile:
For an overview I would recommend looking at an abstract algebra textbook. My go-to is this one. It is really remarkable how many mathematical structures have natural interpretations of multiplication, addition, etc. There are also some examples from physics like annihilation and creation operators, and certain optimized representations of quantum states that come to mind. I myself am only familiar with my area of math, so I can only point to the ones I know, but the discipline is very broad. This is why I don't think it is a good idea to try to anticipate all common use cases here - you simply won't know all possible applications unless you know everything about scientific computing, which certainly I don't.
What are the downsides to adding such a capability? Roc already infers if a numeric type is floating or integer from context. Why can't such a system be made more extensible?
one downside is types getting more complex
for example, today Num.add is Num a, Num a -> Num a
instead of something like a, a -> a where a implements Add
another is numeric operators getting used for DSLs that have nothing to do with math
which has both a stylistic consistency downside and also compounds another downside, namely that looking at x + y no longer tells you as much about the types (e.g. today you know that both x and y are some flavor of Num)
Patrick Rall said:
For an overview I would recommend looking at an abstract algebra textbook. My go-to is this one. It is really remarkable how many mathematical structures have natural interpretations of multiplication, addition, etc. There are also some examples from physics like annihilation and creation operators, and certain optimized representations of quantum states that come to mind. I myself am only familiar with my area of math, so I can only point to the ones I know, but the discipline is very broad. This is why I don't think it is a good idea to try to anticipate all common use cases here - you simply won't know all possible applications unless you know everything about scientific computing, which certainly I don't.
I appreciate this perspective, but another perspective that's important to consider is that the most likely outcome is that no matter how many features we add to Roc to accommodate scientific computing, everyone continues to use Python anyway
see for example Julia adoption despite being designed explicitly to cater to that use case
so my mindset here is not "how can we make the nicest experience for scientific computing, even though that would have downsides for the rest of the audience" but rather "are there ways we can make things nicer for the scientific computing audience in a way that has minimal downsides for everyone else?"
Those are both fair points. Indeed a more flexible type system adds confusion in several ways, and the scientific community is unlikely to switch away from python even if you cater to them.
Perhaps there is a less invasive solution. Let me think about this more.
awesome, thank you for understanding! :smiley:
Last updated: Jun 16 2026 at 16:19 UTC