Stream: ideas

Topic: cool potential use-case for operators-as-aliases


view this post on Zulip Jasper Woudenberg (Sep 23 2025 at 17:25):

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.

view this post on Zulip Kiryl Dziamura (Sep 23 2025 at 18:20):

I wonder how shaders might look like with such approach. This way, in theory, the shader program code might be generated at comptime :thinking:

view this post on Zulip Joshua Warner (Sep 25 2025 at 22:48):

This works... until... you try to do fancier things like conditionals and loops

view this post on Zulip Joshua Warner (Sep 25 2025 at 22:50):

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:

view this post on Zulip souf (Sep 26 2025 at 06:05):

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.

view this post on Zulip souf (Sep 26 2025 at 06:06):

If you can overload existing operators. They also make it harder for the compiler to perform inference.

view this post on Zulip Kiryl Dziamura (Sep 26 2025 at 07:56):

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.

view this post on Zulip Kiryl Dziamura (Sep 26 2025 at 07:59):

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

view this post on Zulip souf (Sep 26 2025 at 11:25):

@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.

view this post on Zulip Kiryl Dziamura (Sep 26 2025 at 11:30):

In future roc, it's only about a.add(b) so it's expected that type a implements add : a, a -> a

view this post on Zulip Anton (Sep 26 2025 at 11:39):

I believe we decided to go with plus instead of add

view this post on Zulip Anton (Sep 26 2025 at 11:43):

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?

view this post on Zulip souf (Sep 26 2025 at 14:38):

@Anton Sorry, I’m a bit confused. The inference is for the compiler, how does that relate to the LSP?

view this post on Zulip Anton (Sep 26 2025 at 14:46):

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?

view this post on Zulip Brendan Hansknecht (Sep 26 2025 at 15:36):

Joshua Warner said:

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:

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.

view this post on Zulip Brendan Hansknecht (Sep 26 2025 at 15:37):

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.

view this post on Zulip Joshua Warner (Sep 26 2025 at 23:21):

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

view this post on Zulip Joshua Warner (Sep 26 2025 at 23:24):

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).

view this post on Zulip souf (Sep 27 2025 at 10:18):

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.

view this post on Zulip souf (Sep 27 2025 at 10:29):

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.

view this post on Zulip Kiryl Dziamura (Sep 27 2025 at 10:38):

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 ]

view this post on Zulip Kiryl Dziamura (Sep 27 2025 at 10:41):

An then, during unification, you'll find out if types used with this function (that come from known types from the application boundaries), comply

view this post on Zulip souf (Sep 27 2025 at 11:16):

@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.

view this post on Zulip Kiryl Dziamura (Sep 27 2025 at 11:20):

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

view this post on Zulip Kiryl Dziamura (Sep 27 2025 at 11:21):

There are no assumptions, that's just how desugaring in roc with static dispatch (you can search for it in threads) will work

view this post on Zulip Kiryl Dziamura (Sep 27 2025 at 11:22):

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

view this post on Zulip souf (Sep 27 2025 at 11:26):

Right, let’s pause this discussion here and refocus on the main topic

view this post on Zulip Richard Feldman (Sep 27 2025 at 13:24):

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:

view this post on Zulip Richard Feldman (Sep 27 2025 at 13:25):

if it weren't true, it could be disproven by writing a roc program which violates one of those properties

view this post on Zulip Richard Feldman (Sep 27 2025 at 13:25):

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)

view this post on Zulip Richard Feldman (Sep 27 2025 at 13:27):

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

view this post on Zulip Richard Feldman (Sep 27 2025 at 13:28):

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:

view this post on Zulip Brendan Hansknecht (Sep 27 2025 at 15:46):

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.

view this post on Zulip Brendan Hansknecht (Sep 27 2025 at 15:49):

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.

view this post on Zulip Richard Feldman (Sep 27 2025 at 16:01):

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!

view this post on Zulip Richard Feldman (Sep 27 2025 at 16:02):

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:

view this post on Zulip Richard Feldman (Sep 27 2025 at 16:05):

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

view this post on Zulip Richard Feldman (Sep 27 2025 at 16:06):

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"

view this post on Zulip Richard Feldman (Sep 27 2025 at 16:12):

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

view this post on Zulip Richard Feldman (Sep 27 2025 at 16:22):

but this seems needlessly inconvenient so instead we have the policy that we evaluate unbound number literals using the concrete types of our choice.

view this post on Zulip Richard Feldman (Sep 27 2025 at 16:24):

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"

view this post on Zulip Richard Feldman (Sep 27 2025 at 16:24):

basically just automatically generate a decoding failure in that situation

view this post on Zulip Richard Feldman (Sep 27 2025 at 16:31):

or we could give a compile error of like "this can't possibly do anything useful and is probably a mistake"

view this post on Zulip Brendan Hansknecht (Sep 27 2025 at 16:37):

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

view this post on Zulip Richard Feldman (Sep 27 2025 at 16:45):

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.

view this post on Zulip Richard Feldman (Sep 27 2025 at 16:45):

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:

view this post on Zulip Richard Feldman (Sep 27 2025 at 16:51):

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:

view this post on Zulip Brendan Hansknecht (Sep 27 2025 at 17:12):

Sounds worth exploring

view this post on Zulip Richard Feldman (Sep 28 2025 at 21:58):

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

view this post on Zulip Brendan Hansknecht (Sep 28 2025 at 22:12):

Yeah, I think the error is quite rare, especially with usage code and even light top level type and types in library code

view this post on Zulip Richard Feldman (Sep 28 2025 at 22:28):

yeah I don't think I've ever seen it happen in the wild :smile:


Last updated: Jun 16 2026 at 16:19 UTC