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)
Or some simpler examples:
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!
read_file! (Path) => Result(List(U64), ReadErr)
Utc.now! : () => Instant
File.save! : (File) => {}
Most types will look the same, as we still use whitespace and arrow symbols as clear gaps between function inputs and outputs
hm, I may have missed something - in this idea, do functions always put parens around their args?
Yep, since that's what makes sense with ()
But now types use the same mechanism for composition as values, e.g. ( and )
so wouldn't it be read_file! : (Path) => ... instead?
Ah, thanks
I think without function bodies, this looks more verbose without much benefit
But with their context, this looks better
arg_to_u64 : (Arg) -> Result(U64, ParseErr)
arg_to_u64 = |arg|
str = arg.as_str()?
str.to_u64()
As opposed to
arg_to_u64 : Arg -> Result U64 ParseErr
arg_to_u64 = |arg|
str = arg.as_str()?
str.to_u64()
I think this is a benefit esp. if it means () does not need to desugar to (())
The main benefit overall though is that we now consistently wrap function args with () and unblock zero-arg functions
As Ayaz said
I do appreciate the terseness of current annotations, but I prefer consistency over concision
some random thoughts:
Str -> Str reads better than (Str) -> Str, all else being equal, but...() -> Str is the nicest 0-arg syntax, and if that's the syntax for 0-arg functions, then Str -> Str feels inconsistent but (Str) -> Str feels consistentList.map : (List(a), ((a) -> b)) -> List b compared to List.map : List a, (a -> b) -> List boh I guess on that last one, it can have 1 fewer group of parens :thinking:
List.map : (List(a), (a) -> b) -> List(b)
compared to today:
List.map : List a, (a -> b) -> List b
I think in that example I prefer today's syntax, but I could get used to the other one
For today's syntax, I guess () special-cases to zero-args?
yeah
definitely less consistent
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
this would look like a bit much to me:
(Str) -> ((Str) -> ((Str) -> Str))
I think currying will be very rare in Roc going forward, though
If this makes it more awkward, I see that as a benefit
it's already very rare, to be fair :big_smile:
and yeah I don't think that's a big deal one way or the other
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
I think that Fn or fn might make these look nicer? But it'd be better if we didn't need them
I don't think we should do that
fn makes sense in Gleam because it's also the keyword they use to start function expressions
It makes sense for inline types, but not without
and in Rust it makes sense because it's a trait
but neither of those apply to Roc
Sam Mohr said:
weave : CliBuilder a action1 action2, CliBuilder b action2 action3, (a, b -> c) -> CliBuilder c action1 action3is 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
I also like that it means trailing commas can look reasonable in multiline function types
, followed immediately by -> always looked strange to me
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.
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.
that's a strong point!
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).
This is despite that the one truly universal programming language - sh (and it's descendents) - is whitespace applied :smile:
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!
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
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
I see support for the idea of () -> Str for 0-arg syntax, but we can do that either way
I see a strong stylistic preference against the change from one of two known people using Roc at work :big_smile:
and then I see the arguments in favor being things like it simplifying the parser and makes things a bit more consistent
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
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:
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
Consistent syntax is easier to teach and easier to learn
true, but we've never had any problems teaching the existing syntax in this case
naturally it can always be even easier
Sure. The syntax doesn't bother me coming from a heavy typescript background (I know, I'm really selling it)
But there is no doubt, it changes the aesthetics of the language in an important way
I will say, all else being equal, I think I'd rather read through docs in the status quo syntax
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
it's not a strong preference, but if I were stack ranking them I'd put status quo ahead
that was the argument behind turbofish in Rust
I think a language with constructs that annoy me a little, but allow parsing to be lightning fast can be very nice to use
Just to do anything to make my edit/compile/test loop faster
I don't think this would make a noticeable difference in our parsing speed in practice
Oh for sure not
just code base simplicity
And that too. Which might be less bugs, but that's kind of our job to manage
We don't want to be Go necessarily, optimizing the entire grammar to be parsed as quickly as possible
well the way I look at that is that what we're doing is being a big multiplier
we spend X time and all the people using the language get to benefit from that
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.
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
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
although separately I do still think we should have () => Str be the syntax for 0-arg functions
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?
yeah I think that's the consistency argument :big_smile:
I don't personally mind the inconsistency though
I think because the types are separate from the expressions
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
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
It's also a nice nod to Elm's syntax -- so we're not throwing everything away
Skimming back through all the repo's I've upgraded for PNC and I think I strongly prefer the current syntax for types
It feels cleaner and less cluttered
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()
Arg with arg, and it grounds the | |s - kind of like they're "unpacking" the arguments packaged in parens.(I'm advocating for less paren-centrism elsewhere, but I'm a sucker for vertical alignment! :laughing:)
Unfortunately the second | being aligned too will be rare.
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!
The large example is Richard's realworld app
Oh, I dont think there is any plan to use parens for types.
The topic's not resolved! :stuck_out_tongue_wink:
I can fork that app to explore it, or get a medium-sized snippet instead.
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
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:
Does that ceiling apply to not adding any |args| vertical lines too?
I remember trying that and ruling it out
we talked about it somewhere in some thread
I think it looked really unpleasant in multiline types
Its doesn't show which bracket is the opener
So nesting is weird
ah right, that too
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>]
I like that
Though (a) is a bit strange
I guess I mostly like types being distinct from normal code
Which we already have with WSA
Would also have work angle brackets instead of parens
I think I would only want parens if we were like zig with functions taking and returning types
I'm not a fan of angle brackets in types
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?
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
seems like the high level tradeoffs to me are:
List a - least mainstream, but also least noisyList(a) - closer to mainstream, and more consistent with expressions and patterns, but noisierList<a> - also noisy, not consistent, but most mainstreamIt feels like using parens is better if we don't put parens around the top level function type
yeah I can't name an upside to putting parens around the top-level function type :sweat_smile:
I mean the args, of course
It would make it align more with how Rust looks, or function calls in general
But then it just gets noisy
List a- least mainstream, but also least noisy
I'm just a beginner learning about the maths, but this is how I've been seeing types represented in papers.
Or maybe people just are using Haskell syntax
I think we shouldn't use academics as our lodestone
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.
fair point about tuples, but tuples are rare enough in function type signatures that I don't weight that consideration very highly :big_smile:
also worth noting that the tuple benefit applies to status quo as well
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
Then tags can have exclusive parens while other generics would have angle brackets :big_smile:
Just joking, I anticipate problems with refactoring
Richard Feldman said:
I can appreciate the visual consistency of the type
[Ok(a), Err(b)]with the expressions and patterns, which areOk(a)andErr(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.
Yeah, I definitely like status quo best than, but parens would be ok I guess.
yeah that's my general feeling too
like in a world where we didn't have any syntax for types, I could see myself being convinced of either syntax :big_smile:
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.
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.
that's a fair point, I forgot about that :thinking:
...though it's still barely not "fully" because of types like init! : List(Arg) => Result((Request => Response), [InitFailed(Str)]) needing parens around functions
but that's way more familiar to most devs
Gleam is the only language I know of that uses List(Str) - I can ask Louis about how that impacted learning curve
because they previously used a more Elm-like syntax, which I assume was true at the type level too
I'm doing a Software Unscripted episode with him next week, and I'm sure Roc syntax will come up! :big_smile:
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(_),
]
)
... 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)
: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:
I'm here for this, and I presume it would mean tokenization could be done per line sans context?
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)])
I'm really on the fence about this. I don't feel strongly either way.
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:
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
although the beginner learning curve point does seem like a reasonable tiebreaker to me
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!
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.
Are there parsing benefits to PNC for types?
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.
yeah I agree, I think if the status quo were reversed, it would be a hard sell
like "yeah it's more concise, but now you have to teach a whole new algorithm for figuring out where parens go to beginners"
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
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
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
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
}
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
A block "expression" with two type annotations and no final expression?
Err sorry
Pretend I had written a final expr in both of those :P
Anyway, I think in an indentation-insensitive world, we have to disallow splitting types onto multiple lines
Wouldn't MyType a b in the RHS of the type be MyType(a,b)?
I'm assuming WSA types here
Ahk, I misinterpreted "In the absence of indentation sensitivity and WSA types"
Ahhh
Parens are important, on multiple levels!
in (the absence of indentation sensitivity) and (WSA types)
Anyway, I'm a bit worried about WSA types, causing problems without indentation sensitivity
Do you have the same concern with PNC types?
Jan's example above doesn't have any examples with type vars...
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"
Here's your example in PNC..
func = |x| {
MyFuncType(a, b) : Str -> MyType(a, b)
MyType(a, b) : [A(a), B(b)]
x
}
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
With WSA you can continue on the next line with no symbol
It sounds like we need PNC types if we want whitespace to be insignificant then.
That, or we have to be very clear that if you want to split a WSA type, you have to put parens around it
There's no "just indent it on the next line", that's been a fallback for a while
ok that plus the beginner learning curve consideration makes me think we should switch to parens
but I'm curious what others think! Does anyone feel strongly that we shouldn't do it?
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.
The beginner learning curve makes a lot of sense
I’ll try it out in a project and report back :ok:
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:
(, ,, ))?)>>> I just check out". The type might not be crazy, but the perceived complexity is high. I think "special char count" adds to that perception. Here we are discussing adding more visual nesting which often will take a type from ) to )), or )) to ))), and sprinkling in some ,.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 "that separates your arguments". In the After their meaning is overloaded.As always, I'd be fine with this change. I prefer the status quo.
Trying this out more I notice:
foo! : Str => Result (a -> b) FooError (vs foo! : Str => Result((a -> b), FooError)). You'll notice a function at a glance, just like you notice an effect via ! or =>. This is probably helpful to newcomers too as first-class functions are new to many, and a weird concept to wrap your head around at first: "a function that takes a function as an argument" or "function that returns a function" :scream:List or some other container type, so even in small signatures you'll have a noticeable diff:# 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!
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)
I am a fan of WSA. But I am an even bigger fan for consistency. Therefore I would prefer PNC for the types.
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.
I don't want to add more clauses
I wish we didn't need where clauses but I don't know of any reasonable alternative :sweat_smile:
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)
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
I don't think we should make any syntax changes because of nested types
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
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).
Arrows are really nice at distinguishing functions, and now this is harder to glance the structure of
Something like fn(a) => b would be more viable in my eyes
hm, I think it's confusing that fn(a, b) means "you call it as fn(a) and it returns b"
fn(a, b) strongly suggests to me that it's a function which takes two arguments
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
I'm pretty for PNC types
I'm VERY against doing Java style function types (return value in the Parens)
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.
In lieu of fn(a, b), I wonder if we could do something like fn a -> b or more rust-like, fn(a)->b?
I really don't want to use a keyword in there
would it help if we required having parens around the function type?
if it's in a tuple I mean
Which would make the zero-arg function type syntax more consistent with normal function syntax
Though the parens-less syntax is less cluttered and therefore more readable IMO
I just mean specifically if the function is in a tuple :big_smile:
Oh, sure
that might just be required actually :thinking:
so doesn't help
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:
Richard Feldman said:
I really don't want to use a keyword in there
Why not?
hey Anton! :smiley:
the basic reason is that I think an important concept here is that "functions are ordinary values"
for example, we have x = 1, or x = "blah" or x = |arg1| ...
we don't have x = fn |arg1| because why are functions the only literal that need a keyword?
same reason I prefer not to have separate syntax for defining named functions vs anonymous ones
it's not like there's separate syntax for defining named number constants :laughing:
I have always considered the \a, b syntax a bit weird though.
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
@Anthony Bullard doing what? I've lost track with this thread
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.
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
main! : List(Str) -> Result({}, _)
main! = |_| {
...
Is very clear and unambiguous
Just implement it, why not
Yeah I thought the decision was made already to try it
Ok, I guess Richard can slap my hand later
The topic wasn't resolved
Ask for forgiveness, not permission!
And even with resolved topics, you can't mark the post where the resolution was made
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
So hard to find it, but I'm going to do it because the code I have now is DISGUSTING
Luke Boswell has marked this topic as resolved.
yeah let's do parens in types!
Last updated: Jun 16 2026 at 16:19 UTC