One half-formed idea for a Roc platform I have is a Roc-based spreadsheet program, where the formulas are written using Roc code. I was thinking maybe using duckdb as the storage backend for this platform.
One thing I didn't like about this design is that to evaluate the formula's you'd have to read cell values out of duckdb, feed them through Roc, then write results back into the database, potentially for a great many cells. Performance might be much better if the calculations were happening in the duckdb engine entirely.
I realized though, with the possibility to define operators for your own custom types, it'd be possible to accept as formula's Roc code that looks like it's operating directly on cell contents, but which in reality is constructing an AST that the platform will compile down to duckdb queries. Pretty cool!
Thought I'd share as another positive to supporting operators for user-defined types, in case it hasn't been thought of before.
I wonder how shaders might look like with such approach. This way, in theory, the shader program code might be generated at comptime :thinking:
This works... until... you try to do fancier things like conditionals and loops
Now, you could imagine a language design under which that works - where if is actually just a method on bool that takes to closures, e.g.:
my_condition.if(|| { 1 }, || { 2 })
Then that just becomes a method you can override on whatever custom-ast-recording type my_condition evaluates to in this context.
You could even have if my_condition { 1 } else { 2 } just be syntax sugar for the above :thinking:
That’s just my personal opinion, but I'm cautious with custom operators. It's extremely powerful but they’re a double-edged sword: on one hand they make it extremely easy to express a DSL without leaving your language; on the other, they’re easy to abuse and can leave you with a codebase that’s hard to understand for newcomers.
If you can overload existing operators. They also make it harder for the compiler to perform inference.
The operators is sugar for function calls, don't think it affects inference anyhow. But I think it may be a problem in context of precedence and algebraic properties of operations. Without proof system, it's easy to break consistency/correctness.
Maybe roc could have at least some simple unavoidable assertions for that? I can think only about fuzzing tho, which is not great for comptime
@Kiryl Dziamura when operators can be overloaded, if you have an instruction such as a + b, you can't infer what is a and b unless they've been declared before. If the + operator is reserved to integers, then you can safely infer that a and b are integers. That's why a language like OCaml has + for integer and +. for floats.
In future roc, it's only about a.add(b) so it's expected that type a implements add : a, a -> a
I believe we decided to go with plus instead of add
souf said:
Kiryl Dziamura when operators can be overloaded, if you have an instruction such as
a + b, you can't infer what is a and b unless they've been declared before. If the+operator is reserved to integers, then you can safely infer that a and b are integers. That's why a language like OCaml has+for integer and+.for floats.
Is that still a significant problem when you have "type on hover" with the LSP?
@Anton Sorry, I’m a bit confused. The inference is for the compiler, how does that relate to the LSP?
Oh I see, yeah the inference then requires that the type implements plus : a, a -> a, as @Kiryl Dziamura referred to. Do you see any further problems with that approach?
Joshua Warner said:
Now, you could imagine a language design under which that works - where
ifis actually just a method on bool that takes to closures, e.g.:my_condition.if(|| { 1 }, || { 2 })Then that just becomes a method you can override on whatever custom-ast-recording type
my_conditionevaluates to in this context.You could even have
if my_condition { 1 } else { 2 }just be syntax sugar for the above :thinking:
I would avoid the closure and if we wanted something like that, make if automatically call to_bool() on a type or something similar. Closures are not as compiler friendly.
In practice, I would just suggests users manually call to_bool() instead....no need for this.
EDIT: I think I missed the context of shader programming...which of course wants no branches.
souf said:
If you can overload existing operators. They also make it harder for the compiler to perform inference.
The compiler will always have full type inference
There are still edges, but they are due to things like decoding, not due to overloading plus.
I was thinking of this closure-style if method conversion as a way to extend a Cap'n Web -like closure mapping system to handle not just field lookups and perhaps basic math, but also conditionals. https://blog.cloudflare.com/capnweb-javascript-rpc-library/#wait-how
It'd be cool to write what looks like normal code:
rpc_api.my_request_returning_array!().map!(|x| if x.value > 3 { x.foo } else { x.bar })
... and have that result in effectively the code for the closure being serialized and sent over the wire, to be executed on the _server_ (avoiding round-trips and unneeded data transfers).
Brendan Hansknecht said:
The compiler will always have full type inference
I don't think this is accurate. You'll have at a point or another the need to declare the variable type. Overloading limits the ability to have full type inference as per. As I mentioned previously, please refer to OCaml design decisions on the matter.
such a function for example:
do_something = |a, b| a + b
if the plus operator can be used for string, and for numbers, you can't tell whether this function returns a number or a string.
Full type inference means that your whole application is a function with known types for inputs and outputs and every type in between these known inputs and outputs is infererred.
In your example, the actual code is
do_something = |a, b| a.plus(b) where [ a.plus : a, a -> a ]
It means the type system expects b to have the same type as a. Thus, the function type is
do_something : a, a -> a where [ a.plus : a, a -> a ]
An then, during unification, you'll find out if types used with this function (that come from known types from the application boundaries), comply
@Kiryl Dziamura You’re right that given a usage of the function, the compiler can eventually infer the type. However, you do get a hint at some point some external information is required. Full inference, and I insist on the word full, is not possible in isolation when operators are overloaded. Without knowing the intended type of a and b, the compiler cannot resolve the expression on its own. That's what I meant to say here.
Ah, now I the the source od the misunderstanding. where clause is important, please pay attention to it.
a + b is equal to a.plus(b) where [a.plus : t, t -> t]. That's how compiler desugars the operator
There are no assumptions, that's just how desugaring in roc with static dispatch (you can search for it in threads) will work
So the use of the a + b constraints the type of value a. And since signature of a.plus : t, t -> t (t is a typevar), it applies the same constraints to the type of value b
Right, let’s pause this discussion here and refocus on the main topic
of note, by design Roc's type system has principal decidable type inference, which means that:
this might sound implausible, but it's true! :smile:
if it weren't true, it could be disproven by writing a roc program which violates one of those properties
as far as I know, we don't have any soundness bugs (in the design or in the Rust implementation of the compiler, although the new Zig implementation of the compiler is incomplete)
also worth noting: operators in Roc are syntax sugar - they deterministically desugar to ordinary static dispatch calls before type checking even begins - and it's not possible for syntax sugar to affect type system semantics
so if Roc's type system did have a problem with soundness or principality or anything like that, the cause couldn't be operator overloading, because that's just a syntax thing that gets desugared into something else before type checking even begins! :smile:
I want to add one extra note. Roc already has a number of undecidable concrete types. I don't see that as breaking full type inference, but that is just my opinion. In some cases it has solutions to pick concrete types, in others it is just stuck and requires user information.
A first example, what is the concrete type of 7 if it is never used in a way that require a specific numeric type? The generic type is Num a. The roc compiler will them pick a concrete type for it. I'm pretty sure that type will be I64 (maybe U64).
Another example, what is the type of x in:
# this is pseudo-syntax cause I don't remember the exact API and syntax for decode with static dispatch.
# Instead using roughly what happens with abilites in the rust compiler
x = Json.decode(bytes)
new_byes = Json.encode(x)
The generic type is just a where a implements { Encoding, Decoding }... And in this case there is no concrete type. Without either specifying a type or using the type in a way that specifies what it concretely must be, roc can not infer the concrete type.
The desugaring of types to enable operator overloading will make this case more common. That said, custom types are almost always are generated in a way that lets the compiler know the type. So I think this problem should be rare in practice. The main exception being decoding.
the terminology is important here though!
Decidable typing means "the compiler can correctly infer what the type is." In both of those cases, the compiler can and will do that - so for example if you hover over the (unannotated) type in an editor, the language server can correctly tell you what the inferred type is.
If Roc didn't have decidable type inference, then there would be some situations where you'd hover over a valid type and see an error like "I couldn't infer what this type was; you need to add an annotation." Roc doesn't have those errors!
Roc can't infer a concrete type that doesn't exist - like if you say "Roc can't infer the concrete type of the identity function's argument" that's only true because the identity function's argument isn't concrete :smile:
there's a separate language design question about what to do at runtime when evaluating expressions with types that are still polymorphic even after instantiation
a valid design choice is to say "we don't want to allow this situation to happen at runtime, so we will give a compile time error in advance"
for example, if you write if (1 + 1 == 2) we can infer all the types involved, but since number literals are polymorphic (which is a super nice language feature!) we could refuse to evaluate this and give you a compile error instead
but this seems needlessly inconvenient so instead we have the policy that we evaluate unbound number literals using the concrete types of our choice.
we could have the same policy for the code "decode these bytes into a shape determined by this unbound type variable, and the type variable will remain unbound"
basically just automatically generate a decoding failure in that situation
or we could give a compile error of like "this can't possibly do anything useful and is probably a mistake"
Yep, or we could just decode into an empty record which also would kinda be valid. Lots of options here.
I think the distinction is very important
that said, something I hadn't considered is that static dispatch could eliminate that situation if we supported nominal records but not the "dispatch on return type" option. That way, you'd always have to have an actual value at runtime to dispatch on (because the only syntax for it would be foo.bar() and you couldn't even syntactically dispatch any other way) and this situation couldn't come up at all.
since in practice people will be decoding into types they've written out basically every time, and doing it based on anonymous types is more of a cool party trick than something of practical value :smile:
that could be nice for learning curve too, and would also enable the nicer type signatures for static dispatch that we'd preciously discussed :thinking:
Sounds worth exploring
I thought about it and removing it would make me significantly less excited to use Roc for scripting. It's really nice just being able to read whatever format without having to stop and write out the types (which I wouldn't have to do in e.g. Python or JS either) and it hasn't been a source of pain in practice to have to this "cyclic dispatch" error at build time
Yeah, I think the error is quite rare, especially with usage code and even light top level type and types in library code
yeah I don't think I've ever seen it happen in the wild :smile:
Last updated: Jun 16 2026 at 16:19 UTC