related to #ideas > custom numbers and strings, I think we should revise our number literal suffix syntax to work with user-defined types
e.g. instead of:
x = 1u8
...we have:
x = 1.U8
and 1.U8 is syntax sugar for U8.from_digits([1])
so you can put whatever module you want there, including user-defined ones and then this Just Works:
x = 1.BigInt
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:
whoa, that also means we could have like:
duration = 5.Seconds
earlier = 2.Days->ago!()
as long as we have Seconds.from_digits and Days.from_digits in those modules
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)
I love that :heart_eyes:
at this point it's basically the Rails syntax I love of 2.days.ago except:
ago! rather than ago, so you can see there's a side effectclock gets passed in, so it can be simulated in testsand it covers the 1u8 use case trivially along the way
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)
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)
yeah that seems nice :smiley:
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:
I like the idea of it just being a technique the user of the library can use (or not) vs. something the library changes
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.
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:
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
actually, even better
ago! : amount, Clock => Instant
where module(amount).to_duration : amount -> Duration
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
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
(or if that seems like an antipattern in practice, can always do the type alias approach instead)
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?
Thanks for the explanation, this could be useful for a lot of stuff. item.shift(2.Pixels.left) or something.
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)
.hours() to be called on literals, also variables (ints, floats, and any other type defining .to_number())Day/Hour/Second/etcThere'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.
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:
clock is for and what effect is happening.3=>hours().after(midnight)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.
less work for the parser, more work for canonicalization :smile:
Still completely cacheable though right?
yep!
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...
yeah exactly!
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.
Also, what do y'all think about 20250825.Date?
Useful shortcut or misuse of the concept?
I think it would make more sense to use a string, since strings will be able to do the same thing
also
Sky Rose said:
would this require every suffix to define its own
from_digits? Like,Day.from_digitsandPixels.from_digitsand ... 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)
I guess my biggest concern is that I feel like it always needs to fold. Like that should be a requirement at comptime
Cause it would be really bad perf otherwise and really awkward ux if the result manifests
what else would it do? :thinking:
I guess my comment doesn't apply to this syntax specifically but to the original design.
Possibly silly idea: add a comptime designator like zig that requires that arg to be constant folded at compile time, else failing the compilation
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
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
instead of the current syntax, which is:
color = { Random.map_both <-
r: Random.u8(),
g: Random.u8(),
b: Random.u8(),
}
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
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)
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!
dictionaries too:
[
("a", 1),
("b", 1),
("b", 1),
("c", 1),
].Dict
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
Feels strange, but probably just cause it is different
I guess the oddest parts are:
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?
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.
that already has meaning, though - it's a tag application :sweat_smile:
so switching to that would actually be overloading
Sure, but is [1, 2, 2, 3].Set worth it over Set.new([1, 2, 2, 3]).
the difference is that the latter can't tell you at compile time that you had a duplicate in there
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
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:
Also I think "literal suffix" is a great way to explain it
Is it ever going to make sense on other literals... like have you thought about string literals?
or rather, the only way it could tell you at compile time would be if it had a
crashon 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 returnsErrwhen 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.
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?
just to let you know that you've made a mistake
dictionary with duplicate keys would be more impactful
maybe you thought a different value was being inserted than what actually was
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 likea => bwas 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))
Yeah, all of this overall makes reasonable sense
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
"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?
yeah that's one use case
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
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
but of course { "_": underscore_thing, "$": dollar } reads nicer for JSON in particular (since it's exactly JSON syntax for objects) than [("_" => underscore_thing), ("$" => dollar)]
This is just more convenient API wise than the alternative. Because we planned to comp-time eval top-levels anyway
yeah, and also the => is very much already taken in Roc
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.
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.
I'm not particularly worried about it
there's no real complexity to it
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.
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!()
(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.)
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?
I guess explicit threading may use a different system or just direct calls....idk
yeah the || are necessary because otherwise you'd just be evaluating them immediately and sequentially :laughing:
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:
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!
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
This looks great!
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
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.
yeah
that's why this distinction matters
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
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:
Other note, any user could opt out by just using Set.new or Dict.new instead of literals
String literals with interpolation will also have nice applications I guess:
div = |content|
"""
<div>${content}</div>
""".Html
pattern = "([A-Z])\w+".RegExp
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?
Seems like content would have to be a Str, how could it be Html?
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)
nevermind, it's unneeded complexity in a lot of places and source of confusion
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
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
I would have expected the .Html to run at compile time, and the interpolation to run at runtime
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).
Or even allow for more flexible interpolation syntax
Kiryl Dziamura said:
I want to explore the interpolation variant a bit
div = |content| """ <div>${content}</div> """.Htmlif it would be possible, should
contentbe type ofStringorHtml?
for this design to work, the entire expression before the .Html would have to be evaluated at compile time
so this example wouldn't work because content is a function argument
if it were a constant, that could be permitted
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
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
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?
yeah I don't think we should have a separate type for that
Richard Feldman said:
for this design to work, the entire expression before the
.Htmlwould 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.
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?
Maybe the example with html is just not a good fit for this feature
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
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
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.
Custom strings may be used instead of single quote btw ("r".U8)
Or bytes only strings, to allow only ASCII
Another thing to explore: type casting between identical types:
TypeB := TypeA
itemA : TypeA
itemB : TypeB
itemB = itemA.TypeB
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.
the shouldn't be allowed anywhere imo
Even at the module level?
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
I need to be free to change what TypeA's internal representation is without breaking anyone's code
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
So no "no" to use in modules
well the uppercased thing refers to a module
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
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:
But likely structured nominal types have more sense
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
TypeB := TypeA
itemA : TypeA
itemB : TypeB
itemB = TypeB.(itemA)
I mean this one: https://roc.zulipchat.com/#narrow/channel/304641-ideas/topic/number.20literals.20for.20custom.20number.20types/near/538829894
Let me try on my own
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?
Ok, I'm convinced. Much better :smile:
I just noticed .Type may be used not only for literals. But agree, unsafe and verbose
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?
so we want single quote anyway for pattern matches, e.g. '.' =>
and I'm not sure what the advantage would be of "r".U8 if we already have single quote
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.
".".U8 => should indeed work! I hadn't thought about that
Minorly related node. In roc, it would be ".".U32
either should work
Fair, but single quotes are U32, or are they also both?
Ha, indeed. In roc, it's utf-8, not ascii after all
single quotes are just number literals
so they take on whatever type based on how you use them
they can be signed, unsigned, whatever size, etc.
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
I guess? haha
doesn't really seem like a problem :shrug:
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
That feels a bit too magical to me....
It is just a non-explicit version of what is in this thread though.
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?
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.
we originally didn't
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
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
otherwise you have to hardcode the Unicode Code Point number to compare to the U8, which people did, and it was terrible for readability
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
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)
in other words, allow things like "x".U8 => in patterns?
it would be more verbose but could work in theory
I'll say I like the idea of trying it out
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
we certainly know how to do it if desired, and obviously "x".U8 patterns can be used for a lot more
Feels like a ton of noise in pattern matching, but maybe that would get glazed over
it's definitely noisier, but I wonder if it's ok in practice...here's an old example
# 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]
}
# 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]
}
# 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]
}
looks fine to me honestly
the first one (numbers only) is the only one that's unpleasant to read imo
Are strings not utf8 by default?
.to_utf8 is for getting a List(U8) representation of the string
yeah I'm just keeping with the example
probably not common
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.
I think ._ should be a separate thread, seems like a potential rabbit hole discussion :smile:
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)
]
one of the ideas behind this design is to not show the types of number literals (or strings) in the repl anymore
on the theory that:
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
Yeah, not awful, but feels noisy for no meaningful reason
Also, think about matching lists of bytes
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?
yeah!
Last updated: Jun 16 2026 at 16:19 UTC