Stream: ideas

Topic: ✔ Using parens for types


view this post on Zulip Sam Mohr (Jan 13 2025 at 04:48):

As an extension to Richard's recent proposal to prioritize zero-arg functions over empty tuples (#ideas > static dispatch - tuple accessors and zero-arg functions), I think we should consider (or reconsider?) the use of parens-based types for Roc.

Roc is currently transitioning from space-delimited function calls (e.g. Num.to_str 123) to parentheses-delimited function calls (e.g. Num.to_str(123)) in an effort to align with the aesthetic of mainstream languages and to support static dispatch. This looks great so far, but our type annotations are now a relic of that Elm tree in our heritage, and are visually distinct from our values. How would the team feel about making things more consistent and using Gleam-style parentheses for type application and function arguments in Roc?

-- old style
ReadResult data : [Ok { data: data, length: U64 }, Err ReadErr]

-- new style
ReadResult(data) : [Ok({data: data, length: U64}), Err(ReadErr)]
weave :
    CliBuilder a action1 action2,
    CliBuilder b action2 action3,
    (a, b -> c)
    -> CliBuilder c action1 action3

is now

weave : (
    CliBuilder(a, action1, action2),
    CliBuilder(b, action2, action3),
    (a, b) -> c,
) -> CliBuilder(c, action1, action3)

view this post on Zulip Sam Mohr (Jan 13 2025 at 04:49):

Or some simpler examples:

view this post on Zulip Richard Feldman (Jan 13 2025 at 04:49):

Sam Mohr said:

I think we should consider (or reconsider?) the use of parens-based types for Roc.

I don't think we ever talked about it in depth, but I don't have strong feelings on this one either way - both options seem fine. Curious what others think!

view this post on Zulip Sam Mohr (Jan 13 2025 at 04:50):

read_file! (Path) => Result(List(U64), ReadErr)

Utc.now! : () => Instant

File.save! : (File) => {}

view this post on Zulip Sam Mohr (Jan 13 2025 at 04:50):

Most types will look the same, as we still use whitespace and arrow symbols as clear gaps between function inputs and outputs

view this post on Zulip Richard Feldman (Jan 13 2025 at 04:51):

hm, I may have missed something - in this idea, do functions always put parens around their args?

view this post on Zulip Sam Mohr (Jan 13 2025 at 04:51):

Yep, since that's what makes sense with ()

view this post on Zulip Sam Mohr (Jan 13 2025 at 04:51):

But now types use the same mechanism for composition as values, e.g. ( and )

view this post on Zulip Richard Feldman (Jan 13 2025 at 04:51):

so wouldn't it be read_file! : (Path) => ... instead?

view this post on Zulip Sam Mohr (Jan 13 2025 at 04:51):

Ah, thanks

view this post on Zulip Sam Mohr (Jan 13 2025 at 04:56):

I think without function bodies, this looks more verbose without much benefit

view this post on Zulip Sam Mohr (Jan 13 2025 at 04:56):

But with their context, this looks better

view this post on Zulip Sam Mohr (Jan 13 2025 at 04:57):

arg_to_u64 : (Arg) -> Result(U64, ParseErr)
arg_to_u64 = |arg|
    str = arg.as_str()?
    str.to_u64()

view this post on Zulip Sam Mohr (Jan 13 2025 at 04:58):

As opposed to

arg_to_u64 : Arg -> Result U64 ParseErr
arg_to_u64 = |arg|
    str = arg.as_str()?
    str.to_u64()

view this post on Zulip Ayaz Hafiz (Jan 13 2025 at 04:59):

I think this is a benefit esp. if it means () does not need to desugar to (())

view this post on Zulip Sam Mohr (Jan 13 2025 at 04:59):

The main benefit overall though is that we now consistently wrap function args with () and unblock zero-arg functions

view this post on Zulip Sam Mohr (Jan 13 2025 at 04:59):

As Ayaz said

view this post on Zulip Sam Mohr (Jan 13 2025 at 05:00):

I do appreciate the terseness of current annotations, but I prefer consistency over concision

view this post on Zulip Richard Feldman (Jan 13 2025 at 05:01):

some random thoughts:

view this post on Zulip Richard Feldman (Jan 13 2025 at 05:01):

oh I guess on that last one, it can have 1 fewer group of parens :thinking:

view this post on Zulip Richard Feldman (Jan 13 2025 at 05:01):

List.map : (List(a), (a) -> b) -> List(b)

view this post on Zulip Richard Feldman (Jan 13 2025 at 05:02):

compared to today:

List.map : List a, (a -> b) -> List b

view this post on Zulip Richard Feldman (Jan 13 2025 at 05:02):

I think in that example I prefer today's syntax, but I could get used to the other one

view this post on Zulip Sam Mohr (Jan 13 2025 at 05:03):

For today's syntax, I guess () special-cases to zero-args?

view this post on Zulip Richard Feldman (Jan 13 2025 at 05:03):

yeah

view this post on Zulip Richard Feldman (Jan 13 2025 at 05:03):

definitely less consistent

view this post on Zulip Richard Feldman (Jan 13 2025 at 05:05):

I guess curried functions would probably format without nested parens?

(Str) -> (Str) -> (Str) -> Str

vs today:

Str -> (Str -> (Str -> Str))

(we could also do this today, but I prefer the formatting with explicit parens):

Str -> Str -> Str -> Str

view this post on Zulip Richard Feldman (Jan 13 2025 at 05:06):

this would look like a bit much to me:

(Str) -> ((Str) -> ((Str) -> Str))

view this post on Zulip Sam Mohr (Jan 13 2025 at 05:06):

I think currying will be very rare in Roc going forward, though

view this post on Zulip Sam Mohr (Jan 13 2025 at 05:06):

If this makes it more awkward, I see that as a benefit

view this post on Zulip Richard Feldman (Jan 13 2025 at 05:07):

it's already very rare, to be fair :big_smile:

