Stream: ideas

Topic: number literals for custom number types


view this post on Zulip Richard Feldman (Aug 21 2025 at 14:12):

related to #ideas > custom numbers and strings, I think we should revise our number literal suffix syntax to work with user-defined types

view this post on Zulip Richard Feldman (Aug 21 2025 at 14:13):

e.g. instead of:

x = 1u8

...we have:

x = 1.U8

view this post on Zulip Richard Feldman (Aug 21 2025 at 14:13):

and 1.U8 is syntax sugar for U8.from_digits([1])

view this post on Zulip Richard Feldman (Aug 21 2025 at 14:13):

so you can put whatever module you want there, including user-defined ones and then this Just Works:

x = 1.BigInt

view this post on Zulip Richard Feldman (Aug 21 2025 at 14:14):

I think doing it as a suffix makes more sense than as a prefix, because 1.U8 is a lot less confusing to me than U8.1, which I can't help but read as 8.1 :sweat_smile:

view this post on Zulip Richard Feldman (Aug 21 2025 at 16:26):

whoa, that also means we could have like:

duration = 5.Seconds

earlier = 2.Days->ago!()

view this post on Zulip Richard Feldman (Aug 21 2025 at 16:26):

as long as we have Seconds.from_digits and Days.from_digits in those modules

view this post on Zulip Richard Feldman (Aug 21 2025 at 16:27):

for testability, could even have ago be in the module itself and take a Clock, which could be mocked:

duration = 5.Seconds

earlier = 2.Days.ago!(clock)

view this post on Zulip Richard Feldman (Aug 21 2025 at 16:29):

I love that :heart_eyes:

view this post on Zulip Richard Feldman (Aug 21 2025 at 16:30):

at this point it's basically the Rails syntax I love of 2.days.ago except:

view this post on Zulip Richard Feldman (Aug 21 2025 at 16:30):

and it covers the 1u8 use case trivially along the way

view this post on Zulip Richard Feldman (Aug 21 2025 at 16:41):

