My understanding is that from_quote is always executed at compile time, and if it compiles successfully, we can entirely ignore the error case. Is that correct?
If so, are the following basically equivalent (they don't call the same function but they do the same thing)?
d: Date
d = "2018" # actually calls Date.from_quote("2018")
Ok(d) = Date.from_str("2018")
Why do we need both from_quote and from_str? Couldn't we get away with a single function?
Also, why must from_quote return Try(_, [BadQuotedBytes(Str)])? Couldn't it allow any other error?
For context, there's an Exercism exercise where the tests look like this:
expect {
result = shared_birthday(["1966-07-29", "1977-02-12", "2001-12-25", "1980-11-10"])
result == Ok(Bool.False)
}
The function must simply parse the strings and return True if any two dates share the same day & month. The type spec of the shared_birthday function could be:
shared_birthday : List(Str) -> Try(Bool, _)
The user would have to implement this function and take care of parsing the dates (returning an error in case of a parsing error).
But it might be more instructive and more idiomatic to define it as:
Date :: {year: I64, month: U8, day: U8}.{
from_quote : Str -> Try(Date, [BadQuotedBytes(Str)])
...
}
shared_birthday : List(Date) -> Bool
In this case the test above would look like this instead:
expect {
shared_birthday(["1966-07-29", "1977-02-12", "2001-12-25", "1980-11-10"])
}
Wdyt?
Aurélien Geron said:
My understanding is that
from_quoteis always executed at compile time, and if it compiles successfully, we can entirely ignore the error case. Is that correct?
yep!
If so, are the following basically equivalent (they don't call the same function but they do the same thing)?
d: Date d = "2018" # actually calls Date.from_quote("2018") Ok(d) = Date.from_str("2018")
also yep!
Why do we need both
from_quoteandfrom_str? Couldn't we get away with a single function?
Also, why mustfrom_quotereturnTry(_, [BadQuotedBytes(Str)])? Couldn't it allow any other error?
These are related. I intentionally chose unusual names for the methods that have special meaning to the compiler (e.g. from_quote instead of naming it from_str) because the compiler is only capable of reporting a string error message to the user. If we allowed other errors, the compiler wouldn't know what to do with them; from_quote is hardcoded to only know how to handle that one type of error (and the way it handles it is by displaying the Str inside BadQuotedBytes to the user in the compile-time error message)
so the purpose of keeping them separate in some cases is that from_str can have richer error descriptions than what the compiler supports
also even if your error is just "a Str describing what went wrong" in both from_str and from_quote, you might still want to phrase that string's contents differently depending on whether you expect it to be displayed to a programmer (from_quote) or potentially an end user (from_str)
so in terms of "what does the function do on success," I'd expect from_quote and from_str to commonly work exactly the same way
but I think it's totally reasonable for them to behave differently with regards to how they handle errors, given that one should only be used at compile-time by Roc's compiler, and the other should be used more often at runtime
does that make sense?
That makes sense, thanks for the explanation.
I've used from_quote in a couple of exercises which had tests containing code such as this:
result = stopwatch.advance_time("00:12:58")
One option would be to parse this at runtime, but since the test specs only contain valid times, I thought it would be interesting to show the user one example of from_quote, so the stub code contains something like:
Time :: { ... }.{
# This from_quote function parses a string formatted as "HH:MM:SS" to a Time
from_quote : Str -> Try(Time, [BadQuotedBytes(Str)])
from_quote = |time_str| {
crash "Please implement the 'from_quote' function"
}
...
}
This works fine, and I think it shows a nice use case for from_quote.
However, it got me thinking. Consider this code again:
result = stopwatch.advance_time("00:12:58")
Nothing tells me that the string is actually a Time, I have to look at the advance_time signature. I guess that's true of result as well, but I feel like that's more obvious, whereas it's easy to think that "00:12:58" is just a Str. Maybe that's not a big deal, but I like Zig's philosophy of avoiding magic as much as possible, so I'm wondering whether from_quote(and from_numeral, etc.) are really good ideas.
One might imagine another approach, for example using string prefixes (as exist in Rust, C++, Python, JS, or Swift) such as:
import Time exposing [t_quote]
result = stopwatch.advance_time(t"00:12:58") # t_quote("00:12:58") called at compile time
This way the conversion is explicit, there's no risk to confuse this with a Str, and the syntax remains convenient and readable.
I know that this ship has probably sailed, but hey we're not quite at 0.1.0 yet, so maybe it's still time to change this?
oh, to be explicit without an annotation you can do "00:12:58".Time
which is basically syntax sugar for:
{
Ok(val) = Time.from_quote("00:12:58")
val
}
Thanks, good to know! However, programmers will often choose the most convenient option, so they'll write"00:12:58" if they're not forced to write "00:12:58".Time. Perhaps the compiler should only allow the "00:12:58".Time syntax.
Basically there's a spectrum from Zig (zero magic) to Ruby (total freedom—magic and monkey patching galore). There's no right or wrong, it depends on what you want to do. Zero magic is awesome for production code, high reliability, team work, ease of maintenance and debugging. Total freedom is awesome for quick scripts and expressive DSLs. My understanding is that Roc wants to find a sweet spot between these two extremes, but it errs much closer to the no-magic side. I feel like from_quote pushes the needle towards magic, and I'm not sure the payoff is worth it.
if we only allowed that, then custom number types would all have to write out e.g. 1.BigNum, and functions that accept a Path couldn't be called with "foo.txt" - it would have to be "foo.txt".Path :smile:
but I think you're making a reasonable argument that Time in particular should not have a from_quote
Richard Feldman said:
if we only allowed that, then custom number types would all have to write out e.g.
1.BigNum, and functions that accept aPathcouldn't be called with"foo.txt"- it would have to be"foo.txt".Path:smile:
Good points. Perhaps the plain syntax ("00:12:58") could be forbidden by default, allowing only the postfix version ("00:12:58".Time), but the programmer could override that somehow? Just thinking out loud.
Richard Feldman said:
but I think you're making a reasonable argument that
Timein particular should not have afrom_quote
Yeah, that's what I thought once I finished coding it. That said, it's a good opportunity to highlight the pros and cons of from_quote, so I think I'll keep it and just add a comment with the pros and cons (I might do the same with the lenses exercise: this way users see the pros and cons, which strongly push against lenses in Roc, thanks to the convenient record update syntax).
sweet, makes sense to me! Thanks for talking through it! :smiley:
Last updated: Oct 15 2026 at 17:56 UTC