view this post on Zulip Richard Feldman (Jan 13 2025 at 05:07):

and yeah I don't think that's a big deal one way or the other

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

looks like Gleam uses almost the same syntax as Rust for this - Rust uses Fn(Arg1, Arg2) -> Ret and Gleam uses fn(Arg1, Arg2) -> Ret

view this post on Zulip Sam Mohr (Jan 13 2025 at 05:23):

I think that Fn or fn might make these look nicer? But it'd be better if we didn't need them

view this post on Zulip Richard Feldman (Jan 13 2025 at 05:28):

I don't think we should do that

view this post on Zulip Richard Feldman (Jan 13 2025 at 05:28):

fn makes sense in Gleam because it's also the keyword they use to start function expressions

view this post on Zulip Sam Mohr (Jan 13 2025 at 05:28):

It makes sense for inline types, but not without

view this post on Zulip Richard Feldman (Jan 13 2025 at 05:28):

and in Rust it makes sense because it's a trait

view this post on Zulip Richard Feldman (Jan 13 2025 at 05:29):

but neither of those apply to Roc

view this post on Zulip Richard Feldman (Jan 13 2025 at 05:31):

Sam Mohr said:

weave :
    CliBuilder a action1 action2,
    CliBuilder b action2 action3,
    (a, b -> c)
    -> CliBuilder c action1 action3

is now

weave : (
    CliBuilder(a, action1, action2),
    CliBuilder(b, action2, action3),
    (a, b) -> c,
) -> CliBuilder(c, action1, action3)

I do like that the ) -> in the second example makes it more visually distinctive where the args end and the return value begins in multiline function definitions

view this post on Zulip Richard Feldman (Jan 13 2025 at 05:33):

I also like that it means trailing commas can look reasonable in multiline function types

view this post on Zulip Richard Feldman (Jan 13 2025 at 05:33):

, followed immediately by -> always looked strange to me

view this post on Zulip Anthony Bullard (Jan 13 2025 at 11:19):

The biggest advantage of this for the compiler is that this completely removes the need for us to deal with whitespace application anywhere in the grammar.

view this post on Zulip Niclas Ahden (Jan 13 2025 at 12:30):

Sometimes I cry at night because of PNC. Then I read the SD proposal and all is well. As a consumer/language user I don't see the same strong positive here to cover the cost of the additional verbosity. I really enjoy that Roc is fast and lightweight to type and read.