a related idea: if this were done with static dispatch on types (e.g. it's not the Days module but rather the Days type, assuming that type is in scope), then you could have type aliases for singular vs plural, e.g. Day.roc exposes Days : Day, and then you can make them more readable:

duration = 5.Seconds
short = 1.Second

earlier = 2.Days.ago!(clock)
yesterday = 1.Day.ago!(clock)

view this post on Zulip Richard Feldman (Aug 21 2025 at 16:44):

actually I guess you can do that yourself with modules using import with as:

import date.Day as Days
import date.Day

# ...

yesterday = 1.Day.ago!(clock)
earlier = 2.Days.ago!(clock)

view this post on Zulip Richard Feldman (Aug 21 2025 at 16:53):

yeah that seems nice :smiley:

view this post on Zulip Kiryl Dziamura (Aug 21 2025 at 19:35):

It looks amazing aestestically, would be interesting to explore how it affects naming conventions.

E.g. I see how this literal syntax affects not only api, but also variables naming. Like, 1.Day.ago looks great, but abstract duration.ago looks weird, and timespan.ago a bit better but still meh.

Also, singular/plural take is too much for me :smile:importing and aliasing multiple variants will definitely annoy me but I will do that anyway because of my stupid perfectionism :sweat_smile:

view this post on Zulip Richard Feldman (Aug 21 2025 at 19:42):

I like the idea of it just being a technique the user of the library can use (or not) vs. something the library changes

view this post on Zulip Fabian Schmalzried (Aug 21 2025 at 19:43):

1.U8 should be rather quick to get used to, even though it looks strange to me at first.

2.Days.ago!(clock) looks awesome. I have a question about implementing a Time package like this. Would this require a module for each supported time interval or can this be done by types within one module somehow? Can there then several from_digita functions in a module? I think I'm just not up to date with the latest dispatch plans.

view this post on Zulip Richard Feldman (Aug 21 2025 at 19:43):

put another way: it is a fact that you can do import with as to make module names plural, so then there's a stylistic preference as to whether you choose to do that :smile:

view this post on Zulip Richard Feldman (Aug 21 2025 at 19:44):

Fabian Schmalzried said:

2.Days.ago!(clock) looks awesome. I have a question about implementing a Time package like this. Would this require a module for each supported time interval or can this be done by types within one module somehow?

yeah it would be something like:

ago! : Duration, Clock => Instant

and then the Day module would define Day to be a type alias of Duration

view this post on Zulip Richard Feldman (Aug 21 2025 at 19:44):

actually, even better

view this post on Zulip Richard Feldman (Aug 21 2025 at 19:45):

ago! : amount, Clock => Instant
    where module(amount).to_duration : amount -> Duration

view this post on Zulip Richard Feldman (Aug 21 2025 at 19:46):

so that way it accepts any nominal type with a to_duration method, which in turn means you can define Day to be a nominal type with a to_duration method

view this post on Zulip Richard Feldman (Aug 21 2025 at 19:47):

that way you can define a function that says it takes a Day count as input specifically, and if you try to give it like minutes or something, it errors

view this post on Zulip Richard Feldman (Aug 21 2025 at 19:47):

(or if that seems like an antipattern in practice, can always do the type alias approach instead)

view this post on Zulip Jasper Woudenberg (Aug 21 2025 at 19:54):

Yeah, personally I think having all the durations use the same type seems more powerful to me. Otherwise you wouldn't be able to write 3.Weeks + 2.Days, which I think is reasonable code?

view this post on Zulip Fabian Schmalzried (Aug 21 2025 at 19:56):

Thanks for the explanation, this could be useful for a lot of stuff. item.shift(2.Pixels.left) or something.

view this post on Zulip Jasper Woudenberg (Aug 21 2025 at 21:14):

One more take on this Ruby-like duration syntax: What if we had:

## Durations.roc
days : number -> Duration where module(number).to_number : number -> Number
hours: number -> Duration where module(number).to_number : number -> Number
seconds: number -> Duration where module(number).to_number : number -> Number
# ... etc

## Roc standard library
Number : [Digits, Int U64, Float F64]
Digits := List(U8)

## Usage in application code
duration = 4=>hours().ago!(clock)

There's a bit of weird wrapping going on behind the scenes: taking a primitive number type, adding a tag on it to produce a Number, then immediately casing on it again to get back the primitive number type. I imagine that would reliably get optimized out again.

view this post on Zulip Jasper Woudenberg (Aug 21 2025 at 21:42):

Separately, I like Rails' 3.hours.ago syntax, but given we have to pass a clock I wonder if this would be clearer:

.before(clock.now!())
.after(clock.now!())

It's a bit more verbose, but:

view this post on Zulip Luke Boswell (Aug 21 2025 at 22:51):

Richard Feldman said:

e.g. instead of:

x = 1u8

...we have:

x = 1.U8

This would mean we can remove the whole complexity around parsing/can for those literals, which is another simplification I imagine.

view this post on Zulip Richard Feldman (Aug 21 2025 at 22:52):

less work for the parser, more work for canonicalization :smile:

view this post on Zulip Luke Boswell (Aug 21 2025 at 22:55):

Still completely cacheable though right?

view this post on Zulip Richard Feldman (Aug 21 2025 at 22:56):

yep!

view this post on Zulip Brendan Hansknecht (Aug 23 2025 at 17:07):

Seems pretty reasonable.... Is it only for raw numbers? Also, I guess it has it really depends on compile time evaluation to not have a perf hit.

How does it deal with hex or binary for example. Like a hex number can be positive or negative without a numeric sign (I guess these are issues from any custom numbers and not this syntax proposal).

0xFF.I8 -> -1?

Also this looks pretty solid: 27.Complex + 12.Complex.i()... Definitely noisy, but maybe reasonable.... idk...

view this post on Zulip Richard Feldman (Aug 23 2025 at 17:08):

yeah exactly!

view this post on Zulip Sky Rose (Aug 25 2025 at 22:20):

I didn't quite follow everything, but would this require every suffix to define its own from_digits? Like, Day.from_digits and Pixels.from_digits and ... That could be a lot, especially if each one has to handle hex inputs as well.

view this post on Zulip Sky Rose (Aug 25 2025 at 22:21):

Also, what do y'all think about 20250825.Date?

view this post on Zulip Sky Rose (Aug 25 2025 at 22:22):

Useful shortcut or misuse of the concept?

view this post on Zulip Richard Feldman (Aug 25 2025 at 22:35):

I think it would make more sense to use a string, since strings will be able to do the same thing

view this post on Zulip Richard Feldman (Aug 25 2025 at 22:37):

also

Sky Rose said:

would this require every suffix to define its own from_digits? Like, Day.from_digits and Pixels.from_digits and ... That could be a lot, especially if each one has to handle hex inputs as well.

hex inputs would get converted automatically. I actually suspect we'll want them to be base-2 digits because that's the most efficient for the compiler to both store and operate on.

it would be trivial to implement these from_digits functions for wrapped integers because they'd just delegate to a builtin from_digits - e.g.

from_digits : Iter(U8) -> Result(Day, OutOfRange)
from_digits = |iter| U32.from_digits(iter).map_ok(Day.from_u32)

view this post on Zulip Brendan Hansknecht (Aug 26 2025 at 00:01):

I guess my biggest concern is that I feel like it always needs to fold. Like that should be a requirement at comptime

view this post on Zulip Brendan Hansknecht (Aug 26 2025 at 00:01):

Cause it would be really bad perf otherwise and really awkward ux if the result manifests

view this post on Zulip Richard Feldman (Aug 26 2025 at 00:59):

what else would it do? :thinking:

view this post on Zulip Brendan Hansknecht (Aug 26 2025 at 01:01):

I guess my comment doesn't apply to this syntax specifically but to the original design.

view this post on Zulip Joshua Warner (Aug 26 2025 at 01:02):

Possibly silly idea: add a comptime designator like zig that requires that arg to be constant folded at compile time, else failing the compilation

view this post on Zulip Richard Feldman (Aug 31 2025 at 12:45):

I just realized we could make record builder syntax use this same metaphor, e.g.

color = {
    r: Random.u8(),
    g: Random.u8(),
    b: Random.u8(),
}.Random

view this post on Zulip Richard Feldman (Aug 31 2025 at 12:46):

so just like how 1.U8 would desugar to U8.from_digits([1]), { ... }.Random would desugar to using Random.map_both (or whatever we decide to call it) to build up all the fields

view this post on Zulip Richard Feldman (Aug 31 2025 at 12:47):

instead of the current syntax, which is:

color = { Random.map_both <-
    r: Random.u8(),
    g: Random.u8(),
    b: Random.u8(),
}

view this post on Zulip Richard Feldman (Aug 31 2025 at 12:48):

conceptually we're doing the same thing in both cases - we have a literal (either a number literal or a record literal) and we want to use a pure function to concisely transform it in a particular way

view this post on Zulip Richard Feldman (Aug 31 2025 at 12:50):

also this made me realize that in both cases (if desired) we could let you customize the exact function, e.g. 1.(U8.something_other_than_from_digits)

view this post on Zulip Richard Feldman (Aug 31 2025 at 14:22):

we could do it for list literals too, and use the fact that the conversion functions return a Result to validate things at compile time - so for example this could give you an error at build time:

[1, 2, 2, 3].Set

basically telling you about the duplicate entry in the Set literal at compile time!

view this post on Zulip Richard Feldman (Aug 31 2025 at 14:47):

dictionaries too:

[
    ("a", 1),
    ("b", 1),
    ("b", 1),
    ("c", 1),
].Dict

view this post on Zulip Richard Feldman (Aug 31 2025 at 15:09):

there was a thing we used to do in Elm where we had a => operator that just made a tuple, so it was like a => b was the same as (a, b), so you could do things like:

[
    "a" => 1,
    "b" => 2,
    "b" => 3,
    "c" => 4,
].Dict

view this post on Zulip Brendan Hansknecht (Aug 31 2025 at 18:29):

Feels strange, but probably just cause it is different

view this post on Zulip Brendan Hansknecht (Aug 31 2025 at 18:33):

I guess the oddest parts are:

  1. Being used for both numbers and record builders, so feels like an overloading
  2. It feels like it is just calling arbitrary functions.

I can see folks being confused and asking:
Why is {...}.U8 trying to call U8.map_both (which doesn't exist), but 0b1101.U8 is calling U8.from_digits? What is map_both (given record builders are rare) and why does it have special syntax as opposed to any other function?

view this post on Zulip Brendan Hansknecht (Aug 31 2025 at 18:33):

Also, this: [1, 2, 2, 3].Set just makes me really want a normal constructor Set([1, 2, 2, 3]). Same with the Dict example.

view this post on Zulip Richard Feldman (Aug 31 2025 at 18:51):

that already has meaning, though - it's a tag application :sweat_smile:

view this post on Zulip Richard Feldman (Aug 31 2025 at 18:52):

so switching to that would actually be overloading

view this post on Zulip Brendan Hansknecht (Aug 31 2025 at 19:06):

Sure, but is [1, 2, 2, 3].Set worth it over Set.new([1, 2, 2, 3]).

view this post on Zulip Richard Feldman (Aug 31 2025 at 19:30):

the difference is that the latter can't tell you at compile time that you had a duplicate in there

view this post on Zulip Richard Feldman (Aug 31 2025 at 19:31):

or rather, the only way it could tell you at compile time would be if it had a crash on duplicates, which would be a really bad API because then it would potentially crash in production at runtime :sweat_smile:

view this post on Zulip Richard Feldman (Aug 31 2025 at 19:31):

the cool part about the "literal suffix" is that the conversion function returns a Result, so it's always safe to use in production at runtime, and yet if it returns Err when the compiler is evaluating it with the literal at compile time, the compiler can give you an error

view this post on Zulip Luke Boswell (Aug 31 2025 at 23:12):

Richard Feldman said:

I just realized we could make record builder syntax use this same metaphor, e.g.

color = {
    r: Random.u8(),
    g: Random.u8(),
    b: Random.u8(),
}.Random

Love this :heart_eyes:

view this post on Zulip Luke Boswell (Aug 31 2025 at 23:12):

Also I think "literal suffix" is a great way to explain it

view this post on Zulip Luke Boswell (Aug 31 2025 at 23:14):

Is it ever going to make sense on other literals... like have you thought about string literals?

view this post on Zulip Brendan Hansknecht (Aug 31 2025 at 23:22):

or rather, the only way it could tell you at compile time would be if it had a crash on duplicates, which would be a really bad API because then it would potentially crash in production at runtime :sweat_smile:

the cool part about the "literal suffix" is that the conversion function returns a Result, so it's always safe to use in production at runtime, and yet if it returns Err when the compiler is evaluating it with the literal at compile time, the compiler can give you an error

To me, this just sounds like some form of comptime crash is missing and we are band-aiding over it. Like we could make Set.new have a comptime crash if wanted.

view this post on Zulip Brendan Hansknecht (Aug 31 2025 at 23:23):

the difference is that the latter can't tell you at compile time that you had a duplicate in there

Also, it is a set, why does duplicate detection at comptime matter?

view this post on Zulip Richard Feldman (Sep 01 2025 at 00:58):

just to let you know that you've made a mistake

view this post on Zulip Richard Feldman (Sep 01 2025 at 00:59):

dictionary with duplicate keys would be more impactful

view this post on Zulip Richard Feldman (Sep 01 2025 at 00:59):

maybe you thought a different value was being inserted than what actually was

view this post on Zulip Richard Feldman (Sep 01 2025 at 02:25):

Richard Feldman said:

there was a thing we used to do in Elm where we had a => operator that just made a tuple, so it was like a => b was the same as (a, b), so you could do things like:

[
    "a" => 1,
    "b" => 2,
    "b" => 3,
    "c" => 4,
].Dict

I just realized there's a really obvious syntax we could offer for this:

{
    "a": 1,
    "b": 2,
    "b": 3,
    "c": 4,
}.Dict

the design could be that if you make a record literal where instead of fields like foo: they are expressions, e.g. 1: or "a": (or if you really wanted to put a lookup in there, (foo):) and all the keys have to have the same type, and all the values have to have the same type, and it's sugar for an Iter((key, val))

view this post on Zulip Brendan Hansknecht (Sep 01 2025 at 02:30):

Yeah, all of this overall makes reasonable sense

view this post on Zulip Brendan Hansknecht (Sep 01 2025 at 02:31):

Feels a bit odd for roc, but don't crazy or anything. I guess a lot of it very fundamentally depends on comptime, but that's fine

view this post on Zulip Luke Boswell (Sep 01 2025 at 02:35):

"and it's sugar for an Iter((key, val))" can you humour me and give me a couple of usecases for using this feature?

Like would this be how someone might initialise a Set or Dict at comptime?

view this post on Zulip Richard Feldman (Sep 01 2025 at 02:39):

yeah that's one use case

view this post on Zulip Richard Feldman (Sep 01 2025 at 02:39):

another is if you want to write a JSON object where the keys aren't valid Roc syntax so you can't just use the normal auto-encoding

view this post on Zulip Richard Feldman (Sep 01 2025 at 02:40):

that was one we used to use => for in Elm - Json.Encode.object takes a list of key/value pairs where the key is a string

view this post on Zulip Richard Feldman (Sep 01 2025 at 02:41):

but of course { "_": underscore_thing, "$": dollar } reads nicer for JSON in particular (since it's exactly JSON syntax for objects) than [("_" => underscore_thing), ("$" => dollar)]

view this post on Zulip Luke Boswell (Sep 01 2025 at 02:41):

This is just more convenient API wise than the alternative. Because we planned to comp-time eval top-levels anyway

view this post on Zulip Richard Feldman (Sep 01 2025 at 02:42):

yeah, and also the => is very much already taken in Roc

view this post on Zulip Luke Boswell (Sep 01 2025 at 02:43):

One thing I really like about the Record literall syntax { .. }.RecordThing is that we are not using the <- back arrow. That arrow just always seemed a little out of place there, especially with backpassing removed now.

view this post on Zulip Brendan Hansknecht (Sep 01 2025 at 02:55):

So we feel this adds to a lot of weirdness/beginner complexity? I feel like roc as a language has been transition to be more complex for new users with more things to know about.

view this post on Zulip Richard Feldman (Sep 01 2025 at 02:58):

I'm not particularly worried about it

view this post on Zulip Richard Feldman (Sep 01 2025 at 02:58):

there's no real complexity to it

view this post on Zulip Brendan Hansknecht (Sep 01 2025 at 03:53):

Yeah, it isn't any form of deep or nestable complexity. It is just more surface level things in roc that the user is required to know. Kinda like adding more sugar/more ways to do similar things. Just very non-obvious without reading about it ina tutorial or looking it up.

view this post on Zulip Richard Feldman (Sep 02 2025 at 01:40):

here's how concurrently-running I/O operations could look in this design:

{ http_result, read_result, write_result } = {
    http_result: || Http.get!(url, Json.utf8),
    read_result: || File.read!(path1),
    write_result: || Fs.write!(path2, data),
}.Task.timeout(500.Ms).run!()

view this post on Zulip Richard Feldman (Sep 02 2025 at 01:41):

(without going on a huge tangent, I realized we do need a Task module and Task wrapper type for this specific case, where you want to say "run all of these concurrently, and tell me when they're all done" - but the wrapper type is kinda useful anyway so you can put a timeout on the whole batched operation and/or make it cancelable etc.)

view this post on Zulip Brendan Hansknecht (Sep 02 2025 at 01:46):

Those || looks so silly, but everything makes sense here.

I wonder if "Task" is the right name in this context...vs like Future or something.....

Also, should we think about explicit threading as well?

view this post on Zulip Brendan Hansknecht (Sep 02 2025 at 01:47):

I guess explicit threading may use a different system or just direct calls....idk

view this post on Zulip Richard Feldman (Sep 02 2025 at 01:50):

yeah the || are necessary because otherwise you'd just be evaluating them immediately and sequentially :laughing:

view this post on Zulip Richard Feldman (Sep 02 2025 at 01:52):

we should definitely explore additional concurrency primitives to see what makes sense (threads? channels? other stuff?) but mainly for the purposes of this thread, it's relevant how the specific use case of "I have some effectful functions I want to run concurrently" would look :smile:

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

side note to the side note: I just realized timeout is probably something that shouldn't be in a builtin, because some platforms will be single-threaded and couldn't possibly support it!

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

but if Tasks are cancelable, then any platform can offer a timeout operation which takes a task, makes it cancelable, and then cancels it if it hasn't completed by the specified time

view this post on Zulip Isaac Van Doren (Sep 02 2025 at 15:22):

This looks great!

view this post on Zulip Isaac Van Doren (Sep 02 2025 at 15:24):

basically telling you about the duplicate entry in the Set literal at compile time!

I personally would not want to have an error because of a duplicate entry in a set, I would rather the set drop duplicates automatically. That's the behavior I normally want from a set

view this post on Zulip Brendan Hansknecht (Sep 02 2025 at 16:23):

Note: this is explicitly for literals. Like if you have a literal written out in your source code that happens to have duplication in it. Which I think is likely to be accidental/a bug.

This also is a compile time error/warning. So it would be caught very early on.

view this post on Zulip Richard Feldman (Sep 02 2025 at 18:30):

yeah

view this post on Zulip Richard Feldman (Sep 02 2025 at 18:30):

that's why this distinction matters

view this post on Zulip Richard Feldman (Sep 02 2025 at 18:30):

if I'm inserting a variable at runtime, e.g. set.insert(foo) and it's a duplicate, then yeah it should silently drop it

view this post on Zulip Richard Feldman (Sep 02 2025 at 18:30):

but if I write out a literal that is just flat-out incorrect, and there is a 100% chance it's that I made a mistake, then sure, I'd prefer to know about it! :smile:

view this post on Zulip Brendan Hansknecht (Sep 02 2025 at 19:30):

Other note, any user could opt out by just using Set.new or Dict.new instead of literals

view this post on Zulip Kiryl Dziamura (Sep 03 2025 at 13:31):

String literals with interpolation will also have nice applications I guess:

div = |content|
  """
  <div>${content}</div>
  """.Html
pattern = "([A-Z])\w+".RegExp

view this post on Zulip Kiryl Dziamura (Sep 05 2025 at 09:23):

I want to explore the interpolation variant a bit

div = |content|
  """
  <div>${content}</div>
  """.Html

if it would be possible, should content be type of String or Html?

view this post on Zulip Anton (Sep 05 2025 at 09:33):

Seems like content would have to be a Str, how could it be Html?

view this post on Zulip Kiryl Dziamura (Sep 05 2025 at 10:11):

you probably right. my assumption is that it's a literal, not a string itself. it would be great to be able to validate html syntax this way and transform it to a sequence of calls

div = |content|
  """
  <div>${content}</div>
  """.Html
div = |content|
  Html.div([], [content])

otherwise it would be much less flexible:

div = |content|
  Html.div([], [Html.text(content)])

so I assume type

Module.from_iterpolation : List([String(Str), Arg(Module)]) -> Result(Module)

view this post on Zulip Kiryl Dziamura (Sep 05 2025 at 10:16):

nevermind, it's unneeded complexity in a lot of places and source of confusion

view this post on Zulip Brendan Hansknecht (Sep 05 2025 at 22:42):

I think the oddity of me is that it feels weird to do interpolation to a multiline string. Especially so if it is done before apply the html constructor. Unclear order of operations and I think interpolation is less common in multiline strings in general

view this post on Zulip Brendan Hansknecht (Sep 05 2025 at 22:43):

As I read it by default, html is a custom literal, so why should it run interpolation first. Or at least whys should it run string interpolation and not custom html interpolation

view this post on Zulip Luke Boswell (Sep 05 2025 at 22:54):

I would have expected the .Html to run at compile time, and the interpolation to run at runtime

view this post on Zulip Brendan Hansknecht (Sep 05 2025 at 23:34):

I expect the same, but in I feel like it could make sense that the HTML literal which runs at compile time decides how interpolation works at runtime (like maybe automatically sanitizing).

view this post on Zulip Brendan Hansknecht (Sep 05 2025 at 23:34):

Or even allow for more flexible interpolation syntax

view this post on Zulip Richard Feldman (Sep 06 2025 at 00:25):

Kiryl Dziamura said:

I want to explore the interpolation variant a bit

div = |content|
  """
  <div>${content}</div>
  """.Html

if it would be possible, should content be type of String or Html?

for this design to work, the entire expression before the .Html would have to be evaluated at compile time

view this post on Zulip Richard Feldman (Sep 06 2025 at 00:25):

so this example wouldn't work because content is a function argument

view this post on Zulip Richard Feldman (Sep 06 2025 at 00:25):

if it were a constant, that could be permitted

view this post on Zulip Richard Feldman (Sep 06 2025 at 00:42):

but it's important for the design that the function being called on the string's contents is evaluated at compile time, so that it can return a Result that gets unwrapped at compile time

view this post on Zulip Richard Feldman (Sep 06 2025 at 00:43):

so that it can give you a compile-time error if the string literal wasn't valid for that purpose (e.g. invalid regex from the example right after the html one), but you don't need to deal with the Result at runtime because it was handled at compile time instead

view this post on Zulip Fabian Schmalzried (Sep 06 2025 at 11:52):

Would it the make sense to have some kind of ComptimeResult type that makes sure it is evaluating at comptime? That would make it more clear that a function is only meant for comptime evaluations. Or would that result on unnecessary double implementation of those from_digits functions, because they might also be useful at runtime?

view this post on Zulip Richard Feldman (Sep 06 2025 at 12:32):

yeah I don't think we should have a separate type for that

view this post on Zulip Kiryl Dziamura (Sep 06 2025 at 20:59):

Richard Feldman said:

for this design to work, the entire expression before the .Html would have to be evaluated at compile time

It actually may work. Just out of blue, it might look like

"<div>${content}</div>".Html

is the same as

"<div>".Html.concat(content).concat("</div>".Html)

concat is what interpolation expects the module to implement, a generalization of Str.concat : Str, Str -> Str so Html.concat : Html, Html -> Html. From this perspective we don't need to comptime evaluate the function argument but only "...".Html parts that are constants.

Now, "<div>".Html is a string literal overloading (Html.from_str : Str -> Result.Html). It may have implementation Html.openTag("div"). So the whole expression is desugared (with $ for comptime evaluation) to

$(Html.openTag("div")).concat(content).concat($(Html.closeTag("div"))

It looks pretty sound and consistent to me. I don't know if its a great power or a heavy responsibility tho.

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

I'm still leaning towards inconvenience of it. It's a funny code golf, but in reality string parsing makes more sense when it's context aware, which is not possible here. On the other hand, what's the other way to have string interpolation overloading?

view this post on Zulip Kiryl Dziamura (Sep 06 2025 at 21:25):

Maybe the example with html is just not a good fit for this feature

view this post on Zulip Brendan Hansknecht (Sep 06 2025 at 21:44):

It definitely is an intriguing possibility. I could see it as quite helpful for html templating with smart escaping, but that would need to be at runtime for at least some of the work

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

An obvious use of the string literal overload is comptime tokenization. You stil have to parse the resulting sequence of tokens in runtime, but with this approach it's slightly more optimal and gives takenization errors in comptime. E.g. html, sql may be used in string form but comptime would validate their tokens

view this post on Zulip Kiryl Dziamura (Sep 07 2025 at 08:16):

Or, by anology with custom numbers, it leads to custom strings. E.g "abc${var}".Hex or Base64, or whatever else where you want a subset of graphemes (or tokens) in the string.

view this post on Zulip Kiryl Dziamura (Sep 07 2025 at 08:25):

Custom strings may be used instead of single quote btw ("r".U8)

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

Or bytes only strings, to allow only ASCII

view this post on Zulip Kiryl Dziamura (Sep 11 2025 at 06:00):

Another thing to explore: type casting between identical types:

TypeB := TypeA

itemA : TypeA

itemB : TypeB
itemB = itemA.TypeB

view this post on Zulip Kiryl Dziamura (Sep 11 2025 at 06:59):

E.g.

Id := U32

nextId = |id| id.U32.add(1).Id

So type casting from number literal may be done like this: number literal passed to U32 and casted to Id:

id = 42.U32.Id

Not sure about at which level it should be accessible. Probably everywhere since it's very explicit.

view this post on Zulip Richard Feldman (Sep 11 2025 at 11:09):

the shouldn't be allowed anywhere imo

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

Even at the module level?

view this post on Zulip Richard Feldman (Sep 11 2025 at 11:10):

if I'm making a type nominal and exposing the type but not a way to get its internal representation, it's very important that the details of what its internal representation are not get leaked like this

view this post on Zulip Richard Feldman (Sep 11 2025 at 11:10):

I need to be free to change what TypeA's internal representation is without breaking anyone's code

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

if this exists, I can never do that in Roc anymore because anyone who has called .TypeA to construct something that used to have the same internal representation will break

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

So no "no" to use in modules

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

well the uppercased thing refers to a module

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

Email := Str

empty : {} -> Email
empty = \{} -> "".Str.Email

from_str : Str -> Email
from_str = \str -> str.Email

to_str : Email -> Str
to_str = \email -> email.Str

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

so within the same scope where the type is already defined, you'd have to be referring to the same module you're already inside, and at that point I'm not even sure if it's more concise :sweat_smile:

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

But likely structured nominal types have more sense

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

Richard Feldman said:

so within the same scope where the type is already defined, you'd have to be referring to the same module you're already inside, and at that point I'm not even sure if it's more concise :sweat_smile:

Could you please show how the example I wrote above looks like in roc without such type casting? I'm not sure how would it look like, was away from roc syntax for quite a bit

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

TypeB := TypeA

itemA : TypeA

itemB : TypeB
itemB = TypeB.(itemA)

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

I mean this one: https://roc.zulipchat.com/#narrow/channel/304641-ideas/topic/number.20literals.20for.20custom.20number.20types/near/538829894

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

Let me try on my own

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

Email := Str

empty : {} -> Email
empty = \{} -> Email.("")

from_str : Str -> Email
from_str = \str -> Email.(str)

to_str : Email -> Str
to_str = \Email.(str) -> str

Smth like this?

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

Ok, I'm convinced. Much better :smile:
I just noticed .Type may be used not only for literals. But agree, unsafe and verbose

view this post on Zulip Kiryl Dziamura (Sep 19 2025 at 15:44):

Kiryl Dziamura said:

Custom strings may be used instead of single quote btw ("r".U8)

What do you think about that? I feel it got lost in the discussion. I find this design very obvious and don't think there is a reason of why single quote is needed for that. Is char needed so often that single quote has better ergonomics?

view this post on Zulip Richard Feldman (Sep 19 2025 at 16:25):

so we want single quote anyway for pattern matches, e.g. '.' =>

view this post on Zulip Richard Feldman (Sep 19 2025 at 16:26):

and I'm not sure what the advantage would be of "r".U8 if we already have single quote

view this post on Zulip Kiryl Dziamura (Sep 20 2025 at 09:31):

The advantage is that if strings are only double quotes - there are no missreads or misuse of them from people came from js and python. Also, if it's used with comptime constructor - it's explicitly shows the type U8, so it's clear there's no char concept in roc. It would also slightly simplify parsing and errors logic in compiler, but ofc would move the complexity to roc implementation, however it would be a good example of how custom strings may be implemented. I'm likely biased so I don't see single quotes for u8 as something really important. It's basically a U8 literal that looks like a string. So why not using overloaded string literal?
It's also not clear why pattern match ".".U8 => won't work if it's a static U8. Like, if pattern match works for numbers - why it wouldn't work for custom numbers?

But, I'm not a savage, I understand the ergonomics advantage of single quote for parser implementations. And I also understand that once you learnt about single quote and double quote difference - it's with you forever.

view this post on Zulip Richard Feldman (Sep 20 2025 at 11:37):

".".U8 => should indeed work! I hadn't thought about that

view this post on Zulip Brendan Hansknecht (Sep 20 2025 at 16:23):

Minorly related node. In roc, it would be ".".U32

view this post on Zulip Richard Feldman (Sep 20 2025 at 16:29):

either should work

view this post on Zulip Brendan Hansknecht (Sep 20 2025 at 16:31):

Fair, but single quotes are U32, or are they also both?

view this post on Zulip Kiryl Dziamura (Sep 20 2025 at 16:32):

Ha, indeed. In roc, it's utf-8, not ascii after all

view this post on Zulip Richard Feldman (Sep 20 2025 at 17:01):

single quotes are just number literals

view this post on Zulip Richard Feldman (Sep 20 2025 at 17:01):

so they take on whatever type based on how you use them

view this post on Zulip Richard Feldman (Sep 20 2025 at 17:01):

they can be signed, unsigned, whatever size, etc.

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

Yes, but current roc has 42u32 or 42u8 for specifing numeric type in literal, but there's no analog for 'x'u32 or 'x'u8

view this post on Zulip Richard Feldman (Sep 21 2025 at 00:16):

I guess? haha

view this post on Zulip Richard Feldman (Sep 21 2025 at 00:16):

doesn't really seem like a problem :shrug:

view this post on Zulip Norbert Hajagos (Sep 23 2025 at 11:37):

Got an idea from reading this thread. It has amazing misuse opportunities, but it's a fun idea :)
What if the single quotes meant for the compiler: "I don't know what this is right now, but based on usage, I see that later it is used as an U8, so I will call U8.from_str_literal on it. This will enable the current behaviour:
new_list = bytes.set(0, 'A')
But also more exotic ones, like:

#CodePoint is a U32 backed nominal type
code_points : List(CodePoint)
code_points = ['h', 'i', '!', '😀']

But ofc, it can do any computation at compile time. It's good, when the verbosity would be too much, like "h".CodePoint, but I would not want to see things like this in my codebase:

# User.from_str_literal is basically a specialized JSON parser
user = '{"name": "Jon"}'
expect user == {name: "Jon"} # true

view this post on Zulip Brendan Hansknecht (Sep 23 2025 at 15:55):

That feels a bit too magical to me....

It is just a non-explicit version of what is in this thread though.

view this post on Zulip Kiryl Dziamura (Sep 24 2025 at 13:05):

why roc needs single quotes at all? I'm not even talking about an alternative. the only justification I can come up with is smaller memory footprint for e.g. U8. afaiu, single quote aka char is needed for C interop and a matter of legacy and tradition, no?

view this post on Zulip Brendan Hansknecht (Sep 24 2025 at 13:43):

Single quotes are just a convenient way define ASCII/Unicode characters. That definitely comes in handy at times (like pattern matching on a list of bytes).

It isn't really memory related as single quotes can be any type of int.

view this post on Zulip Richard Feldman (Sep 24 2025 at 14:54):

we originally didn't

view this post on Zulip Richard Feldman (Sep 24 2025 at 14:55):

the specific reason we added them was that there was a really nasty tension between wanting to do certain tasks (e.g. writing a JSON parser) in a performant vs readable way

view this post on Zulip Richard Feldman (Sep 24 2025 at 14:55):

if you can only pattern match on double-quoted strings, then you have to convert a single byte into a Str just to pattern match on it

view this post on Zulip Richard Feldman (Sep 24 2025 at 14:55):

otherwise you have to hardcode the Unicode Code Point number to compare to the U8, which people did, and it was terrible for readability

view this post on Zulip Richard Feldman (Sep 24 2025 at 14:56):

it's important to be able to write high-performance parsers that are readable, so we added single quote to the language to fix that problem

view this post on Zulip albx (Sep 24 2025 at 17:57):

Hmm I understood the question not as "do we need character literals?" (we clearly do), but as "do we need the ' to express a character literal?" (which is the most obvious choice because that's what most languages use). But now that we have the <literal>.<type> syntax to express literals if any type, do we still need the single quote for chars? Maybe that syntactic space can be freed for some other use. (I'm not pushing for this change btw, just saying how I've read the question)

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

in other words, allow things like "x".U8 => in patterns?

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

it would be more verbose but could work in theory

view this post on Zulip Richard Feldman (Sep 24 2025 at 19:06):

I'll say I like the idea of trying it out

view this post on Zulip Richard Feldman (Sep 24 2025 at 19:07):

e.g. make the new compiler not support ' once we have "x".U8 in patterns, and see if even with that option available we still have sufficient demand for (re)adding ' to the language

view this post on Zulip Richard Feldman (Sep 24 2025 at 19:07):

we certainly know how to do it if desired, and obviously "x".U8 patterns can be used for a lot more

view this post on Zulip Brendan Hansknecht (Sep 24 2025 at 19:38):

Feels like a ton of noise in pattern matching, but maybe that would get glazed over

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

it's definitely noisier, but I wonder if it's ok in practice...here's an old example

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

Numbers only

# Prepend an "\" escape byte
escaped_byte_to_json : U8 -> List U8
escaped_byte_to_json = |b|
    match b {
        0x22 => [0x5c, 0x22] # U+0022 Quotation mark
        0x5c => [0x5c, 0x5c] # U+005c Reverse solidus
        0x0a => [0x5c, 'n'] # U+000a Line feed
        0x0d => [0x5c, 'r'] # U+000d Carriage return
        0x09 => [0x5c, 'r'] # U+0009 Tab
        _ => [b]
    }

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

Single quotes

# Prepend an "\" escape byte
escaped_byte_to_json : U8 -> List U8
escaped_byte_to_json = |b|
    match b {
        '"' => ['\\', '"']
        '\\' => ['\\', '\\']
        '\n' => ['\\', 'n']
        '\r' => ['\\', 'r']
        '\t' => ['\\', 't']
        _ => [b]
    }

view this post on Zulip Richard Feldman (Sep 24 2025 at 20:21):

.U8 suffix

# Prepend an "\" escape byte
escaped_byte_to_json : U8 -> List U8
escaped_byte_to_json = |b|
    match b {
        "\"".U8 => "\\\"".to_utf8()
        "\\".U8 => "\\\\".to_utf8()
        "\n".U8 => "\\n".to_utf8()
        "\r".U8 => "\\r".to_utf8()
        "\t".U8 => "\\t".to_utf8()
        _ => [b]
    }

view this post on Zulip Richard Feldman (Sep 24 2025 at 20:21):

looks fine to me honestly

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

the first one (numbers only) is the only one that's unpleasant to read imo

view this post on Zulip Karl (Sep 24 2025 at 20:44):

Are strings not utf8 by default?

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

.to_utf8 is for getting a List(U8) representation of the string

view this post on Zulip Richard Feldman (Sep 24 2025 at 21:49):

yeah I'm just keeping with the example

view this post on Zulip Richard Feldman (Sep 24 2025 at 21:49):

probably not common

view this post on Zulip Kiryl Dziamura (Sep 24 2025 at 21:50):

What's the default type of number literal and how inference would work for it? Would it infer built-in numeric types?
I was thinking about explicit inference operator (_) in different parts of the language, and was wondering how bad would it be to have such for the literal overloads:

# Prepend an "\" escape byte
escaped_byte_to_json : U8 -> List U8
escaped_byte_to_json = |b|
    match b {
        "\""._ => "\\\"".to_utf8()
        "\\"._ => "\\\\".to_utf8()
        "\n"._ => "\\n".to_utf8()
        "\r"._ => "\\r".to_utf8()
        "\t"._ => "\\t".to_utf8()
        _ => [b]
    }

So the explicit inference operator would mean "allow inference from any type, not only native ones".

I'm coming from the fact that any mention of a type name in code is virtually the same as a separate type annotation. I know, it's impractical to have a strict separation of types and logic in code. But maybe it makes sense to give the user ability to explicitly unlock inference (I'm not talking about this particular case, but in general). It also means more generic code, which is not always a great idea.

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

I think ._ should be a separate thread, seems like a potential rabbit hole discussion :smile:

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

What's the default type of number literal

it would need to be something like:

num where [
    module(num).from_digits : List(U8) -> Result(num, BadDigits)
]

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

one of the ideas behind this design is to not show the types of number literals (or strings) in the repl anymore

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

on the theory that:

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

if we really wanted to, we could show something like:

1 + 1
2 : num where [num.Numeric]

but I'd rather try it with just the plain numbers first and see how that goes

view this post on Zulip Brendan Hansknecht (Sep 25 2025 at 01:20):

Yeah, not awful, but feels noisy for no meaningful reason

view this post on Zulip Brendan Hansknecht (Sep 25 2025 at 01:21):

Also, think about matching lists of bytes

view this post on Zulip Luke Boswell (Dec 31 2025 at 20:11):

Richard Feldman said:

e.g. instead of:

x = 1u8

...we have:

x = 1.U8

I see this change is underway... :grinning_face_with_smiling_eyes:

Just had to go back and find this thread to remind myself what the design is. I'd even forgotten about the awesome Record Builder syntax... another one for the langref maybe?

view this post on Zulip Richard Feldman (Dec 31 2025 at 20:13):

yeah!


Last updated: Jun 16 2026 at 16:19 UTC