Down the line I'd be fine with the extra parens, but I'd hope that it gave someone somewhere a substantial gain (e.g. easier grammar, even though I'm still hopeful for PNC-less-DSLs for HTML etc. which would eliminate that gain). I see the symmetry argument, but I'm not convinced that symmetry is more important than ease of typing/visual parsing. Nothing really beats "fewer meaningless (to me) characters" in that regard.

view this post on Zulip Richard Feldman (Jan 13 2025 at 12:35):

that's a strong point!

view this post on Zulip Anthony Bullard (Jan 13 2025 at 14:12):

I feel your pain @Niclas Ahden but I have come to the opinion that while PNC is "uglier" in a sense, it is unambiguous and easy for both a human and a computer to parse (the latter especially in a recursive descent style). Having auto-surround and making sure my theme has parens with a low alpha/saturation helps me a lot.

The biggest gain outside of parsing (and enabling things like static dispatch), is the familiarity. Whitespace application eats a lot of strangeness budget for many programmers. The same can be said for whitespace delimited blocks (less so, since Python is popular) and implicit returns - but those don't seem to be as big of a deal. Though there are some people out there that refuse to use implicit return (no ;) in Rust (I know that some of them like the ThePrimeagen are just noisy cogs not really familiar with functional/expression-based languages).

view this post on Zulip Anthony Bullard (Jan 13 2025 at 14:13):

This is despite that the one truly universal programming language - sh (and it's descendents) - is whitespace applied :smile:

view this post on Zulip Niclas Ahden (Jan 13 2025 at 15:14):

Sorry if I misled you with my overly dramatic antics @Anthony Bullard :sweat_smile: I don't have any beef with PNC since it's leading to SD. Without SD I would probably prefer PNC-less, and I wrote down my counters to your arguments, but realized that it's not benefiting this thread. Let me know if you want to discuss that elsewhere or in PMs perhaps!

view this post on Zulip Richard Feldman (Jan 14 2025 at 01:51):

Niclas Ahden said:

As a consumer/language user I don't see the same strong positive here to cover the cost of the additional verbosity. I really enjoy that Roc is fast and lightweight to type and read.

I think this is an important point to consider

view this post on Zulip Richard Feldman (Jan 14 2025 at 01:53):

I'm comparing this thread to the snake_case thread, which was also about a stylistic preference with no major semantic tradeoffs involved (unlike parens-and-commas and static dispatch), but the response is much more mixed in this thread compared to that one

view this post on Zulip Richard Feldman (Jan 14 2025 at 01:54):

I see support for the idea of () -> Str for 0-arg syntax, but we can do that either way

view this post on Zulip Richard Feldman (Jan 14 2025 at 01:55):

I see a strong stylistic preference against the change from one of two known people using Roc at work :big_smile:

view this post on Zulip Richard Feldman (Jan 14 2025 at 01:56):

and then I see the arguments in favor being things like it simplifying the parser and makes things a bit more consistent

view this post on Zulip Sam Mohr (Jan 14 2025 at 01:57):

As the guy that made the thread, space-delimited types look better. I'm letting it stir to see if the consistency feels worth the aesthetic drop

view this post on Zulip Richard Feldman (Jan 14 2025 at 01:58):

yeah I think it's important that we don't sacrifice something that feels nicer for quantifiable benefits just because we can quantify them more easily - that doesn't necessarily make them more valuable overall! :big_smile:

view this post on Zulip Richard Feldman (Jan 14 2025 at 02:00):

I have similar "the aesthetic downsides are real but the all-around benefits are so big they seem worth it" feelings about PNC and static dispatch, and although I don't have particularly strong feelings one way or the other when it comes to the two options for types here, I do think the upsides are much less than they are at the expression level

view this post on Zulip Anthony Bullard (Jan 14 2025 at 02:01):

Consistent syntax is easier to teach and easier to learn

view this post on Zulip Richard Feldman (Jan 14 2025 at 02:02):

true, but we've never had any problems teaching the existing syntax in this case

view this post on Zulip Richard Feldman (Jan 14 2025 at 02:02):

naturally it can always be even easier

view this post on Zulip Anthony Bullard (Jan 14 2025 at 02:03):

Sure. The syntax doesn't bother me coming from a heavy typescript background (I know, I'm really selling it)

view this post on Zulip Anthony Bullard (Jan 14 2025 at 02:03):

But there is no doubt, it changes the aesthetics of the language in an important way

view this post on Zulip Richard Feldman (Jan 14 2025 at 02:04):

I will say, all else being equal, I think I'd rather read through docs in the status quo syntax

view this post on Zulip Anthony Bullard (Jan 14 2025 at 02:04):

But as someone working on the compiler (who is the LEAST IMPORTANT PERSON) I see the benefits to consistent, performant, and deterministic parsing and formatting and lust for it

view this post on Zulip Richard Feldman (Jan 14 2025 at 02:04):

it's not a strong preference, but if I were stack ranking them I'd put status quo ahead

view this post on Zulip Richard Feldman (Jan 14 2025 at 02:05):

that was the argument behind turbofish in Rust

view this post on Zulip Anthony Bullard (Jan 14 2025 at 02:06):

I think a language with constructs that annoy me a little, but allow parsing to be lightning fast can be very nice to use

view this post on Zulip Anthony Bullard (Jan 14 2025 at 02:06):

Just to do anything to make my edit/compile/test loop faster

view this post on Zulip Richard Feldman (Jan 14 2025 at 02:06):

I don't think this would make a noticeable difference in our parsing speed in practice

view this post on Zulip Anthony Bullard (Jan 14 2025 at 02:06):

Oh for sure not

view this post on Zulip Richard Feldman (Jan 14 2025 at 02:06):

just code base simplicity

view this post on Zulip Anthony Bullard (Jan 14 2025 at 02:07):

And that too. Which might be less bugs, but that's kind of our job to manage

view this post on Zulip Anthony Bullard (Jan 14 2025 at 02:07):

We don't want to be Go necessarily, optimizing the entire grammar to be parsed as quickly as possible

view this post on Zulip Richard Feldman (Jan 14 2025 at 02:08):

well the way I look at that is that what we're doing is being a big multiplier

view this post on Zulip Richard Feldman (Jan 14 2025 at 02:08):

we spend X time and all the people using the language get to benefit from that

view this post on Zulip Richard Feldman (Jan 14 2025 at 02:09):

so as long as we're able to keep things manageable, it's ok if we pay some implementation complexity cost to make the end user experience better. Error messages are a great example of that.

view this post on Zulip Richard Feldman (Jan 14 2025 at 02:10):

it's a different story if the complexity gets so overwhelming that we can't make the thing work correctly, but that's not where we are with the parser

view this post on Zulip Richard Feldman (Jan 14 2025 at 02:12):

personally I'd say given how this thread has gone, I'd say we stick with the status quo for 0.1.0, if nothing else to cut scope for that project, but be open to revisiting it sometime in the future

view this post on Zulip Richard Feldman (Jan 14 2025 at 02:13):

although separately I do still think we should have () => Str be the syntax for 0-arg functions

view this post on Zulip Luke Boswell (Jan 14 2025 at 02:21):

Having the types look the same as how they are used seems like a really relevant consideration. I haven't really chimed in here because I've been busy this week, and it's hard to keep up with the conversations across threads.

I kind of assumed the WSA -> PNC applied to types as well... but I guess I hadn't really noticed it. I'm definitely in a transition phase, and without Sam kindly picking out the things I've missed (particularly in docs) I don't see it.

Isn't it a little strange to have WSA for the types, and PNC when they are used?

view this post on Zulip Richard Feldman (Jan 14 2025 at 02:22):

yeah I think that's the consistency argument :big_smile:

view this post on Zulip Richard Feldman (Jan 14 2025 at 02:23):

I don't personally mind the inconsistency though

view this post on Zulip Richard Feldman (Jan 14 2025 at 02:23):

I think because the types are separate from the expressions

view this post on Zulip Richard Feldman (Jan 14 2025 at 02:24):

I might feel differently if we had inline types, but with them being separate they aren't really intermingled so visually it looks fine to me

view this post on Zulip Luke Boswell (Jan 14 2025 at 02:24):

I guess there is an argument there to say that's a benefit, like how lowercase idents are different to uppercase -- it's a subtle but significant distinction

view this post on Zulip Luke Boswell (Jan 14 2025 at 02:25):

It's also a nice nod to Elm's syntax -- so we're not throwing everything away

view this post on Zulip Luke Boswell (Jan 14 2025 at 02:28):

Skimming back through all the repo's I've upgraded for PNC and I think I strongly prefer the current syntax for types

view this post on Zulip Luke Boswell (Jan 14 2025 at 02:28):

It feels cleaner and less cluttered

view this post on Zulip jan kili (Jan 15 2025 at 18:29):

There are some nice things about this aesthetically -

arg_to_u64 : (Arg) -> Result(U64, ParseErr)
arg_to_u64 = |arg|
    str = arg.as_str()?
    str.to_u64()

(I'm advocating for less paren-centrism elsewhere, but I'm a sucker for vertical alignment! :laughing:)

view this post on Zulip jan kili (Jan 15 2025 at 18:30):

Unfortunately the second | being aligned too will be rare.

view this post on Zulip jan kili (Feb 12 2025 at 21:23):

Let's get a large example in here that includes all latest syntax proposals, to do a realistic side-by-side of WSA types vs PNC types!

view this post on Zulip Luke Boswell (Feb 12 2025 at 21:23):

The large example is Richard's realworld app

view this post on Zulip Luke Boswell (Feb 12 2025 at 21:24):

Oh, I dont think there is any plan to use parens for types.

view this post on Zulip jan kili (Feb 12 2025 at 21:24):

The topic's not resolved! :stuck_out_tongue_wink:

view this post on Zulip jan kili (Feb 12 2025 at 21:25):

I can fork that app to explore it, or get a medium-sized snippet instead.

view this post on Zulip Richard Feldman (Feb 12 2025 at 21:37):

if you're exploring, I'd say explore this syntax:

List.map : List(a), (a -> b) -> List(b)

the alternatives we talked about earlier in the thread, where there are always parens around the arguments, seemed too noisy in higher-order functions like this

view this post on Zulip Richard Feldman (Feb 12 2025 at 21:38):

I don't currently have strong feelings about this vs status quo, but I do have strong feelings against a version of that List.map signature that has more parens than that one :big_smile:

view this post on Zulip jan kili (Feb 12 2025 at 21:39):

Does that ceiling apply to not adding any |args| vertical lines too?

view this post on Zulip Richard Feldman (Feb 12 2025 at 21:39):

I remember trying that and ruling it out

view this post on Zulip Richard Feldman (Feb 12 2025 at 21:40):

we talked about it somewhere in some thread

view this post on Zulip Richard Feldman (Feb 12 2025 at 21:40):

I think it looked really unpleasant in multiline types

view this post on Zulip Sam Mohr (Feb 12 2025 at 21:40):

Its doesn't show which bracket is the opener

view this post on Zulip Sam Mohr (Feb 12 2025 at 21:40):

So nesting is weird

view this post on Zulip Richard Feldman (Feb 12 2025 at 21:40):

ah right, that too

view this post on Zulip Kiryl Dziamura (Feb 12 2025 at 22:00):

There’s also an option to distinguish function args and type args with angle brackets.

List.map : (List<a>, (a) -> b) -> List<b>
weave : (
    CliBuilder<a, action1, action2>,
    CliBuilder<b, action2, action3>,
    (a, b) -> c,
) -> CliBuilder<c, action1, action3>
arg_to_u64 : (Arg) -> Result<U64, ParseErr>
arg_to_u64 = |arg|
    str = arg.as_str()?
    str.to_u64()
ReadResult<data> : [Ok<{data: data, length: U64}>, Err<ReadErr>]

view this post on Zulip Brendan Hansknecht (Feb 12 2025 at 22:46):

I like that

view this post on Zulip Brendan Hansknecht (Feb 12 2025 at 22:47):

Though (a) is a bit strange

view this post on Zulip Brendan Hansknecht (Feb 12 2025 at 22:47):

I guess I mostly like types being distinct from normal code

view this post on Zulip Brendan Hansknecht (Feb 12 2025 at 22:47):

Which we already have with WSA

view this post on Zulip Brendan Hansknecht (Feb 12 2025 at 22:48):

Would also have work angle brackets instead of parens

view this post on Zulip Brendan Hansknecht (Feb 12 2025 at 22:48):

I think I would only want parens if we were like zig with functions taking and returning types

view this post on Zulip Richard Feldman (Feb 12 2025 at 22:53):

I'm not a fan of angle brackets in types

view this post on Zulip Richard Feldman (Feb 12 2025 at 22:56):

I can appreciate the visual consistency of the type [Ok(a), Err(b)] with the expressions and patterns, which are Ok(a) and Err(b) but if we're not getting that consistency benefit, why introduce a new delimiter instead of having one less delimiter in the language?

view this post on Zulip Richard Feldman (Feb 12 2025 at 22:58):

obviously there's the "it's more mainstream" benefit but if that's the only upside, it seems to me that either status quo or parens would be better

view this post on Zulip Richard Feldman (Feb 12 2025 at 23:10):

seems like the high level tradeoffs to me are:

view this post on Zulip Sam Mohr (Feb 12 2025 at 23:11):

It feels like using parens is better if we don't put parens around the top level function type

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

yeah I can't name an upside to putting parens around the top-level function type :sweat_smile:

view this post on Zulip Sam Mohr (Feb 12 2025 at 23:12):

I mean the args, of course

view this post on Zulip Sam Mohr (Feb 12 2025 at 23:12):

It would make it align more with how Rust looks, or function calls in general

view this post on Zulip Sam Mohr (Feb 12 2025 at 23:13):

But then it just gets noisy

view this post on Zulip Luke Boswell (Feb 12 2025 at 23:21):

I'm just a beginner learning about the maths, but this is how I've been seeing types represented in papers.

view this post on Zulip Luke Boswell (Feb 12 2025 at 23:22):

Or maybe people just are using Haskell syntax

view this post on Zulip Sam Mohr (Feb 12 2025 at 23:23):

I think we shouldn't use academics as our lodestone

view this post on Zulip Kiryl Dziamura (Feb 12 2025 at 23:37):

The angles benefit is not only mainstream, but a way to visually distinguish tuples and functions from types.
Re expressions and patterns argument: patterns to expressions is what construction to deconstruction of data. But you don’t have a data constructor for each type, right? So what’s the point of consistency between constructor and type syntax if they are not always the same? In the case of records and tuples the syntax is truly consistent.

view this post on Zulip Richard Feldman (Feb 12 2025 at 23:39):

fair point about tuples, but tuples are rare enough in function type signatures that I don't weight that consideration very highly :big_smile:

view this post on Zulip Richard Feldman (Feb 12 2025 at 23:40):

also worth noting that the tuple benefit applies to status quo as well

view this post on Zulip Richard Feldman (Feb 12 2025 at 23:42):

Kiryl Dziamura said:

But you don’t have a data constructor for each type, right? So what’s the point of consistency between constructor and type syntax if they are not always the same? In the case of records and tuples the syntax is truly consistent.

I just mean parens is consistent for tag unions specifically, whereas neither of the other syntaxes are

view this post on Zulip Kiryl Dziamura (Feb 12 2025 at 23:48):

Then tags can have exclusive parens while other generics would have angle brackets :big_smile:
Just joking, I anticipate problems with refactoring

view this post on Zulip Brendan Hansknecht (Feb 13 2025 at 00:08):

Richard Feldman said:

I can appreciate the visual consistency of the type [Ok(a), Err(b)] with the expressions and patterns, which are Ok(a) and Err(b) but if we're not getting that consistency benefit, why introduce a new delimiter instead of having one less delimiter in the language?

Oh wait, types are functions if they are tags....nevermind to angle brackets.

view this post on Zulip Brendan Hansknecht (Feb 13 2025 at 00:09):

Yeah, I definitely like status quo best than, but parens would be ok I guess.

view this post on Zulip Richard Feldman (Feb 13 2025 at 00:20):

yeah that's my general feeling too

view this post on Zulip Richard Feldman (Feb 13 2025 at 00:20):

like in a world where we didn't have any syntax for types, I could see myself being convinced of either syntax :big_smile:

view this post on Zulip Kiryl Dziamura (Feb 13 2025 at 00:29):

types are functions

Yes, although there are no other “function calls” inside of the types. However, tags can be implied as irreducible executions. From this perspective, angle brackets for generics and parens for “function calls” don't look that weird...

In any case, I won’t die on this hill, I just like to think about the semantics.

view this post on Zulip jan kili (Feb 13 2025 at 00:50):

I'm refactoring a braces___and_pnc_in_types fork of roc-realworld right now, and the first thing I notice is that being consistent (with extending PNC to types) would eliminate the last traces of tricky WSA parens like in Result (List U8) MyErr - not needing to teach newcomers those was a perk of PNC originally, and this would mean we fully don't need to teach it anymore.

view this post on Zulip Richard Feldman (Feb 13 2025 at 00:56):

that's a fair point, I forgot about that :thinking:

view this post on Zulip jan kili (Feb 13 2025 at 00:58):

...though it's still barely not "fully" because of types like init! : List(Arg) => Result((Request => Response), [InitFailed(Str)]) needing parens around functions

view this post on Zulip jan kili (Feb 13 2025 at 00:58):

but that's way more familiar to most devs

view this post on Zulip Richard Feldman (Feb 13 2025 at 00:59):

Gleam is the only language I know of that uses List(Str) - I can ask Louis about how that impacted learning curve

view this post on Zulip Richard Feldman (Feb 13 2025 at 00:59):

because they previously used a more Elm-like syntax, which I assume was true at the type level too

view this post on Zulip Richard Feldman (Feb 13 2025 at 01:02):

I'm doing a Software Unscripted episode with him next week, and I'm sure Roc syntax will come up! :big_smile:

view this post on Zulip jan kili (Feb 13 2025 at 01:10):

Ooh, I know this will tickle some folks, especially after our braces consensus... Reduced indentation & rigorous delimiters for nested types:

WSA

prepare_list_articles! :
    Client
    => Result
        Cmd (List ListArticlesRow)
        [
            PgExpectErr _,
            PgErr Pg.Error,
            PgProtoErr _,
            TcpReadErr _,
            TcpUnexpectedEOF,
            TcpWriteErr _,
        ]

PNC

prepare_list_articles! : Client => Result(
    Cmd(List(ListArticlesRow)),
    [
        PgExpectErr(_),
        PgErr(Pg.Error),
        PgProtoErr(_),
        TcpReadErr(_),
        TcpUnexpectedEOF,
        TcpWriteErr(_),
    ]
)

view this post on Zulip jan kili (Feb 13 2025 at 01:17):

... though not every multi-line type would have its indentation affected/improved:

WSA

list_articles! :
    Pg.Client,
    Pg.Cmd (List ListArticlesRow),
    Str,
    Str,
    Str,
    U64,
    U64
    => Result
        (List ListArticlesRow)
        [
            PgExpectErr _,
            PgErr Pg.Error,
            PgProtoErr _,
            TcpReadErr _,
            TcpUnexpectedEOF,
            TcpWriteErr _,
        ]
    )

PNC

list_articles! :
    Pg.Client,
    Pg.Cmd(List(ListArticlesRow)),
    Str,
    Str,
    Str,
    U64,
    U64
    => Result(
        List(ListArticlesRow),
        [
            PgExpectErr(_),
            PgErr(Pg.Error),
            PgProtoErr(_),
            TcpReadErr(_),
            TcpUnexpectedEOF,
            TcpWriteErr(_),
        ]
    )

(unless Richard likes these rigorous multi-line delimeters enough to revoke his reasonable "no arg delimeters" constraint nvm, arg wrappers wouldn't provide similar value)

view this post on Zulip jan kili (Feb 13 2025 at 01:29):

:point_down: :point_down: :point_down: :point_down: :point_down: :point_down:
Here's roc-realworld (with braces) with PNC in type signatures:
diff
file tree
:point_up: :point_up: :point_up: :point_up: :point_up: :point_up:

view this post on Zulip Sam Mohr (Feb 13 2025 at 01:30):

I'm here for this, and I presume it would mean tokenization could be done per line sans context?

view this post on Zulip Luke Boswell (Feb 13 2025 at 01:33):

Could we do without the additional braces around a function if we have the , to delimit the type args.

init! : List(Arg) => Result((Request => Response), [InitFailed(Str)])

# like this instead
init! : List(Arg) => Result(Request => Response, [InitFailed(Str)])

view this post on Zulip Luke Boswell (Feb 13 2025 at 01:34):

I'm really on the fence about this. I don't feel strongly either way.

view this post on Zulip Richard Feldman (Feb 13 2025 at 01:34):

yeah, seeing it written out in a larger example, I still don't personally have a strong preference for how either of these look...they both seem fine to me :big_smile:

view this post on Zulip jan kili (Feb 13 2025 at 01:35):

Luke Boswell said:

Could we do without the additional braces around a function if we have the , to delimit the type args.

init! : List(Arg) => Result((Request => Response), [InitFailed(Str)])

# like this instead
init! : List(Arg) => Result(Request => Response, [InitFailed(Str)])

once you have multiple args, the parentheses become necessary, so it seems good for consistency:
init! : List(Arg) => Result((Request, Context => Response), [InitFailed(Str)])

because we don't want arg wrappers

view this post on Zulip Richard Feldman (Feb 13 2025 at 01:35):

although the beginner learning curve point does seem like a reasonable tiebreaker to me

view this post on Zulip Richard Feldman (Feb 13 2025 at 01:36):

given that as an advanced user they both seem similarly fine - if it makes a bigger difference to a beginner's learning curve, that's important too!

view this post on Zulip Luke Boswell (Feb 13 2025 at 01:36):

I lean towards the status quo because it's familiar. But in a world where I had never known Elm or Haskell, I could imagine preferring the parens because it's consistent everywhere.

view this post on Zulip Luke Boswell (Feb 13 2025 at 01:37):

Are there parsing benefits to PNC for types?

view this post on Zulip Luke Boswell (Feb 13 2025 at 01:39):

Doing the Richard classic... if we already had PNC implemented, could we convince ourselves that we should switch to WSA -- and I would point out that it's confusing for beginners.

view this post on Zulip Richard Feldman (Feb 13 2025 at 01:40):

yeah I agree, I think if the status quo were reversed, it would be a hard sell

view this post on Zulip Richard Feldman (Feb 13 2025 at 01:40):

like "yeah it's more concise, but now you have to teach a whole new algorithm for figuring out where parens go to beginners"

view this post on Zulip jan kili (Feb 13 2025 at 01:52):

Luke Boswell said:

Are there parsing benefits to PNC for types?

I believe this would remove WSA parsing from the new compiler and smart words context-free grammar smart words

view this post on Zulip Kiryl Dziamura (Feb 13 2025 at 01:52):

PNC and WSA are fundamentally different. When I learned about Elm/Haskell, I couldn't get used to my new relationship with parens. It's not that obvious when you already have the skill and a parser in your head. But mixing them is very, very confusing

view this post on Zulip Kiryl Dziamura (Feb 13 2025 at 01:59):

Re no parens around single argument: this is one of the most hated things in permissive js syntax for me. Yes, parens bring some noise, but how annoying it is to add arguments when there are no parens yet. And you still have to have parens for no arguments!
But read > write, so no parens around single arg is probably ok

view this post on Zulip Joshua Warner (Feb 13 2025 at 03:35):

With indentation insensitivity and WSA types, I present for you a puzzle:

func = |x| {
    MyFuncType a b: (Str) -> MyType a b
    MyType a b : [A a, B b]
    x
}

view this post on Zulip Joshua Warner (Feb 13 2025 at 03:36):

Does that format to:

func = |x| {
    MyFuncType a b: (Str) -> MyType a b
    MyType a b : [A a, B b]
    x
}

Or:

func = |x| {
    MyFuncType a b: (Str) -> MyType a b MyType a
    b : [A a, B b]
    x
}

AFAICT, either would technically be valid, unless we additionally disallow you from line-breaking in an apply of a WSA type

view this post on Zulip Luke Boswell (Feb 13 2025 at 03:37):

A block "expression" with two type annotations and no final expression?

view this post on Zulip Joshua Warner (Feb 13 2025 at 03:37):

Err sorry

view this post on Zulip Joshua Warner (Feb 13 2025 at 03:37):

Pretend I had written a final expr in both of those :P

view this post on Zulip Joshua Warner (Feb 13 2025 at 03:38):

Anyway, I think in an indentation-insensitive world, we have to disallow splitting types onto multiple lines

view this post on Zulip Luke Boswell (Feb 13 2025 at 03:38):

Wouldn't MyType a b in the RHS of the type be MyType(a,b)?

view this post on Zulip Joshua Warner (Feb 13 2025 at 03:39):

I'm assuming WSA types here

view this post on Zulip Luke Boswell (Feb 13 2025 at 03:39):

Ahk, I misinterpreted "In the absence of indentation sensitivity and WSA types"

view this post on Zulip Joshua Warner (Feb 13 2025 at 03:39):

Ahhh

view this post on Zulip Joshua Warner (Feb 13 2025 at 03:40):

Parens are important, on multiple levels!

view this post on Zulip Joshua Warner (Feb 13 2025 at 03:40):

in (the absence of indentation sensitivity) and (WSA types)

view this post on Zulip Joshua Warner (Feb 13 2025 at 03:42):

Anyway, I'm a bit worried about WSA types, causing problems without indentation sensitivity

view this post on Zulip Luke Boswell (Feb 13 2025 at 03:42):

Do you have the same concern with PNC types?

view this post on Zulip Luke Boswell (Feb 13 2025 at 03:42):

Jan's example above doesn't have any examples with type vars...

view this post on Zulip Joshua Warner (Feb 13 2025 at 03:43):

With type vars, you'd put those in parens - and so it's pretty natural to say "only linebreak inside the parens, not before the parens"

view this post on Zulip Luke Boswell (Feb 13 2025 at 03:43):

Here's your example in PNC..

func = |x| {
    MyFuncType(a, b) : Str -> MyType(a, b)
    MyType(a, b) : [A(a), B(b)]
    x
}

view this post on Zulip Joshua Warner (Feb 13 2025 at 03:44):

Yep, it's no longer possible to continue the type in the absence of a symbol like -> on the next line, so we're fine

view this post on Zulip Joshua Warner (Feb 13 2025 at 03:44):

With WSA you can continue on the next line with no symbol

view this post on Zulip Luke Boswell (Feb 13 2025 at 03:45):

It sounds like we need PNC types if we want whitespace to be insignificant then.

view this post on Zulip Joshua Warner (Feb 13 2025 at 03:51):

That, or we have to be very clear that if you want to split a WSA type, you have to put parens around it

view this post on Zulip Joshua Warner (Feb 13 2025 at 03:52):

There's no "just indent it on the next line", that's been a fallback for a while

view this post on Zulip Richard Feldman (Feb 13 2025 at 05:24):

ok that plus the beginner learning curve consideration makes me think we should switch to parens

view this post on Zulip Richard Feldman (Feb 13 2025 at 05:25):

but I'm curious what others think! Does anyone feel strongly that we shouldn't do it?

view this post on Zulip Brendan Hansknecht (Feb 13 2025 at 06:06):

No strong feelings here. They basically read the same. Arguably WSA reads a bit better. I think the only disadvantage is that more parens means harder to edit. More likely to get things lined up wrong (that said, even with WSA, nesting leads to parens, but less super nested parens that are likely to get messed up). Also, if I have too many parens in a type due to nesting, I would just make an alias.

view this post on Zulip Brendan Hansknecht (Feb 13 2025 at 06:07):

The beginner learning curve makes a lot of sense

view this post on Zulip Niclas Ahden (Feb 13 2025 at 08:00):

I’ll try it out in a project and report back :ok:

view this post on Zulip Niclas Ahden (Feb 13 2025 at 09:21):

I tried it in a few projects and I think it's OK, but I'd prefer status quo.

I agree with the pros posted!

Cons:

Examples from roc-pg:

# Before
from : Table table, (table -> Select a err) -> Query a err
# After
from : Table(table), (table -> Select(a, err)) -> Query(a, err)

# Before
join : Table table, (table -> Expr (PgBool *) *), (table -> Select a err) -> Select a err
# After
join : Table(table), (table -> Expr(PgBool(*), *), (table -> Select(a, err)) -> Select(a, err)

# Before
column : Expr * a -> (Selection (a -> b) err -> Selection b err)
# After
column : Expr(*, a) -> (Selection((a -> b), err) -> Selection(b, err))

Before:

As always, I'd be fine with this change. I prefer the status quo.

view this post on Zulip Niclas Ahden (Feb 13 2025 at 10:09):

Trying this out more I notice:

# Before
get_remote_file_names! : List Str => Result (List Str) _
# After
get_remote_file_names! : List(Str) => Result(List(Str), _)

Again, I prefer the status quo, but I'd be fine with the change, and I hope it's helpful that I lay out the cons!

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

It's a matter of skill, but I find this example confusing for newcomers from non-ML languages:

column : Expr * a -> (Selection (a -> b) err -> Selection b err)

one might read as

column : Expr * (a -> (Selection (a -> b) err -> Selection b err))

because when you got used to commas for arguments, it's easy to recognize a -> ... as lambda with a single argument because of familiarity (e.g. a => ... in javascript)

view this post on Zulip Oskar Hahn (Feb 13 2025 at 12:08):

I am a fan of WSA. But I am an even bigger fan for consistency. Therefore I would prefer PNC for the types.

view this post on Zulip jan kili (Feb 13 2025 at 17:04):

I agree that it's hard to read highly-nested types, but that seems true whether we add one additional layer of parentheses with PNC or not. While we're reconsidering our type syntax, can we address the problem of over-nesting more directly? For example:

join : Table(table), (table -> Expr(PgBool(*), *)), (table -> Select(a, err)) -> Select(a, err)

# vs

join : Table(table), expressor, selector -> Select(a, err)
    with expressor : table -> Expr(PgBool(*), *)
    with selector : table -> Select(a, err)

We're already planning to add where clauses on types, so why not other clauses? You can already do this by simply delegating some of the complexity to a sister type, but there is value in fully-self-contained types.

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

I don't want to add more clauses

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

I wish we didn't need where clauses but I don't know of any reasonable alternative :sweat_smile:

view this post on Zulip jan kili (Feb 13 2025 at 17:17):

If we don't add a clause for this, then how do we feel about the status quo of encouraging sister type delegation? Though I'm not experienced with databases, this is my refactoring impulse:

Expressor(table) : table -> Expr(PgBool(*), *)
Selector(table, a) : table -> Select(a, err)

join : Table(table), Expressor(table), Selector(table, a) -> Select(a, err)
join = ...

I expect this will hide information if you're inspecting join's type, leading to scavenger hunts, but perhaps that's already inevitable? I feel that scavenger hunt a lot in Rust, but not yet in Roc.

(@Kiryl Dziamura jinx, I think we're thinking similarly)

view this post on Zulip Kiryl Dziamura (Feb 13 2025 at 17:17):

can it be solved by type aliases? also, the where clauses won't present in the inferred types, so if the code depends heavily on inference, it might be a hard time to read the generated types anyway

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

I don't think we should make any syntax changes because of nested types

view this post on Zulip Richard Feldman (Feb 13 2025 at 17:28):

I'm sold on parens over whitespace in types, but it's close enough that I think if you add in any other supporting syntax it's not worth it anymore

view this post on Zulip Pit Capitain (Feb 13 2025 at 17:38):

What if we change the function type syntax from a -> b to fn(a, b) and a => b to fn!(a, b)? Multiple args: from a -> b -> c to fn(a, b, c). No-arg functions: fn(a).

view this post on Zulip Sam Mohr (Feb 13 2025 at 17:42):

Arrows are really nice at distinguishing functions, and now this is harder to glance the structure of

view this post on Zulip Sam Mohr (Feb 13 2025 at 17:43):

Something like fn(a) => b would be more viable in my eyes

view this post on Zulip Richard Feldman (Feb 13 2025 at 18:01):

hm, I think it's confusing that fn(a, b) means "you call it as fn(a) and it returns b"

view this post on Zulip Richard Feldman (Feb 13 2025 at 18:01):

fn(a, b) strongly suggests to me that it's a function which takes two arguments

view this post on Zulip Sam Mohr (Feb 13 2025 at 18:01):

Even as an advanced user, I'd constantly be translating the (a, b) in my head to (a) -> b, meaning the syntax doesn't reflect what's happening

view this post on Zulip Anthony Bullard (Feb 18 2025 at 15:29):

I'm pretty for PNC types

I'm VERY against doing Java style function types (return value in the Parens)

view this post on Zulip Joshua Warner (Feb 18 2025 at 20:51):

fn types in tuples are somewhat annoying to parse because the , could either indicate the next element of the list, or a continuation of the same (function) type - so we need to keep both possible realities in mind as we continue to parse.

view this post on Zulip Joshua Warner (Feb 18 2025 at 20:51):

In lieu of fn(a, b), I wonder if we could do something like fn a -> b or more rust-like, fn(a)->b?

view this post on Zulip Richard Feldman (Feb 18 2025 at 22:10):

I really don't want to use a keyword in there

view this post on Zulip Richard Feldman (Feb 18 2025 at 22:14):

would it help if we required having parens around the function type?

view this post on Zulip Richard Feldman (Feb 18 2025 at 22:14):

if it's in a tuple I mean

view this post on Zulip Sam Mohr (Feb 18 2025 at 22:14):

Which would make the zero-arg function type syntax more consistent with normal function syntax

view this post on Zulip Sam Mohr (Feb 18 2025 at 22:15):

Though the parens-less syntax is less cluttered and therefore more readable IMO

view this post on Zulip Richard Feldman (Feb 18 2025 at 22:23):

I just mean specifically if the function is in a tuple :big_smile:

view this post on Zulip Sam Mohr (Feb 18 2025 at 22:24):

Oh, sure

view this post on Zulip Richard Feldman (Feb 18 2025 at 22:25):

that might just be required actually :thinking:

view this post on Zulip Richard Feldman (Feb 18 2025 at 22:25):

so doesn't help

view this post on Zulip Pit Capitain (Feb 20 2025 at 09:13):

By coincidence, I just saw that in "Typed Racket" (a Lisp-like language) the type of a function is (-> a b). For example (: distance (-> pt pt Real)) declares a function with two input parameters https://docs.racket-lang.org/ts-guide/beginning.html#(part._beginning). I suggested a syntax like this because it would be completely whitespace-independent (I think), but I also think that it looks strange :man_shrugging:

view this post on Zulip Anton (Feb 22 2025 at 18:51):

Richard Feldman said:

I really don't want to use a keyword in there

Why not?

view this post on Zulip Richard Feldman (Feb 22 2025 at 19:02):

hey Anton! :smiley:

view this post on Zulip Richard Feldman (Feb 22 2025 at 19:04):

the basic reason is that I think an important concept here is that "functions are ordinary values"

view this post on Zulip Richard Feldman (Feb 22 2025 at 19:05):

for example, we have x = 1, or x = "blah" or x = |arg1| ...

view this post on Zulip Richard Feldman (Feb 22 2025 at 19:05):

we don't have x = fn |arg1| because why are functions the only literal that need a keyword?

view this post on Zulip Richard Feldman (Feb 22 2025 at 19:05):

same reason I prefer not to have separate syntax for defining named functions vs anonymous ones

view this post on Zulip Richard Feldman (Feb 22 2025 at 19:06):

it's not like there's separate syntax for defining named number constants :laughing:

view this post on Zulip Eelco Hoekema (Feb 23 2025 at 14:09):

I have always considered the \a, b syntax a bit weird though.

view this post on Zulip Anthony Bullard (Feb 23 2025 at 22:04):

I've been writing the Parser for type annotations / declarations this weekend and I will say that doing this would make this part of the parser MUCH simpler

view this post on Zulip Luke Boswell (Feb 23 2025 at 22:06):

@Anthony Bullard doing what? I've lost track with this thread

view this post on Zulip Anthony Bullard (Feb 23 2025 at 22:07):

The problem is this:

main! : List String -> Result {} _
main! = |_| {
    ...

When I'm parsing this I am almost forced to use indentation significance to determine that the second main! (a LowerIdent token) isn't another type var for the Result tag.

view this post on Zulip Luke Boswell (Feb 23 2025 at 22:07):

Is this what you're thinking?

Richard Feldman said:

ok that plus the beginner learning curve consideration makes me think we should switch to parens

view this post on Zulip Anthony Bullard (Feb 23 2025 at 22:08):

main! : List(Str) -> Result({}, _)
main! = |_| {
    ...

Is very clear and unambiguous

view this post on Zulip Sam Mohr (Feb 23 2025 at 22:08):

Just implement it, why not

view this post on Zulip Luke Boswell (Feb 23 2025 at 22:08):

Yeah I thought the decision was made already to try it

view this post on Zulip Anthony Bullard (Feb 23 2025 at 22:09):

Ok, I guess Richard can slap my hand later

view this post on Zulip Anthony Bullard (Feb 23 2025 at 22:09):

The topic wasn't resolved

view this post on Zulip Sam Mohr (Feb 23 2025 at 22:09):

Ask for forgiveness, not permission!

view this post on Zulip Anthony Bullard (Feb 23 2025 at 22:09):

And even with resolved topics, you can't mark the post where the resolution was made

view this post on Zulip Luke Boswell (Feb 23 2025 at 22:09):

Just for you Anthony.

Richard Feldman said:

I'm sold on parens over whitespace in types, but it's close enough that I think if you add in any other supporting syntax it's not worth it anymore

view this post on Zulip Anthony Bullard (Feb 23 2025 at 22:10):

So hard to find it, but I'm going to do it because the code I have now is DISGUSTING

view this post on Zulip Notification Bot (Feb 23 2025 at 22:10):

Luke Boswell has marked this topic as resolved.

view this post on Zulip Richard Feldman (Feb 23 2025 at 22:55):

yeah let's do parens in types!


Last updated: Jun 16 2026 at 16:19 UTC