I wrote up a proposal...any feedback welcome!
https://docs.google.com/document/d/1OUd0f4PQjH8jb6i1vEJ5DOnfpVBJbGTjnCakpXAYeT8/edit
I think this is great!!
This is fantastic! I'm very in favor.
I'm not very keen on the .() syntax for chaining local function calls:
pipes =
prev
.pipes
.(updatePipes("extra argument"))
.appendIfOk(pipe)
Given that the syntax is intentionally similar to string interpolation, I assumed initially that the contents would need to be a normal function expression like any of these:
.(someFunction)
.(.updatePipes("extra argument"))
.(\x -> x * x)
It surprised me that the leading . is not required in the case that the function accepts more than one argument.
.(updatePipes("extra argument"))
This seems inconsistent as the code within the .() would not be a valid expression outside of the .().
I propose instead allowing local functions to be chained with : like this:
pipes =
prev
.pipes
:updatePipes("extra argument")
.appendIfOk(pipe)
pipes = prev.pipes:updatePipes("extra argument").appendIfOk(pipe)
This syntax is more concise and seems more natural to me. It is easy to guess how it works, it is cohesive with the . syntax, and it also has a clear visual difference from the . syntax. With this approach you'll be able to immediately communicate to the tooling if you're looking for suggestions for local functions or methods by typing either . or :.
One consideration is that current piping approach and the proposed syntax (I think) both allow lambdas to be used as standalone pipeline stages, but the : syntax does not.
number
|> \x -> x * x
|> Num.toStr
number
.(\x -> x * x)
.toStr()
I think this is fine, or perhaps even a good thing. When I see a standalone lambda in a pipeline, it is a strong indication that the pipeline should instead be broken up with an intermediate definition.
I agree, that syntax is the only part I don't love. I think .. might also work?
pipes =
prev
.pipes
..updatePipes("extra argument")
.appendIfOk(pipe)
I thought of it as a simplification of:
.(.updatePipes("extra argument"))
I think it works well for autocompletion too. When you type the first ., you see all methods, and when you type the second you see all functions
qualified example:
pipes =
prev
.pipes
..Foo.updatePipes("extra argument")
.appendIfOk(pipe)
An upside of : is that it lines up with . because it's only one character
but maybe the distinction is actually a good thing?
Yeah I do like that : keeps everything in line. Having the full autocomplete menu appear when hitting . does seem nice though.
The qualified example looks very weird to me
pipes = prev.pipes..updatePipes("extra argument").appendIfOk(pipe)
I think the .. syntax works less well when the calls are on a single line
Yeah? I feel like it reads the same
I agree the qualified example is a bit weird
It looks very strange to me but of course it's subjective. Part of it is probably because I'm used to seeing foo()..bar() as a typo after I removed a method call from a chain but missed the period when deleting it.
just throwing it out there:
pipes =
prev
.pipes
|updatePipes("extra argument")
.appendIfOk(pipe)
pipes = prev.pipes|updatePipes("extra argument").appendIfOk(pipe)
That is, after all, the one character pretty much every programmer associates with "pipe" :smiley:
While we're leaning towards Gleam's syntax, we can just take their function capture syntax and make calling methods and functions look the same. If you call a function like this:
pipes =
items
.map(.transform("abc", _))
The _ usage calls transform like \x -> transform("abc", x).
But if you do:
pipes =
items
.map(.transform("abc"))
The lack of _means we should look for a method called Item.transform : item, Str -> ABC
I think that makes sense, but I wonder if it's a little too subtle
just throwing it out there:
pipes = prev .pipes |updatePipes("extra argument") .appendIfOk(pipe) pipes = prev.pipes|updatePipes("extra argument").appendIfOk(pipe)That is, after all, the one character pretty much every programmer associates with "pipe" :smiley:
This is a solid option. I still have a slight preference for : though because it looks more consistent with .
The _ syntax seems like a good candidate for adding later if there is enough demand.
I think in the way Sam suggested, it'd be required to distinguish functions from methods
_ both signifies that this is a pipe and where the argument goes
I think the main issue with | is that this symbol is often sorrounded with spaces, but that looks weird if there's a method call after in the same line:
pipes = prev.pipes | updatePipes("extra argument").appendIfOk(pipe)
I think I'm a bit confused about the "make calling methods and functions look the same part".
All syntaxes suggested so far:
pipes = prev.pipes.(updatePipes("extra argument")).appendIfOk(pipe)
pipes = prev.pipes:updatePipes("extra argument").appendIfOk(pipe)
pipes = prev.pipes..updatePipes("extra argument").appendIfOk(pipe)
pipes = prev.pipes|updatePipes("extra argument").appendIfOk(pipe)
pipes = prev.pipes.updatePipes(_, "extra argument").appendIfOk(pipe)
_ looks pretty cool to me now
I'm used to foo.bar(10) meaning that foo is the first argument to bar even if it is very implicit, so having foo.bar(_, 10) instead looks very wrong to me.
I'm thinking about it like this:
_, bar is a method on foo_, bar takes foo in the position the _ appearsthis does mean . works differently depending on the RHS, which as you pointed out, is also a problem in the syntax in the proposal
That's my thoughts exactly, it must be the right way if we both had it
hm, qualified looks pretty confusing, though:
pipes = prev.pipes.Foo.updatePipes(_, "extra argument").appendIfOk(pipe)
technically not ambiguous because only a module name can be uppercase
but that totally looks like pipes is a record with a field called Foo
pipes are very often used with qualified functions, so the syntax should be good for that case
I think I'm back to .. or :
Why can't local functions be chained?
I assume you mean chained with .?
One downside is that there could be conflicts where you might have a method MyType.foo and a local function called foo. When you type value.foo() the compiler would presumably pick the method. If you then deleted MyType.foo, the previous usage would silently fall back to the local function instead.
It would also make it more difficult to determine which calls are local functions and which are methods
I think for the local function chaining, we should look at fully-qualified examples
e.g.
pipes = prev.pipes.(Pipes.update("extra argument")).appendIfOk(pipe)
I also don't love this syntax, but the parens make it clear when it's fully-qualified local call
I think _ is a deep rabbit hole...Scala and LiveScript have it (as mentioned briefly in the doc) and I'd rather not introduce it if we can avoid it
then you get into things like "how about when _ is ..." etc. etc.
For chaining functions imported from another module, would it be .Module.func() or .(Module.func())?
actually I think it's best to talk about examples of both qualified and unqualified side by side!
pipes = prev.pipes.(Pipes.update("extra argument")).appendIfOk(pipe)
pipes = prev.pipes.(updatePipes("extra argument")).appendIfOk(pipe)
so one reason .Module.func() can't work is that if you drop the Module. because you imported func unqualified, now you're at .func() which is a method call rather than a local function call :big_smile:
Me at the top of the doc: ohnoooooobleghhhhhh
Me halfway down: damnit I'm in
(clarification: @Richard Feldman I'm consistently very impressed with your technical writing's clarity and your rhetorical ability to change my mind. The insatiable bikeshedder in me is often sated in this chatroom.)
yeah, that's becoming a pattern with these proposals for me too :laughing:
Random idea about operator overloading:
I remember liking Python's magic methods syntax in 2014, so maybe we could take inspiration from that to avoid users accidentally opting in to an operator - name the de-sugared operator functions something a little goofy but stylish, like ._add_()
Or literally the symbol: (+)
(without allowing you to define new ones)
Roc is my first language with Elm-like whitespace-y pipe chain syntax (see those examples in the Background section of the proposal), and I really like it. I hope that we don't trade much form/style for function/features here. I expect that if this is a more powerful syntax, then the whitespace alternative will be abandoned in a year or so. As long as I can still have a splendid time reading Roc via cat/less/vim, I'm happy.
Significant reduction in # of sad lambda expressions required might be worth a significant reduction in helpfully-qualified function names.
I suppose a motivated qualifier-lover could choose to write .try(Str.toI64) as .(Result.try(Str.toI64)), but that won't be the norm.
Really love the general direction of this, especially how it manages to replace abilities! Couple of things I was thinking as I read this.
For me too one thing that I didn't super like was the .(func) syntax. I want to add another alternative to the ones already proposed:
pipes =
prev
.pipes
.updatePipes
.List:appendIfOk pipe
The idea here is:
. works the same |> does today, in taking any to the right any function that's locally in scope.: but another separator could work too.The above suggestion would break the possibility to use this syntax to replace abilities. We could bring that back by optionally allowing the module qualifier to be ellided:
pipes =
prev
.pipes
.updatePipes
._:appendIfOk pipe
In the above the compiler would replace _ with a module name in the same way that . does by default in the proposal. I'm using _ here because we use that in other places to tell the compiler it should fill in a gap for us, but other syntaxes could work too.
The above design would combine a couple of things I like both in current Roc / Elm and the new design:
|> today is just function application. I think it'd be nice if . were the same, and didn't include scoping behvior as well.. is consistently used to continue the chain, and trigger autocompletion. The above changes maintain that property.Another thing I noticed in the codesnippets is that replacing |> with . removed whitespace and made the code look more compact to me, but not necessarily in a nice way.
I don't know if it's just a matter of getting used to or if there's something more to this, curious if anyone else had this experience, I might just be an outlier.
I was wondering if it'd be an improvement for the formatter to put whitespace after . by default, similar to what we do for |>:
pipes =
prev.pipes
. (updatePipes)
. appendIfOk pipe
This looks a bit different from what folks coming from languages with a C-like syntax are used to, so that's a downside, though you could still write the code without the spaces, so :shrug:
One last thought: with regards to the problems newcomers have with putting parens in the right places to get function calls. One thought that occured to me: If the syntax of the language is such that it allows both C-like parens and ML-like parens to live side-by-side, then we have other options:
While that might help people learning the language, code snippets shared on the internet would still look ML-like so it'd maybe not trigger the "Gleam Effect" of pulling in people that otherwise bounce off the syntax.
a downside of using a separator other than . for module qualifiers is that it would impact the autocomplete experience; in the proposal, if I say either dict. or Dict., in both cases right after I press . I get an autocomplete for whatever's valid in that spot
I like the idea of . always resulting in autocomplete
here's an option we haven't discussed:
pipes = prev.pipes.(updatePipes)("extra argument").appendIfOk(pipe)
pipes = prev.pipes.(Pipes.update)("extra argument").appendIfOk(pipe)
Yes, not needing to think which key to press to bring up the completion list is very helpful. The experience of pressing ., then realising "ahh, it's somewhere in here, not in the module" (or the reverse), deleting ., then pressing that other key isn't that great. I also think piping to a qualified fn is confusing without the parens, so although my initial reaction to the .(fn) syntax wasn't joyful, all examples look and feel great.
Richard Feldman said:
here's an option we haven't discussed:
pipes = prev.pipes.(updatePipes)("extra argument").appendIfOk(pipe) pipes = prev.pipes.(Pipes.update)("extra argument").appendIfOk(pipe)
this has the same number of parens as in the doc, but they aren't as nested. it's basically:
foo.bar(baz) uses static dispatch to find bar
foo.(bar)(baz) uses local scope to find bar
I like this better than the doc version because it doesn't have the weird "using normal-looking function call syntax to call a function with one of its arguments missing" problem
e.g. str.(Str.append("!")) vs str.(Str.append)("!")
a downside of using a separator other than
.for module qualifiers is that it would impact the autocomplete experience; in the proposal, if I say eitherdict.orDict., in both cases right after I press.I get an autocomplete for whatever's valid in that spot
A possible way around this would be to make the module separator .::
pipes =
prev.pipes
. updatePipes
. List.:appendIfOk pi
But also, pushing back on the previous a little, if : were the module separator, then I don't think putting a . after a capitalized word would be valid syntax, right?
I agree that having to try out both . and : to get all the possible completions would be annoying, but if the only thing that can get you completions in that context is : then maybe it's not a problem?
Because there's many other possible autocompletions that might trigger on other keypresses: ( to insert a closing bracket, any letter key to complete an identifier, etc.
I'm really not a fan of foo.(bar)(baz). I think a beginner would have no idea what it means, and it looks like some dynamic dispatch/currying stuff is happening without explanation. Maybe it's just a familiarity with Gleam, but it seems like _ capture syntax is pretty obvious because you have the same number of arguments passed to the function, and the missing one is what gets piped to. It still looks like a function call
With respect to qualified function calls, we can actually easily tell whether a function is qualified based on the capitalization of the function names, right?
pipes =
prev.pipes
.updatePipes
.List.appendIfOk(xyz)
The formatter would just say "any list of title-case names is kept on the same line, and then one last lowercase ident is on the same line"
I'm not sure why we need to qualify things within parentheses
Also, on _ syntax, we can keep it very simple by saying "only one of the function args can be _, and it has to be the whole arg, not nested in an expression"
@Richard Feldman what do you think of the one I suggested at the beginning?
pipes = prev.pipes..updatePipes("extra argument").appendIfOk(pipe)
pipes = prev.pipes..Pipes.update("extra argument").appendIfOk(pipe)
Feels lightweight, distinguished enough from method calling, and the autocomplete experience is really good I think. The first . shows you method + functions, and the second shows you only functions.
You do have to learn what it means but I think that’s true of all the other syntaxes suggested.
I really like this suggestion, and on the point of . vs. .. with respect to auto-complete, you just say:
., look for methods.., look for functionsI think the first . should suggest methods first, but it should also suggest .-prefixed functions after
Though as I said a few messages ago, I think basically the same benefit can come without ... You suggest both methods and functions (methods first), and then:
kinda how rust analyzer suggests &-prefixed fields
the .. syntax works, as far as I can tell
And there's no need for surrounding parens AFAICT
Agus Zubiaga said:
technically not ambiguous because only a module name can be uppercase
I mentioned that yesterday, but it looks like record field access to me
my only concern with that one is that we already use .. to mean certain list-specific things, and we're planning to use it for even more list things, and this would be a use of it that has nothing to do with lists
I disagree that it looks like record field access. The capitalization is not valid as a record field name, and we'd have different color syntax highlighting
Sam Mohr said:
With respect to qualified function calls, we can actually easily tell whether a function is qualified based on the capitalization of the function names, right?
pipes = prev.pipes .updatePipes .List.appendIfOk(xyz)The formatter would just say "any list of title-case names is kept on the same line, and then one last lowercase ident is on the same line"
yes, but if that's the design, how does it look if the call is not qualified?
Richard Feldman said:
my only concern with that one is that we already use
..to mean certain list-specific things, and we're planning to use it for even more list things, and this would be a use of it that has nothing to do with lists
Yeah, that’s valid. We could do .> or something like that.
When you say "the call is not qualified", are you asking how a user would write "call a function in scope that's not a method"?
yeah exactly
(this is why I think whenever we post examples in this thread about non-dispatch chaining, we should always post 2 versions, one qualified and one unqualified, because a lot of syntaxes seem to work fine with one and not at all with the other! :sweat_smile:)
Yes, that's a good callout
Okay, I think it doesn't work, so yep, leaning towards ..
I'd much prefer .. to needing more parentheses personally
It works with auto-complete, it's easy to type, it's visually sparse, and leaves parens to just denote function calls around args
(unless I can figure out a way without it
The one detriment I can see to . and .. is that they are different lengths
But that communicates something, so I don't think that's so bad
I think the collision with how we use .. for lists (and are planning to massively expand that use to things like record updates and possibly type signatures) is the main downside
And I think
data
.first()
|second()
is visually inconsistent with respect to vertical space
That's totally fair
Do we want it that much for lists? haha
I feel like piping is a lot more common
well we already use it for pattern matching and I think the thread where we talked about using it more was really well received
Then to make my thoughts clear, it's a high bar for me to accept
data
.first()
.(second)()
or
data
.first()
(.second())
because there's a strong currying/dynamic dispatch implication that I think is pretty misleading
You're right that this is now syntactically ambiguous (or at least visually):
x = [first..second()]
to me I think of dynamic dispatch being things like foo.dispatch(:bar, arg)
Is second an unqualified function call on object first, or is second() generating a second list
I don’t think that’s ambiguous, that’s not a valid pattern
where there's some "symbol" concept that you send to specify the method
pipes = prev.pipes->updatePipes("extra argument").appendIfOk(pipe)
pipes = prev.pipes->Pipes.update("extra argument").appendIfOk(pipe)
Yeah, sorry. It probably shouldn’t be -> because of the autocomplete experience.
@Agus Zubiaga you're right, we require a comma according to the issue: https://github.com/roc-lang/roc/issues/7093
to me, .(foo) reads like "evaluate the expression foo and then keep going based on that," kinda like $(foo) in string interpolation
I'd be okay with -> or .>, though .> has the benefit of not having a current meaning, whereas -> means "define a closure"
I hadn't thought about it, but I could see allowing foo.(makeFnOnTheFly(bar))(baz) working
not saying that's a good idea, but rather that it explains what the syntax does
Makes sense
I know that in Roc, it's compiled to a lambda set, so it's not true "dynamic dispatch", but foo.(makeFnOnTheFly(bar))(baz) makes perfect sense as a "generate a function and then call it"
I thought about foo->bar but a downside is that in every language that supports it, it means some combination of record access (basically) and/or method calls - so kinda the opposite of what we want it to mean here :sweat_smile:
Which is what makes foo.(bar)(baz) smell weird to me, but it's literally just a vibes thing so that's not a good counterargument
when you say "evaluate the expression foo", that implies a runtime cost
But we don't have a runtime cost, it's just a compiler inference cost
But it's a static dispatch
That's maybe the best I can put it
The syntax implies a runtime cost when it should be more obvious that it's a static call, if possible
vibes are always a valid consideration when it comes to syntax, although for me I like the vibes of foo.bar(baz) vs foo.(bar)(baz) the only thing I've changed is .bar for .(bar) and all that's changing when I do that is how that one function gets determined (static dispatch vs evaluating the expression)
it feels like it composes nicely to me
Oh, I think I misunderstood
If you want a syntax to do "evaluate this expression and then call it"
That's literally perfect
yeah that wasn't what I originally thought, but I realized that's actually a better design haha
I thought we were looking for a way to do "look for an unqualified function and call it statically"
well, and that's a subset of this
in that in general in Roc if I write the expression (foo), foo can certainly happen to be a function
Since it's a pure lookup, it'd constant eval to a static dispatch, sure
yeah exactly
so foo.(bar)(baz) would just be syntax sugar for bar(foo, baz)
it wouldn't involve the type checker at all
and then bar can be an arbitrary expression if you like
Yeah, it works, but I think pipes will still be really common for structural types at the app level and that syntax looks pretty alien to me.
welllllllll... is there a reason we couldn't just keep |> in addition to methods?
pipes =
data
.method()
|> List.map(x -> x + 2)
-- unqualified
pipes =
data
.method()
|> map(x -> x + 2)
I'm okay with your suggestion because:
But I'd rather have something that communicates our goal of "pass this to a local function statically"
the problem with |> is: what if you want to keep calling methods afterwards?
kinda need parens and dots to have one long chain which intermingles method calls and direct function calls
pipes =
data
.method()
|> map(x -> x + 2)
.length()
This requires newlines to have a specific meaning
yeah
looking at that, I'd expect the formatter to move .length() to the previous line :big_smile:
It also doesn’t have the nice autocomplete experience, I think we want . on the first char
true
if the only downside of .(foo)(bar) is that it looks weird, I wonder if we'd just get used to it
That's pretty much it, yeah
Oh, we'd get used to it
like the semantics feel straightforward, it has the nice autocomplete experience
My worry is that newcomers would find it "icky"
"Why can't I just put the function name"
it might look funny to beginners but I don't think it would come up that often - like I went through all of rocci bird and it only came up once or twice that I wanted it
all the others became either methods or direct calls
(e.g. bar(foo, baz))
So to beat a horse to death: if we can't find a syntactically ambiguous version of .. between list/record spreading and function calls, that's still probably better for cleanliness and newcomers in my mind.
And I know it breaks the 3 char operator rule, but this problem goes away if we use what basically every other language uses for spreads, which is ...
I'd say that single . has multiple meanings as well, we're just used to them:
3.2 for floats.1 for tuple accessorsx.y for field accessModule.function for module lookupSo having .. mean "spread" and "local function call" is not so bad
I don't remember what language already uses .. for function calls almost exactly like this (maybe Agus does), but there's precedent for this
Let me look through the PL subreddit
I think you mean Dart, but that’s a little different
(deleted)
https://dart.dev/language/operators#cascade-notation
Yep, that's what I tried to link
It's different, yep
So not precedent
I think it might be valuable to support foo.(bar)(baz), but I don't imagine that's almost ever good to write with a complex expression for bar
foo.(if x then bar else baz)(123) screams code golf to me
same here at first glance, although I suppose I could imagine in a vertical pipeline maybe it could make sense legitimately in some circumstance?
very rarely if at all though
shrug
I keep thinking back to that example in the doc:
[-2, 0, 2].map(.abs().sub(1))
This is all ways to "call a chain of functions"
I think foo..bar(123) does that, I think foo.(bar)(123) does that with a slight hiccup
So either of these is still an improvement to Roc for sure, but I'd rather find a solution that visually implies "simple chain of function calls"
I think foo|bar(123), foo..bar(123), foo.>bar(123) all do that
I think I've made my point, I'm going to go to bed
yeah I dunno, to me both foo..bar(123) and foo.(bar)(123) look strange, just in different ways
neither feels like it's obviously the way to go
Still definitely an exciting change, any of these make me confident I could go up to someone that's only written code a couple times and get them to understand how things work
I think I just don't like parentheses
haha
Richard Feldman said:
neither feels like it's obviously the way to go
Totally agree
foo.>bar(123) looks a bit more intuitive to me
I like foo.>bar(123)
We could also do foo.|bar(123) which evokes the unix pipe and still has autocomplete on .
.> seems optimal for communicating "now pipe without interrupting the method chain", if that's our intent
Less notably, it kind of evokes a terminal prompt, which is appropriate for finding functions in the current namespace.
We also have a sign from the universe: on a US keyboard layout, > is shift + .
definitely an option, but I'd like to keep exploring....> still looks weird to me haha
it visually reminds me of the kind of custom operator I'd see in like a Haskell or Scala DSL or alternative stdlib :sweat_smile:
It's not vertically balanced
I wonder about like .= maybe
like foo.=bar(baz)
eh that feels like assignment
@ and $ are free, single char symbols
And neither mean anything in Roc
. auto-completes methods, $ does functions
Is the plan to keep |> with the nearly identical meaning?
Otherwise, how is existing whitespace syntax preserved as an equal alternative?
I would vote for removing the whitespace syntax
Because it's better to have one way to do things
The exception would be if we could start a whitespace block, aka
component = props -> do:
use whitespace syntax
Any way to denote "this is all whitespace, and it's on purpose and special"
Richard Feldman said:
the problem with
|>is: what if you want to keep calling methods afterwards?
@JanCVanB this is why pipelines can't stay in their current form
@Agus Zubiaga message received lmao
let's hold off on discussing "whitespace calling vs commas and parens" until later
I agree we shouldn't do it
certainly there will be a world where both coexist, for backwards compatibility, then once we're in that world we can experiment with having the formatter enforce one rule or the other and see how things look
then have a more informed discussion with that info
I really feel like trying to chain local functions or imported functions in some syntax that is shoved into dot based calling is a mistake. I think it eats into the weirdness budget and is harder to reader.
Either you define a method on a nominal type which is callable via x.method(...) or you define a function which will require calling with function(x,...) or `x |> function(...)
Other languages have perfectly fine syntax without needing something super strange like x.(function(...)).
This feels like we are prematurely trying to solve a problem that doesn't exist.
That or we accidentally added a problem into roc that somehow isn't a problem in other languages
interesting, I hadn't considered that possibility!
yeah my general assumption was like "yeah of course we'll want that" but admittedly I've never wanted it in Rust or JavaScript
definitely the easiest design to try out is "hold off on implementing it and see how much demand there is in practice first"
I disagree. I love that |> allows me to conveniently pipe things I don’t control into my own functions. That’s something I really miss in mainstream languages.
Some languages tackle this problem by allowing you to extend classes from the outside, but I don’t love that
Oh, for sure keep |>. It is great for removing nested parens and making code more readable
Just don't try to jam an extra variant of pipe into the dot calling syntax. Use pipe. It already exists
Yeah, but in a world where most things in the stdlib are nominal types with methods, |> won’t compose nicely
I think the better statement is that pipe generally isn't needed. Which is why I think for most mainstream languages it isn't added. A lot less pressure when dot syntax works for almost wverything
There is nothing wrong with
y = x.method(...) |> function(...)
(where function is a method and method is a local function?) heehee edited
I get it isn't perfect (cause occasionally you will need a nested lambda to deal with things), but it is still better than any mainstream language. I think it solves most of the need. You also could allow defining local methods if you really wanted. They would just need unique names compared to the imported list of methods for a nominal type (though this can get hairy).
@Brendan Hansknecht I agree, the only cost is that this elision of types that comes from the method paradigm is lost once you pipe to other functions. It seems like .> allows that, which is why we're trying to find a nice looking version of it.
But I agree that it's minor
Arguably breaking up the method dot chain with a pipe operator is a good thing, because I imagine it will usually correlate with a switch from one type to another. For example, |> Str.fromUtf8 is where the Str party starts.
I'm not sure I follow. How does this change anything about the types?
I think Jan is working under the assumption that most methods act like builder methods, where they take and return the same custom type
But if that's not the case, then the types change a good deal
Oh also, with pipe, this should be valid right?
list.method1().method2(123) |> localfn().method3()
I think Brendan and I were both replying to a few messages prior heh
Brendan Hansknecht said:
Oh also, with pipe, this should be valid right?
list.method1().method2(123) |> localfn().method3()
This implies that you're calling a method on the return value of localfn, since whitespace binds weaker than .
It's clearer when you put newlines in this chain, but we shouldn't treat newlines special here IMO
If we use .> sans spaces instead of current |> the problem disappears
I think that should desugar to
localfn(list.method1().method2(123)).method3()
That's what it would need to desugar to, yes
So why do we need .>?
They would just desugar to the same thing.
To adapt Richard's last examples:
pipes = prev.pipes.(Pipes.update("extra argument")).appendIfOk(pipe)
pipes = prev.pipes.(updatePipes("extra argument")).appendIfOk(pipe)
vs.
pipes = prev.pipes.(updatePipes)("extra argument").appendIfOk(pipe)
pipes = prev.pipes.(Pipes.update)("extra argument").appendIfOk(pipe)
both seem fine as
pipes = prev.pipes |> updatePipes("extra argument").appendIfOk(pipe)
pipes = prev.pipes |> Pipes.update("extra argument").appendIfOk(pipe)
though now I think this prevailing "local function" example looks more like it would actually be a method in this particular case
pipes = prev.pipes.update("extra argument").appendIfOk(pipe)
so maybe we need a new canonical syntax bikeshedding example :stuck_out_tongue_wink:
Brendan Hansknecht said:
They would just desugar to the same thing.
The whitespace is the problem. Though I'd be okay with this, it's still a misleading visual grouping
@JanCVanB sounds like you're on the same page as Brendan?
Sam Mohr said:
This implies that you're calling a method on the return value of localfn, since whitespace binds weaker than .
Wait, isn't that the goal?
Oh, so no type problem. I guess I am still confused by your comment on "the only cost is that the Ellison of types that comes from the method paradigm is lost on e you pipe to another function".
I don't understand how anything changes from .> to |> that would affect types.
Sam Mohr said:
JanCVanB sounds like you're on the same page as Brendan?
I think so, but I'm getting confused too
pipes = prev.pipes.(Pipes.update)("extra argument").appendIfOk(pipe)
pipes = prev.pipes.(updatePipes)("extra argument").appendIfOk(pipe)
I guess this could be expressed using pipe and parens:
pipes = (prev.pipes |> Pipes.update("extra argument")).appendIfOk(pipe)
pipes = (prev.pipes |> updatePipes("extra argument")).appendIfOk(pipe)
but the parens would mess up a multiline version of this
Why are the outer parens necessary?
Brendan Hansknecht said:
Oh, so no type problem. I guess I am still confused by your comment on "the only cost is that the Ellison of types that comes from the method paradigm is lost on e you pipe to another function".
I don't understand how anything changes from
.>to|>that would affect types.
It doesn't. I'm saying that the |> function().method3() example looks like it's calling method3 on the function result, so it is currently disallowed and you'd only be able to call functions once the |> operator is used in a pipeline to mitigate that confusion
...but it is calling method3 on the function result, right?
But since .> is a dot operator, it's not confusing so we'd allow method calls to follow function calls
However, if you remove that assumed-necessary restriction, they're the exact same thing
Oh (x.method() |> func()).method() vs x.method() |> (func().method())
The second version where func has no arms.
Exactly
.> is unambiguous, |> is ambiguous
It isn't ambiguous. It is a precedence question
Sure
I kinda assumed . is basically a form of |>. So same precedence
I think we'd be keeping |> because it's familiar at that point, even though it looks further from most of our function call syntax
I'd prefer something that looks similar to .
.| or .. or .> do that
If this is just about making whitespace visually match precedence, then don't we already allow the same issue with 1 |> add(2)/3? (lame example, can't think of a good one, maybe that means I'm wrong hehe)
I don't think that's allowed?
That would be (add(1, 2))/3
What do languages with universal method calling syntax do to deal with these issues? They theoretically support calling any function as a method with dot syntax.
They don't support this
We're trying to do things with the same syntax: call local functions and call methods
UFCS picks one of those to my understanding
Sam Mohr said:
That would be (add(1, 2))/3
just like x.m1() |> f().m2() would be f(x.m1()).m2() as we want it to be
This isn't a thing in other languages
@JanCVanB the precedence is different for math operators, but sure
Example from d that I think would definitely hit this issue. So I think they need to have a solution to the problem:
int first(int[] arr)
{
return arr[0];
}
int[] addone(int[] arr)
{
int[] result;
foreach (value; arr) {
result ~= value + 1;
}
return result;
}
void main()
{
auto a = [0, 1, 2, 3];
// all the following are correct and equivalent
int b = first(a);
int c = a.first;
// chaining
int[] e = a.addone().addone();
}
Oh nvm... I already know D's solution and we won't like it
All functions in one global name space...no modules (though I should double check this)
(going afk for activities, :heart: byeee)
Oh, actually, D has proper modules. So maybe they have a real solution to this.
I don't think we can reasonably define |> to have the same precedence as . - that would be really confusing :sweat_smile:
foo.bar() + baz.blah()
foo.bar() |> baz.blah()
I think if these two expressions have difference precedence on the operator in the middle, that's really surprising
er, precedence with respect to .blah()
like, this is really surprising:
foo.bar() + baz.blah() == (foo.bar()) + (baz.blah())
foo.bar() |> baz.blah() == (foo.bar() |> baz).blah()
I don't think anyone would expect that to be how it worked :big_smile:
So for D to allow local functions to use dot syntax. They basically have a precedence rule for which function to pick. If a class defines a method direct (in roc's case, this would be if it was defined and exposed from the same module as the nominal type), it is picked first. After that, a local function can be used with the method call syntax.
...
I feel like that would not be nice to work with....
Yeah, that’s why having a separate operator makes sense to me
and I don’t think we need to keep |> if we have .>
I wonder if just > could work: if > is followed directly by an ident, it's a function call, otherwise it's the comparison operator
ok so to summarize some of the options:
pipes = prev.pipes.(updatePipes)("extra argument").appendIfOk(pipe)
pipes = prev.pipes.(Pipes.update)("extra argument").appendIfOk(pipe)
pipes =
prev
.pipes
.(updatePipes)("extra argument")
.appendIfOk(pipe)
pipes =
prev
.pipes
.(Pipes.update)("extra argument")
.appendIfOk(pipe)
pipes = prev.pipes..updatePipes("extra argument").appendIfOk(pipe)
pipes = prev.pipes..Pipes.update("extra argument").appendIfOk(pipe)
pipes =
prev
.pipes
..updatePipes("extra argument")
.appendIfOk(pipe)
pipes =
prev
.pipes
..Pipes.update("extra argument")
.appendIfOk(pipe)
pipes = prev.pipes.>updatePipes("extra argument").appendIfOk(pipe)
pipes = prev.pipes.>Pipes.update("extra argument").appendIfOk(pipe)
pipes =
prev
.pipes
.>updatePipes("extra argument")
.appendIfOk(pipe)
pipes =
prev
.pipes
.>Pipes.update("extra argument")
.appendIfOk(pipe)
I vote dot parens. It feels like you are modifying the lambda to make it work in a dot chain.
Dot dot looks like a bug/typo
I vote dot dot. It looks the nicest to me :point_right::point_left:
x.foois still a record field access; I can tell x is a record just from looking at this.
x.foo(y)is now a statically-dispatched method call, which I can also tell just from looking at it.
What is x.foo is a field of function type? How would I be able to call it?
that's explained in the next paragraph or so
(x.foo)(bar)
I object that this sample from Rocci Bird is the canonical example of needing to pipe in the middle of the method chain. It overemphasizes that piping should feel method-y because updateThings/Things.update takes a List Thing as its first argument and that feels a lot like taking Thing as its first argument. I imagine that most mid-chain piping will be to change types entirely.
also lol I'm back my first activity canceled
You can change types without using pipe
myStr.toUtf8().somlistMethod()
yeah this is definitely 100% unrelated to types :big_smile:
nothing about this discussion changes no matter what the return types and/or argument types in question are
okie dokie nvm :)
Richard Feldman said:
that's explained in the next paragraph or so ...
(x.foo)(bar)
I want to resist extra parens in Roc, yet I'm warming up to .(up) over .>up/..up, partly because it would be consistent with this (x.foo) syntax - identify a function by putting parens around its qualified name. It also feels the most like "hey Roc please treat this function like a method" before any explanation is provided.
yeah I still don't love the way it looks, but I do like the explanation for .(foo)(bar) the most
it's like, it's the same thing as .foo(bar) except you're substituting an arbitrary expression for the name foo
it sort of "feels right" but doesn't "look right" :stuck_out_tongue:
to me
Maybe it will feel right after we introduce the many parens this proposal requires anyways!
in comparison to .foo(bar) vs ..foo(bar) or .>foo(bar) where foo means something completely different in the first example vs the other two, but it's still kinda in the same position if that makes sense
but also I don't really love how any of them look :sweat_smile:
I think that's what makes me gravitate towards dot-parens - I'm not thrilled with how any of them look, but that one stands out for feeling like it has the best explanation for what's happening
if we found something that just looked awesome, that would be a different story though
Parens are like JSON - simultaneously an eyesore and hyper readable
I think syntax highlighting makes a big difference...when punctuation like dots and parens are kind of a low-contrast gray, they fade into the background more easily (while still being visible as delimiters if you're looking for them to figure out where things start and end)
Screenshot 2024-11-09 at 12.32.03 PM.png
Side benefit of .(up) - while it technically breaks the vertical alignment of the method chain by inserting an extra character like all options above... it doesn't FEEL like it is, because we know from other languages to visually anchor on the left paren as the start of the phrase.
Visualized:
pipes =
prev
.pipes
.>Pipes.update("extra argument")
# ^-- start of the phrase
..Pipes.update("extra argument")
# ^-- start of the phrase
.(Pipes.update)("extra argument")
# ^-- start of the phrase
.appendIfOk(pipe)
I also expect that, just like in this code snippet, every editor will color parens differently than ./>, which is good here to preserve vertical alignment. (Maybe not always the same color as text like here, but still different than operators.)
Why doesn’t .> feel right but |> currently does? As it was pointed out before, it’s basically the same thing with different precedence
Sure you might have to look it up to understand, but I don’t think most people would get the parens syntax immediately either
Agus Zubiaga said:
Why doesn’t
.>feel right but|>currently does? As it was pointed out before, it’s basically the same thing with different precedence
trying to unpack my own instinctual impression, I think it's because when I think about foo.bar, foo.>bar, and foo |> bar, in the first two cases the dot makes me think "this is looking something up on foo named bar" because that's what it means when there's a dot immediately following something
and I think the reason foo.(bar) doesn't make me think that (as much) is that when something is surrounded in parens, that pretty much always means "this is its own self-contained thing"
so I think (bar) in an expression refers to the expression bar
(which is correct in this case)
all that said, I think I could get used to any of these
No matter what option we go with, there will be situations where it will feel more/less obvious to more/less experienced users. The canonical example we're using now anchors us to think of its use in the context of a longer chain with methods before and after, but what if the first ten experiences someone has with it are just foo.>bar(baz) by itself? We can say "well why wouldn't they just write it like bar(foo, baz)", but we're introducing the capability, and piping is stylistically enticing in general.
Possible horrors:
(a, b) = c((d, e)).(f)(g).h(i.(j))
a = [b..c(d.e(f..g)...h.i] # assuming we rename .. to ...
a = \b -> b.>c.>d > b.>(\e -> e.>f)
(sorry for editing this indefinitely, it's just too much fun)
In other words, I think no matter the syntax, introducing a . version of |> will enable both more elegant code and more horrific code. It would be up to us to shield new users from pain.
the middle one there looks the most horrifying to me haha
First line is definitely most readable to me of those. Though it is a bit of apples and oranges all with the worst syntax
For example, it's common in many languages to have a function like Set.add.
(We happen to call it Set.insert, but Set.add was considered too.) If that's
the best name for your function, is it okay that + now Just Works on your type,
even if that usage doesn't make sense? It's a valid question. Also, the + operator
in arithmetic is commutative, but nothing says a.add(b) has to be
commutative; b doesn't even have to be the same type as a!
What about a new operator ++ that desugars to append?
this might end up being a situation where we try out one thing and then reconsider later. fortunately, the semantics wouldn't be affected
.> is my favorite by a large margin
Derin Eryilmaz said:
For example, it's common in many languages to have a function like Set.add.
(We happen to call it Set.insert, but Set.add was considered too.) If that's
the best name for your function, is it okay that + now Just Works on your type,
even if that usage doesn't make sense? It's a valid question. Also, the + operator
in arithmetic is commutative, but nothing says a.add(b) has to be
commutative; b doesn't even have to be the same type as a!What about a new operator
++that desugars toappend?
that's interesting! I thought originally it didn't seem worth it, but in the context of operator overloading the tradeoffs are different - e.g. having ++ would discourage overloading + for that operation
I only just now realized that .>foo(bar) isn't "piping" any more than .foo(bar) is - it's just a question of namespaces... Both are new pipe-like operators. That makes it feel weird to give one of them the > from |> but not the other one. My .(foo)(bar) preference is getting stronger.
.>bar and .(bar) would do the same thing as |> bar today, just with different precedence
Richard Feldman said:
.>barand.(bar)would do the same thing as|> bartoday, just with different precedence
Yes, but critically: .bar would do the same thing too (the function just happens to live somewhere special). In other words, . alone is now a pipe.
we should consider the rest of the ._ family besides .>, e.g..:, .$, .<, etc
like maybe it's the best of that bunch, but I'd like to see how the others look in examples to compare
(I'm on mobile now, so hard for me to do!)
.! would look hilarious with effectful functions - foo.!bar!(baz)
i like .$ in line with "$(str)" interpolation
you're inserting the function
JanCVanB said:
Yes, but critically:
.barwould do the same thing too (the function just happens to live somewhere special). In other words,.alone is now a pipe.
Now that I've realized this, I strongly oppose introducing .> because it would communicate to learners that .> is doing more |> than . is.
Derin Eryilmaz said:
you're inserting the function
instead of inserting a string into another string, you're inserting a function into the call chain
makes sense to me
Richard Feldman said:
dot-greater-than
pipes = prev.pipes.>updatePipes("extra argument").appendIfOk(pipe)pipes = prev.pipes.>Pipes.update("extra argument").appendIfOk(pipe)pipes = prev .pipes .>updatePipes("extra argument") .appendIfOk(pipe)pipes = prev .pipes .>Pipes.update("extra argument") .appendIfOk(pipe)
can we get some more examples like this with other operator styles? e.g. .$
(from anyone who is at a keyboard right now :big_smile:)
sure
huh somehow my formatting isnt working
Note: Richard's been using python syntax highlighting
'''python
code
'''
I used perl above
just bc it highlighted .> as one operator
Maybe the ## is interfering?
what..
Not attacking Derin - Will this topic break any records for length in this Zulip? :laughing:
not yet haha
pretty sure we've already had longer ones
Derin, want me to tag in? (assuming I have any better luck than you)
can someone else please try writing it out? sorry my formatting isnt working and idk why
Syntax discussions are the real Zulip stress test
Derin Eryilmaz said:
i like .$ in line with "$(str)" interpolation
I guess the most direct translation of that would be foo.$(bar)(baz)
that one might be the absolute easiest to explain haha
pipes = prev.pipes.$updatePipes("extra argument").appendIfOk(pipe)
pipes = prev.pipes.$Pipes.update("extra argument").appendIfOk(pipe)
pipes =
prev
.pipes
.$updatePipes("extra argument")
.appendIfOk(pipe)
pipes =
prev
.pipes
.$Pipes.update("extra argument")
.appendIfOk(pipe)
I'm not endorsing this (yet?), just being our hands.
pipes = prev.pipes.$(updatePipes)("extra argument").appendIfOk(pipe)
pipes = prev.pipes.$(Pipes.update)("extra argument").appendIfOk(pipe)
pipes =
prev
.pipes
.$(updatePipes)("extra argument")
.appendIfOk(pipe)
pipes =
prev
.pipes
.$(Pipes.update)("extra argument")
.appendIfOk(pipe)
adding what richard suggested
AYO, Derin in the formatting house
pipes = prev.pipes.:updatePipes("extra argument").appendIfOk(pipe)
pipes = prev.pipes.:Pipes.update("extra argument").appendIfOk(pipe)
pipes =
prev
.pipes
.:updatePipes("extra argument")
.appendIfOk(pipe)
pipes =
prev
.pipes
.:Pipes.update("extra argument")
.appendIfOk(pipe)
Derin Eryilmaz said:
adding what richard suggested
not necessarily a suggestion, just a thought :big_smile:
Fair enough, it is my favorite so far though because of its similarity to string interpolation
pipes = prev.pipes.<updatePipes("extra argument").appendIfOk(pipe)
pipes = prev.pipes.<Pipes.update("extra argument").appendIfOk(pipe)
pipes =
prev
.pipes
.<updatePipes("extra argument")
.appendIfOk(pipe)
pipes =
prev
.pipes
.<Pipes.update("extra argument")
.appendIfOk(pipe)
imma say right now, ew
that one at least has more vertical smoothness than .>
pipes = prev.pipes.|updatePipes("extra argument").appendIfOk(pipe)
pipes = prev.pipes.|Pipes.update("extra argument").appendIfOk(pipe)
pipes =
prev
.pipes
.|updatePipes("extra argument")
.appendIfOk(pipe)
pipes =
prev
.pipes
.|Pipes.update("extra argument")
.appendIfOk(pipe)
(less ew but I still object for the same reason I objected to .>)
My clipboard is ready, if anyone wants to shout ascii characters...
Honestly... .$ is really smooth. It makes it clear the distinction is namespacing, not piping. And it leverages an association used in both Roc and other langs.
.> is still my favorite but I would be happy with any ._ variety
Here's the previously mentioned "horrifying syntax", tried with $()
(a, b) = c((d, e)).$(f)(g).h(i.$(j))
but without parens:
(a, b) = c((d, e)).$f(g).h(i.$j)
if we went with that one, we could say the parens after the $ are optional
so explain it exactly like string interpolation, but explain that the parens are optional unlike in string interpolation
$ looks like it’s part of the name to me, but maybe I’m just scarred from jQuery and PHP
yeah I feel that way a little bit too, although in those cases there's never a . before the $
Optional parens could help with module qualification looking more natural sometimes
foo.$Bar.baz(a).b.c vs foo.$(Bar.baz)(a).b.c
I think the parenthesis should be necessary at least if you're going to reference a moduled function like foo.$(Thing.test)
otherwise foo.$test should be fine
jinx
fwiw I think module qualification would be a rare use of this
because aside from .fromFoo functions, modules rarely take another type as their first arg?
yeah
I could see it in like test helpers maybe
but locally defined functions would be the most common by far, I'd expect
Yeah, and the optional parens will be great for lambdas, though hopefully many most lambdas will be rendered moot by point-free fun.
pipes = prev.pipes.$updatePipes("extra argument").appendIfOk(pipe)
pipes = prev.pipes.$Pipes.update("extra argument").appendIfOk(pipe)
pipes =
prev
.pipes
.$updatePipes("extra argument")
.appendIfOk(pipe)
pipes =
prev
.pipes
.$Pipes.update("extra argument")
.appendIfOk(pipe)
an interesting observation about this is that the fact that various languages treat $foo as a variable makes this maybe look more approachable to beginners
like they might incorrectly guess what it's doing, but their first impression might be more "this looks familiar" than "this looks alien"
looking back at each of the examples, .$ now looks the least strange to me. it's my new frontrunner
In the expression
!foo.bar("hi"), the type of foo would be inferred as:
a where a.bar(Str) -> BoolYou can read this as "some value (whose type has been named a) that has a method named
foowhich returns a Bool and takes a Str as its second argument." (The first argument in a method is always the value it's being dispatched on.)
Should this be "has a method named bar" ?
.$ looks a lot less intuitive to me, but at least it’s concise, so I’ll take it :grinning:
Is my proposal still in the mix? I had the sense it had the same benefits as the original proposal but without introducing new syntax weirdness, but curious if I overlooked something.
To reiterate, I propose replacing |> with ., but otherwise not changing what the operator does. The benefit here would purely be easy-to-typeness. That would look like this:
pipes =
prev.pipes
. Pipes.update "extra argument"
. List:appendIfOk
Module/exposed separators would become something else, could be : as in above, .:, or something else, to avoid ambiguity with the new . operator.
Then optionally the module qualifier can be elided to let the compiler use the same qualifier as the one of the previous method in the chain, so you can also write this:
pipes =
prev.pipes
. Pipes.update "extra argument"
. _:appendIfOk
That would imply there's two different concepts to learn, one is ./|> which works exactly as it does today, the other is qualifier-elision using _ (or some other operator). My sense is that smooshing the two together creates a difference between method and function calls, while the above avoids that by just having function calls but keeping all the autocomplete benefits.
hm, I think if we started with parens-and-commas and then proposed that syntax, we'd probably conclude it wasn't worth the strangeness budget cost :big_smile:
but I'm curious what others think!
it just occurred to me that this is an option:
.callpipes = prev.pipes.call(updatePipes, "extra argument").appendIfOk(pipe)
pipes = prev.pipes.call(Pipes.update, "extra argument").appendIfOk(pipe)
pipes =
prev
.pipes
.call(updatePipes, "extra argument")
.appendIfOk(pipe)
pipes =
prev
.pipes
.call(Pipes.update, "extra argument")
.appendIfOk(pipe)
Would it have to defined manually or does every type get it automatically?
I'm not sure if .call is the right name, but the idea would be that this is a reserved method name which the type-checker is aware of, and would apply constraints the way you'd expect (like a normal direct call)
I think you’d need different names for the different number of args
yeah every type would get it automatically, just like Inspect
nah that could Just Work
Ah, so is kinda how crash isn’t really a function but looks like one
right
it looks like a method but it's actually just a special case in the compiler
edit: specifically it would be treated by the compiler as syntax sugar for a normal function call, just like |> is today, so it would have been desugared before type-checking even began, and it wouldn't add any extra type constraints to anything compared to a direct call
Would it work for structural types too?
might read better as .passTo
.passTopipes = prev.pipes.passTo(updatePipes, "extra argument").appendIfOk(pipe)
pipes = prev.pipes.passTo(Pipes.update, "extra argument").appendIfOk(pipe)
pipes =
prev
.pipes
.passTo(updatePipes, "extra argument")
.appendIfOk(pipe)
pipes =
prev
.pipes
.passTo(Pipes.update, "extra argument")
.appendIfOk(pipe)
Or .pipe?
maybe, although I think we need a different example to see how that one reads
otherwise it's like pipes.pipe(updatePipes, ... :laughing:
what I like about this general idea is that it looks totally normal
I don't think any beginner would look at prev.pipes.call(updatePipes, "extra argument") and be like "what is that doing?!?!?!"
Yeah, that’s a good point
and it wouldn’t be the first time we do something like that
I do like that .passTo precisely describes what it's doing, although I wish there were a single word that was as self-descriptive :big_smile:
.call isn't quite as accurately self-descriptive because you're not calling the subject itself
.apply?
.pipe feels like it needs more explanation than .passTo
yeah same with .apply
.forward? idk
so far of all the options we've discussed in this thread, I like .passTo the best because:
it's less concise than the operators but I'm fine with that given the other tradeoffs involved
I don't know about all this--I really like the fact that roc has a lot of whitespace. A . isn't strictly necessary for autocomplete to exist. I also feel like we shouldn't be making this syntax easier/more visually pleasing than explicit module qualification for every function call, because that encourages importing tons of functions, which could be pretty bad. And this also makes it hard to decipher types at times (if you're reading code without an IDE).
That said, I think the power of this static dispatch idea is allowing a nicer form of abilities, while also allowing limited operator overloading.
I like what Jasper proposed earlier. here's my (similar) syntax idea:
fn = \header ->
when headers | _keepIf \{ name } -> name == "cookie" is
[reqHeader] ->
reqHeader.value
| Str.fromUtf8
| _try \s -> s | _split "=" | _get 1
| _try Str.toI64
| _mapErr \_ -> BadCookie
_ -> Err NoSessionCookie
I just put a singular pipe char as an operator; that part of it could change.
I'd have the same thought with both designs, which is "if we were already using mainstream calling syntax, would it be worth paying the strangeness budget to switch to something totally different?"
but I appreciate that different people may assign more or less value to the aesthetic delta there, based on personal preference :big_smile:
Richard Feldman said:
I do like that
.passToprecisely describes what it's doing, although I wish there were a single word that was as self-descriptive :big_smile:
The first thing that came to mind for me was .next(..)
Richard Feldman said:
I'd have the same thought with both designs, which is "if we were already using mainstream calling syntax, would it be worth paying the strangeness budget to switch to something totally different?"
If we decide not to switch to the dot syntax with parentheses and commas, I think adding an underscore before function names that should be statically dispatched is probably the best approach to implement this:
foo |> _fn bar
foo |> _.fn bar is logical too but the extra dot is just a bit too clunky imo.
I'd have the same thought with both designs, which is "if we were already using mainstream calling syntax, would it be worth paying the strangeness budget to switch to something totally different?"
The aesthetic delta is definitely part of it. I do believe it would probably help adoption if the syntax were more C-like, though it hurts a bit to say it :sweat_smile: . That said, wouldn't that argument apply just as easily to many other things, like wrapping blocks in {}?
But it's not just the aesthetics for me:
Jasper Woudenberg said:
I like the default-explicit prefixes. I wonder if omitting module names by default will result in function names getting longer, to add extra context. Also, I use Vim and don't have type hints set up currently. Github PR diffs don't either. New programmers might not necessarily have all the best tooling set up either.
I also like them, although there is a tradeoff here - someone (I think @Aurélien Geron if I remember right?) mentioned that a current downside of Roc compared to (for example) Python is that when you're doing things like quick scripts and exploratory programming (e.g. data science, where you're doing relatively more writing code compared to reading it later), which are domains where conciseness is disproportionately valuable, it's an advantage to Python that you have the ability to write these things more concisely
Don't you get the write-speed improvements with auto-complete though?
Jasper Woudenberg said:
The proposal introduces a difference between method and function calls. I love that in Roc currently and Elm there is no such difference. I think it's also easier to explain one calling convention over two.
in fairness, I think this is actually more about aesthetics than it might appear.
today we have direct function calls and ability calls, which use type information to look up which specific function implementation to use. In the proposal, we have the same split, just with "methods" instead of "abilities" being the ones that use type information to look up which specific function implementation to use :big_smile:
Jasper Woudenberg said:
Don't you get the write-speed improvements with auto-complete though?
to some extent, although you'd need an autocomplete that would automatically fill in the module prefix I suppose (so that you could just type the name of the function, unqualified, like you can in the method-style design)
i would support the autocomplete doing that. i also think that, if we take that path, the dot syntax wouldn't look great
also, another advantage of using foo |> _fn bar syntax:
_fn foo bar means the same thing, so you wouldn't be forced to write the function after the argument if using abilities
something worth noting is that I have a lot of affection and nostalgia for whitespace calling, although I remember something Evan once said about Elm - "I didn't make this language to fight the Syntax Wars."
To him, that means having a syntax that's in the tradition of the ML family of languages that Elm is descended from, and not spending a lot of time deviating from that. But the same "not fighting the Syntax Wars" sentiment resonates with me in a different way - in that it does feel like the syntax being so different from what people are used to is holding back the other great ideas in the language from reaching a wider audience, and that creates tension in my mind with the feelings of affection and nostalgia for the current syntax
which is not to say that I want to (for example) just make the syntax be a subset of C or Java or something, but rather acknowledging that some syntax changes are more or less discouraging to beginners than others (much as I wish it weren't the case), and part of being a beginner-friendly language is having a syntax that beginners find approachable
for example, I don't think anyone looks at if/then/else and quietly closes the tab
but whitespace calling does make the code look a lot different, and I know that's a barrier for people
and that shouldn't be the only consideration, of course, but I do think it should be a consideration, and it deserves thought and weight alongside the pros and cons of other syntax considerations (e.g. the whitespace calling looking better in DSLs, as noted in the doc)
Richard Feldman said:
which is not to say that I want to (for example) just make the syntax be a subset of C or Java or something, but rather acknowledging that some syntax changes are more or less discouraging to beginners than others (much as I wish it weren't the case), and part of being a beginner-friendly language is having a syntax that beginners find approachable
i guess you're using "beginner" here to refer to someone who's already used java or c--what if beginner means someone who's never coded before? I would argue whitespace syntax could be nicer to a lot of those people: it's far less overwhelming than a barrage of dots and parentheses :grinning_face_with_smiling_eyes:
yeah, sorry - beginner in the sense of coming to Roc for the first time
some percentage of beginners trying Roc for the first time will never have used any programming language, but I'd guess it would be less than 1% :big_smile:
hey, i do think it would actually be a good first language--if only more people knew about it :grinning:
it would be cool if someday that were different, but realistically the path to being that mainstream would involve first getting widespread adoption through the path of lots of beginners coming to the language with a background in other languages
@Richard Feldman I have a serious but also :laughing: question for you: How much of this proposal and focus on autocomplete is influenced by your new (to me) role at an IDE company?
I've been here since when the editor was still a more active effort, so I know that Roc has always had UI in mind - I just want us to do a quick due diligence on conflict of interest. Please place your hand on the Tufte:
oh almost none
we've talked about autocomplete for years
also, one of the reasons I applied to Zed in the first place is wanting to learn about editors, and also how a really good (and open-source!) one works
The ML syntax adding the the weirdness budget and that pushing some people away I get. It's a shame, but yeah, maybe not worth the fight.
Do you see removing default-qualifiers and replacing pizza-style function chaining with method chaining in a similar vein, ways to get to syntax more familiar to folks new to the language? Wondering if workshopping alternative syntax that makes pizza-chaining and module qualifier (elision) look more like mainstream languages would change things for you, or if those changes are less about the syntax for you.
Richard Feldman said:
Jasper Woudenberg said:
The proposal introduces a difference between method and function calls. I love that in Roc currently and Elm there is no such difference. I think it's also easier to explain one calling convention over two.
in fairness, I think this is actually more about aesthetics than it might appear.
today we have direct function calls and ability calls, which use type information to look up which specific function implementation to use. In the proposal, we have the same split, just with "methods" instead of "abilities" being the ones that use type information to look up which specific function implementation to use :big_smile:
Most of our ability calls are wrapped up. You use an operator like == and it calls eq for you behind the scenes but it's not something you're thinking a lot about. Similar with running decoders via Decode.fromBytesPartial. So I agree with the principle, but think in practice you can get by today without knowing the details of the calling conventions that exist, while with this proposal I think you'd need to actively understand these two calling syntaxes to be able to work with a codebase.
good point about abilities!
Jasper Woudenberg said:
The ML syntax adding the the weirdness budget and that pushing some people away I get. It's a shame, but yeah, maybe not worth the fight.
Do you see removing default-qualifiers and replacing pizza-style function chaining with method chaining in a similar vein, ways to get to syntax more familiar to folks new to the language?
yeah, that's why I started the doc out with the example of how the string replacing looks in mainstream languages
to me, though, it makes a really big difference what the semantics of those calls are
e.g. if it's an inheritance hierarchy vs. "we just call the function in the module where the subject was defined"
Gotcha, that makes sense. How important is it that calling the function in the module where the subject was defined is the 'favored syntax', so to speak? Compared with say local module top-level functions or let bindings?
I don't think it's necessarily crucial, but it does seem like in practice that's what tends to get used the most
like how for example in Elm and Roc code bases it feels to me like most |> calls are qualified by module
as opposed to local
so, from what i can tell, the tradeoffs are:
|>)? syntax (you could argue whether that's better or worse than try)but:
|> gets phased outI think saying "slightly better auto-complete" is underrating the benefit
Gonna think a bit on this. I definitely better appreciate the syntax in the proposal. Sorry for covering some ground I think the doc covered, some if it I don't think fully clicked before.
With people that don't know the API they're working with, method calls don't require knowing where to import the qualifying type from
You just do thing.otherthing().next()
But if those are all in different modules, today you'd have to either:
With methods, you can "discover as you type"
- breaks a lot of old code if
|>gets phased out
I think we could upgrade that via the formatter
and maybe even upgrade function calls to method calls, but that'd require type information
I like the idea of using a word like call instead of a symbol as it's more explicit. But one think I don't like about .passTo is that it includes a capital letter.
And for the record, data.passTo(func, other, args) removes the problems from the dot parens syntax for me, so besides it being longer, I'm here for it because it is "shaped" like the other method calls
Though as Agus said:
Agus Zubiaga said:
.$looks a lot less intuitive to me, but at least it’s concise, so I’ll take it :grinning:
I prefer .$ if we're all onboard to the same thing
The benefit of .$ over .call/.passTo is that it's "a little weird"
I'm leaning towards .passTo (or whatever name) now
The main thing I don't like about .passTo is that beginners will probably think that methods can be variadic, but only .passTo will be able to do that
.$ looks like an operator, so it has the benefit of implying it's special
I feel like it wouldn't be fair not to ask our LLM friends. They might have words in the future if we never even consulted them about this problem.
Based on the extensive discussion, here are my key recommendations:
Keep the dot-method syntax (.method()) as proposed for static dispatch, as it provides good ergonomics, familiar syntax, and excellent autocomplete support.
For chaining local/unqualified functions, I recommend NOT trying to force it into the dot syntax. Instead:
(x.method1().method2() |> localFn).method3()
Rationale:
Example of recommended style:
result =
input.parse().validate()
|> transform
|> format
|> (.toString().trim())
This recommendation prioritizes:
The key insight is that trying to unify method calls and function application into a single chaining syntax may be more trouble than it's worth. The small ergonomic cost of occasional parentheses is outweighed by the benefits of having clear, separate syntaxes for these two different operations.
I believe this approach would serve Roc well - it maintains the benefits of the method syntax proposal while avoiding unnecessary complexity and confusion around local function chaining.
I hadn't thought of |>-ing into method call shorthand, that's interesting.
Side note, .$() could also accept the function as the first arg, like call/passTo.
Agus Zubiaga said:
The main thing I don't like about
.passTois that beginners will probably think that methods can be variadic, but only.passTowill be able to do that
I also briefly had that concern, but I think it would help to teach it as "hey this is just syntax sugar for a normal function call, it looks like a method call for stylistic reasons but it's not actually dispatching to anything at all"
just like |>
which is also unusual when it comes to arity, in that you have functions that look like they're being called with 1 fewer arg than normal, but actually it's coming from the |> etc.
but that hasn't felt like a significant problem in practice
yeah, I just think |> is a little different because it looks like an operator and you can't define those, so it's normal for it to be special
I don't think it's a big deal, though
Luke Boswell said:
I believe this approach would serve Roc well - it maintains the benefits of the method syntax proposal while avoiding unnecessary complexity and confusion around local function chaining.
I agree that this whole proposal seems worth implementing without any .$/.passTo initially, and then after a few months of lots of already-planned refactoring we can see if it feels wanted. A lot of us seem in agreement on moving 90% of the way in the same direction, but there are still valuable discussions being had about tradeoffs of the proposal's core. I wonder if .$ alone should split into a secondary proposal and topic.
F# for example, doesn't allow variadic functions (except in classes but that's for C# interop), and it still has printfn which is variadic and it even parses the format string at compile time
how is static dispatch going to work for functions that take non-opaque types as their first parameters? imagine in a module you have:
Person : { name: Str, age: U32 }
lastName : Person -> Str
then from another you call
person = {name: "J", age: 100}
last = person.lastName()
the compiler would have no way of knowing which module the type of person "belongs to," so I guess we couldn't do static dispatch in this case? like .lastName() would be an error?
They just wouldn't work, I think
(you'd get a compiler error telling you that you can only call methods on nominal types)
Luke Boswell said:
I like the idea of using a word like
callinstead of a symbol as it's more explicit. But one think I don't like about.passTois that it includes a capital letter.
if we went snake_case it could be .pass_to
Oh pleasssse let this be the day Roc goessss ssssnake casssse :pleading_face:
:100000:
haha I guess we've never really talked about it...I'm open to it, but I think that's a totally separate topic
if someone wants to start a topic in #ideas about it, seems fine to discuss!
didn't we? I'm pretty sure it was even in a proposal
It came up as a "if we did this, it would be compatible" in one of the purity inference docs
yeah, it was in purity inference v2: https://docs.google.com/document/d/1Nsg6I8Y27WAk7Aj6_fxKB17VGjQaCRMJ-nFfc0hJiwQ/edit?tab=t.0#heading=h.qfvf41di9dxa
I think the original ! syntax sugar proposal (or maybe it was Purity Inference) noted that functions ending in ! look nicer to me in snake_case
e.g. Ruby has that convention, Rust uses it for macros, and both of them are snake_case
anyway, happy to discuss in another thread if anyone wants to start one! :big_smile:
started: #ideas > snake_case instead of camelCase
omg #manifesting
Oh, also, we were discussing .passTo at some point while I was away. I'm not sure if it is still in the field of options. My first git feeling was that I really dislike it compared to parens or a symbol. Then I remembered that most languages I use every day just don't have a solution for this. So that would strictly be more flexible than what I am used to and it is very readable. I think I would prefer|> over it, but that doesn't always play nice with precedence.
I think it should be much nicer of the rust/c++ equivalent of needing to read in different directions
// I honestly think many people would reformat this in rust due to going between method and function and back
function1(a.method1().method2()).method3()
// This on the other hand would probably be left alone. (Formatter would just reflow it)
a
.method1()
.method2()
.pass_to(function1)
.method3()
So pass_to probably is a very readable and reasonable starting point. And it may also be a reasonable finishing point.
Is this similar to Extension Methods in C#? Back in the day when they introduced that feature, I thought of C# trying to be more functional without bringing the pipe operator :-D
https://learn.microsoft.com/en-us/dotnet/csharp/programming-guide/classes-and-structs/extension-methods
Hello, I'm not very active here and just lurking around until Roc stabilizes, but I really love the direction the language is taking.
I like the approach of using 'special' function call instead of separate syntax as it keeps mental model very simple. Other solutions make understanding code at a glance harder and while current pipe syntax ( |> ) is easy to understand for goblin like me (actually I like it very much) none of other solutions capture that elegance.
Using something like pass_to isn't as nice to look at, but doesn't break the mental model, I can just read the chain from left to right, from top to bottom without having to pay attention to where exactly the brackets are placed.
The only thing I don't like about pass_to is that it's two words and is harder to type. I suggest using something like thru. This suggestion comes from two places:
1) it's function used in lodash, so it comes with some history that already may suggest what it's doing (take input, pass it through the function).
2) It's short, simple to write and more importantly it won't take naming space from functions/methods (I've someone propose 'next' which is something that might be very useful as a method name).
Can't wait to try new syntax :snake:
EDIT. actually I think the 'pass_to'/'thru' approach maps almost 1 to 1 to lodash which is staple JS/TS functional programming library. It's almost as if everything in Roc would be at the start of _.chain(thing) in lodash, so that would make Roc approachable for all JS/TS devs using functional approach.
Here's simple comparison in case you want to see how close they look.
passApproach =
array = [1, 2, 3].map(\n -> n+1).thru(getSecondEl)
array
getSecondEl = \list
list.get(1)
function passApproach() {
const array = _.chain([1,2,3]).map(n => n+1).thru(getSecondEl).value()
return array
}
function getSecondEl(list) {
return list.at(1)
}
By the way I think this comparison shows Roc in very good light. Functional style becomes more and more common in JS/TS land and Roc with this syntax looks like a obvious step up. It's 'lighter', clean, less verbose and has better support for functional programming (duh).
Good to know that this has been done before and works well!
Perhaps we can start with the clear and self-evident pass_to and move to something shorter if there is demand.
I've read the proposal and skimmed most of the this thread, but apologies if I've missed something here: There was a lot of chatter about .( vs .> vs .| vs .passTo( vs so many other things, and the lack of consensus suggests to me that it might not be possible to find a "non-weird" option, which sort of defeats the purpose of making the syntax more familiar to mainstream language users.
What I'm wondering is whether the local function piping needs any special syntax at all.
Would it be possible to alter the resolution algorithm such that val.localFunc() Just Works? No val.(localFunc) necessary? Perhaps I'm overlooking something important, but to an outsider, it's not obvious to me why local functions need a special syntax. I'd imagine we'd need to solve the issue of disambiguation, but realistically how often will that come up for users? I would think it's more common that a user would want to write val.method().localFunc() than result.mapErr with a local, custom mapErr, and in those rare situations it's obvious why a special syntax is needed to disambiguate.
Perhaps local functions take priority over methods from external modules? And if you actually intend to use a same-named function from a different module, that is when you use the dot-parens syntax (e.g., result.(Result.mapErr) to prevent a local mapErr function from being used, which again, I think would be rare)
maybe functions with the same name and same first type just shouldn't be allowed, so you'd have to rename one of them
i can't think of any case where you'd actually want to override a method provided for a type
but i could see why you'd want to add new ones locally with the same syntax
so i support that idea :thumbs_up:
I was just thinking about the same thing! If we prioritize local functions during method resolution, then we could just say "sorry, gotta rename the local function if you don't want it to override the function from the other module"
This wouldn't seem too unreasonable to me as a language user. And down the line we could introduce a special disambiguation syntax to improve quality of life, but that potentially could be separate from this proposal to keep the scope small.
I know it's a long thread, but we did talk about all of those things earlier :big_smile:
e.g. we talked about "do we really need this?" and someone pointed out that they've wanted it in other languages (and actually now that we've talked about it, I keep seeing cases in Rust where I'd use it :laughing:)
we also talked about "Perhaps local functions take priority over methods from external modules?" - the problem with that is, it can create bugs
where I define a new local function, not realizing I have code elsewhere in that module which happens to use an external function with the same name, and bad things silently happen
also, I definitely think .pass_to is the design we want
Ah shoot, sorry for rehashing things that were already discussed. And thanks for the summary!
no worries! I know this is a particularly long thread :big_smile:
actually it's nice to have a summary of those things here!
oh, also we talked about one-word alternatives to .pass_to like .call, but the problem is then they're inaccurate - e.g. if I say foo.call(bar, baz) then it's saying foo.call() but that's wrong; foo is not a function, and it's not being called!
foo.pass_to(bar, baz) is accurate; foo is being passed to bar
Richard Feldman said:
where I define a new local function, not realizing I have code elsewhere in that module which happens to use an external function with the same name, and bad things silently happen
well, the other solution is to make it so that the methods, not local functions, have the out-of-place syntax, which would also discourage overuse when autocomplete could be designed to fill in module names:
[1, 2, 3]
|> _map \x -> x + 1
|> _keepIf \x -> x > 2
|> _first
|> someLocalFunction
|> Inspect.toStr
|> Stdout.line!
and i like this better than anything proposed in this channel, but i realize that's an unpopular opinion.
I also like the .pass_to solution, because even if it's implemented as a special case in the compiler, an unfamiliar user can at least imagine it as a "builtin method" that can be used like any other method via static dispatch:
pass_to : a, (a -> b) -> b
i don't know if it would be possible to write a type signature for pass_to with the current syntax
Right, it's not a perfect mental model, but an unfamiliar user could at least think of it this way
well that only works for pass_to if there are no other arguments :sweat_smile:
but it's close enough
Rust users might think of it more like a macro then to account for the variable number of args :laughing:
And maybe if Roc ever supports variadic functions in the future, then this could be implemented in the standard library, rather than a compiler special case
nah, varargs would all need to have the same type
I don't think it could be implemented as a "normal" function
Is .pass_to( still up for discussion or petty much chosen at this point? I really dislike it. It's 9 characters to do a basic function call when the status quo is at most 4 (a space, |> , (), or .()). It also got letters in it, which when reading will make it harder to pick out actually meaningful names like the function name and nearby variables and methods. I'd prefer something punctuation-only.
Also, once the dust settles on all these discussions, are you planning to release a second draft of the proposal document? A lot has changed and it's hard to keep track of what the current plan is.
To be clear, you can still use |> even if .pass_to exists. Just a tradeoff around parens and readability in certain cases.
I wasn't thinking we'd keep |> in that world - really seems like what we want there is something that starts with . and can have things chained after it
keep in mind that I think this would be infrequently used compared to |> today - pretty much only when doing |> into an unqualified call today
(with some exceptions that are even rarer)
so I don't think the verbosity will come up that often
I'm open to alternative ideas but so far this feels like the best option by a significant margin to me
like if we wanted it to be shorter, I could see an argument for .pt(fn, arg)
on the theory that it's a common enough builtin to use that everyone will learn what it stands for quickly
I like the idea of pt. We could also go with just to
I really like the current syntax where you have one thing and pipe it through a list of functions.
In the proposal is a quote from gleam, that it is just syntax and should not matter, but it did.
My question is, if it is just syntax, why will I not use the alternative to |> as frequently as I use it today?
Oskar Hahn said:
My question is, if it is just syntax, why will I not use the alternative to
|>as frequently as I use it today?
I think a motivation behind static dispatch is reducing the amount of times we have to write module qualifiers, currently a lot more frequent in Roc than in other programming languages. A syntax-only change wouldn't reduce qualifier usage. Adopting static dispatch will make Roc more competitive with mainstream programming languages in terms of verbosity, and more familiar to programmers in those language as well.
I strongly think .> Is the best of the alternative pipe syntaxes:
Would |> really go away? That will only happen if you mostly use nominal types and also don't have many local or imported functions. I think it would still be super common to have local or imported functions that aren't from the module a nominal type is defined in. So I would expect to still significantly use |> even if static dispatch existed.
I don't really see .pass_to as a viable replacement for |>. .pass_to would only be used if everything else is static dispatch and only one or two things need .pass_to. I would probably switch back to |> if there was too much noise or too many .pass_tos in a row
If we truly expect this to be super common such that pass_to or the equivalent is used all over the place, I would argue that we should just commit to a smarter resolution algorithm that allows using local functions directly with the dot method syntax instead of limiting it to nominal types and functions exposed in their modules.
Just a little syntax comparison using an example from above:
What we have now:
[1, 2, 3]
|> List.map \x -> x + 1
|> List.keepIf \x -> x > 2
|> someLocalFunction
|> List.first
|> Inspect.toStr
|> Stdout.line!
With the pass_to syntax:
[1, 2, 3]
.map( \x -> x + 1)
.keepIf( \x -> x > 2)
.first()
.pass_to someLocalFunction("extra arg)
.pass_to toStr
.pass_to Stdout.line!
with .> syntax:
[1, 2, 3]
.map( \x -> x + 1)
.keepIf( \x -> x > 2)
.first()
.> someLocalFunction( "extra arg" )
.> toStr
.> Stdout.line!
A syntax I just thought of:
[1, 2, 3]
|> .map \x -> x + 1
|> .keepIf \x -> x > 2
|> someLocalFunction
|> .first
|> Inspect.toStr
|> Stdout.line!
Was keeping dropping the fully qualified requirement by simply using .name _.name? Instead of changing the method call syntax.
Or even having a different pipe operator, eg:
[1, 2, 3]
.> map \x -> x + 1
.> keepIf \x -> x > 2
|> someLocalFunction
.> first
|> Inspect.toStr
|> Stdout.line!
Of all these syntaxes, the suggested syntax with .> for piping, and the last one that uses .> for static dispatch calls are my favourites.
Brendan Hansknecht said:
I don't really see
.pass_toas a viable replacement for|>..pass_towould only be used if everything else is static dispatch and only one or two things need.pass_to. I would probably switch back to|>if there was too much noise or too many.pass_tos in a row
A very significant number of my pipe functions would be static dispatch, though. Like a huge number of my function calls are operating on some built in data type.
I do think that .> looks nicer than |> when combined with static dispatch calls.
yeah in Rocci Bird there are 40 uses of |> and 38 would become static dispatch. The two that wouldn't are these two:
pipes =
prev.pipes
|> updatePipes
|> List.appendIfOk pipe
plants =
prev.plants
|> updatePlants
|> List.appendIfOk plant
...because the update___ functions take a List
so these might become:
plants =
prev.plants
.pass_to(update_plants)
.append_if_ok(plant)
or:
plants =
prev.plants
.pt(update_plants)
.append_if_ok(plant)
Wouldn’t .thru be a lot more intuitive than .pt?
I feel like I wouldn’t have to Google the former
well personally my main reaction to the above is that .pass_to is fine :big_smile:
Yeah, I mean if we are considering something different
I think .pt is too cryptic
also of note, in the Roc-Ray physics example port it started with 17 uses of |> and ended up with only static dispatch
so between the two, that's 57 uses of |> and 55 of them became static dispatch, meaning (at least for those two examples) we're discussing syntax for 2 out of 57 usess
I had a general intuition that almost every use of |> would become static dispatch, but going through those two examples reinforced it
of note, in mainstream languages, this:
plants =
prev.plants
.pass_to(update_plants)
.append_if_ok(plant)
...could only be written as:
plants =
update_plants(prev.plants)
.append_if_ok(plant)
which also looks fine to me
personally I could see myself writing it either way
so my overall thought here is that if we're talking about an edge case, it doesn't make sense to spend a bunch of strangeness budget to make it look more concise
Brendan Hansknecht said:
If we truly expect this to be super common such that
pass_toor the equivalent is used all over the place, I would argue that we should just commit to a smarter resolution algorithm that allows using local functions directly with the dot method syntax instead of limiting it to nominal types and functions exposed in their modules.
oh I forgot to address this earlier - the problem with this is that any resolution that allows using local functions silently is error-prone
the specific way it's error-prone is:
Richard Feldman said:
we also talked about "Perhaps local functions take priority over methods from external modules?" - the problem with that is, it can create bugs
where I define a new local function, not realizing I have code elsewhere in that module which happens to use an external function with the same name, and bad things silently happen
so to summarize:
|> in the two examples of Rocci Bird and the roc-ray physics example became static dispatch insteadfoo.bar() style to e.g. foo |> bar() for the sake of the edge case..pass_to(), has very little strangeness budget cost (it just looks like a normal method call) and depending on the name, the learning curve can be small too..pass_to() for this, but given that it's an edge case, and that all the alternatives are significantly less self-descriptive (or in some cases actively misleading, like .call), I still think .pass_to() is—by a substantial margin—the best design of all the many options we've discussed in this thread! :big_smile:How is it 2 out of 55? You expect numbers and model to be nominal types? Also, you can't use static dispatch with functions like List.map. so it would be all of those as well.
Also, I really don't think these are even close to representative when thinking about how real projects and large code bases will be architected. These examples are single file scripts.
I do agree that mainstream languages don't have |>, but I think that it is one of my favorite pieces of roc syntaxes that would enhance other languages.
Brendan Hansknecht said:
How is it 2 out of 55? You expect numbers and model to be nominal types? Also, you can't use static dispatch with functions like
List.map. so it would be all of those as well.
sorry, I don't follow :sweat_smile:
can you give an example of some code from Rocci bird that you don't think would use static dispatch?
Any |> List.map cause static dispatch doesn't support it
The places where |> is used with the model like:
prev
|> updateFrameCount
|> runGameOver
Any use with Num I assume cause Num isn't a single type but multiple types. So I don't think it will have static dispatch? Though I just don't know how resolution works there (we should probably discuss that)
frameCount
|> Num.bitwiseAnd 0xFF
|> Num.toU8
oh Num is a nominal type, so static dispatch would definitely work on it
Since there are no big projects in Roc jet, maybe you could analyze an elm codebase how many usages of |> could use static dispatch?
For example the real world example app?
https://github.com/rtfeldman/elm-spa-example/tree/master
Ah yeah, Num is a single nominal type and all of the specific types are just aliases.
and updateFrameCount takes a TitleScreenState, which I was assuming would become a custom record in this world, which in turn would mean you could just do prev.update_frame_count().run_game_over()
It's not an opaque type today, would it really be a nominal type in this new world?
Is nominal the expected default?
I'm not sure one way or the other to be honest
my default assumption is that for things that don't get constructed very often (e.g. application state), nominal would make more sense
so for any non-nominal type, you will still want |> and this decision feels a lot harder in general
:thinking: what would be the downside of making TitleScreenState nominal?
adding .0 everywhere. feeling more complex. feeling concerned about not being able to pass it to another module if the app gets more complex. No longer supporting pattern matching or partial record functions. Maybe not specifically an issue for rocci-bird, but definitely issue for real projects
.0 wouldn't be necessary for custom records
that's only for custom tuples, which are wrappers around other types (e.g. Email being a wrapper around Str)
btw in the specific case of Rocci Bird, if you did want them to be structural, it would change this:
TitleScreen prev ->
prev
|> updateFrameCount
|> runTitleScreen
Game prev ->
prev
|> updateFrameCount
|> runGame
GameOver prev ->
prev
|> updateFrameCount
|> runGameOver
...to this:
TitleScreen prev ->
run_title_screen(update_frame_count(prev))
Game prev ->
run_game(update_frame_count(prev))
GameOver prev ->
run_game_over(update_frame_count(prev))
...or this:
TitleScreen prev ->
prev
.pass_to(update_frame_count)
.pass_to(run_title_screen)
Game prev ->
prev
.pass_to(update_frame_count)
.pass_to(run_game)
GameOver prev ->
prev
.pass_to(update_frame_count)
.pass_to(run_game_over)
all 3 of these look totally fine to me
like I could do a stack ranking of my preferences in terms of how they look, but the delta between them is not like "I'm gonna flip a table if it can't be written that way" :big_smile:
at least to me!
Switching between, rust/c++ and roc, going from run_title_screen(update_frame_count(prev)) to prev |> update_frame_count |> run_title_screen is absolutely huge
So I disagree on those being equivalent
well if you wanted this (which is what I personally would go for):
TitleScreen prev ->
prev
.update_frame_count()
.run_title_screen()
Game prev ->
prev
.update_frame_count()
.run_game()
GameOver prev ->
prev
.update_frame_count()
.run_game_over()
...the only additional change would be changing TitleScreen : to TitleScreen := and then in initGame and initTitleScreen changing { ... } to TitleScreen.{ ... }
But only if a nominal type makes sense. I don't think it always will and it definetly is a decision
totally!
I have nominal types in rust and c++, but I see run_title_screen(update_frame_count(prev)) all the time
Brendan Hansknecht said:
Switching between, rust/c++ and roc, going from
run_title_screen(update_frame_count(prev))toprev |> update_frame_count |> run_title_screenis absolutely huge
can you say more about why you see this as huge? :thinking:
to me, the main situations where pipelines are much nicer is when a single-line call wouldn't be reasonable, and when order of arguments matters and it's clearer with a pipe (e.g. today I like to do str |> Str.endsWith ".txt" over Str.endsWith str ".txt")
and I do have a preference for prev.update_frame_count().run_title_screen() over run_title_screen(update_frame_count(prev)) but I wouldn't call it a huge difference
I think prev.update_frame_count().run_title_screen() is best in general. I think that prev |> update_frame_count |> run_title_screen is essentially as readable and also pressure people into designing to put the most important arg first like with the dot syntax. run_title_screen(update_frame_count(prev)) with more complex functions and multiple args quickly gets complex and hard to read. Often requires lots of extra parens or breaking out a temporary variable to deal with splitting complex expressions. I think it is a significant readability hit.
Oskar Hahn said:
Since there are no big projects in Roc jet, maybe you could analyze an elm codebase how many usages of |> could use static dispatch?
For example the real world example app?
good idea! It has 127 uses of |> and I went through and looked at which ones would be static dispatch vs not. It's a bit of an apples-to-oranges comparison because a ton of them are involved in writing out encoders and decoders, which in Roc would just go away because we can infer those. So the total count in Roc would be a lot less than 127 (maybe like 25% of them would go away).
there are a bunch like this, which wouldn't come up in Roc because of automatic encoder/decoder inference:
there are also some where I don't think it really mattered one way or the other:
this one is interesting because we don't have the equivalent of Tuple.mapFirst in Roc:
...but with static dispatch, we could offer .map_0, .map_1, etc. as builtin methods on tuples that are just automatically available. Not saying we should or shouldn't do that, just noting that if we do want something like this to be available, static dispatch makes it possible for it to be more ergonomic than what's possible today.
here's a good example of something similar to the Rocci Bird structural vs nominal type question:
Editor is a structural record today (Elm doesn't have a concept of nominal records), but it has its own Editor.elm module, and I think it would be about as ergonomic as a nominal record.
overall I think if you take out the encoding/decoding uses of |> that wouldn't apply in Roc (but which would use static dispatch anyway even if they stayed), this looks pretty comparable to Rocci Bird in that:
|> Endpoint on the end vs .pass_to(Endpoint) vs Endpoint(...))this one is interesting:
List.range 1 totalPages
|> List.map viewPageLink
|> ul [ class "pagination" ]
the direct translation would be:
List.range({ start: At(1), end: Length(total_pages) })
.map(view_page_link)
.pass_to(ul, [class("pagination")])
if there were no .pass_to (or equivalent), it would have to be this:
ul(
[class("pagination")],
List.range({ start: At(1), end: Length(total_pages) })
.map(view_page_link)
)
here are some different ways this could be written based on some ideas in this thread:
.pass_to() List.range({ start: At(1), end: Length(total_pages) })
.map(view_page_link)
.pass_to(ul, [class("pagination")])
.pt List.range({ start: At(1), end: Length(total_pages) })
.map(view_page_link)
.pt([class("pagination")])
|> List.range({ start: At(1), end: Length(total_pages) })
.map(view_page_link)
|> ul([class("pagination")])
.> List.range({ start: At(1), end: Length(total_pages) })
.map(view_page_link)
.> ul([class("pagination")])
of all these options, including the "just write it in the direct call style" I personally prefer .pass_to over all the other options
also, something I don't think I noted earlier - only .pass_to(...) and .pt(...) allow you to continue chaining afterwards
|> and .> don't have the same precedence, so if you wanted to throw another .foo() on the end there, it wouldn't work
Would have to be this, right?
List.range({ start: At(1), end: Length(total_pages) })
.pass_to(List.map, view_page_link)
.pass_to(ul, [class("pagination")])
hm, why?
.map on a List would dispatch to List.map
I thought we said that static dispatch doesn't work with function like map due to changing the inner type variable of List.
Just like abilities fail on this case today
Though I guess it should work as map: a, (b -> c) -> d?
oh, no it turned out that works fine
what doesn't work is if you try to abstract over that
in a way that uses the element type
but using it like this is fine
Can you give an example?
the example we talked about last time was trying to make something like Rust's IntoIterator - where you say like "give me a container with elem as a type parameter and I'll give you back an Iter elem"
that can't be done today, and it also couldn't be done using static dispatch
But you could make List.toIter: List a -> Iter a and it would work to call myList.toIter with toIter: a -> b, right?
So you can't make an abstract toIter, but you can make many types have a concrete impl?
Or is that toIter still broken?
Also, I feel like I have gotten into the weeds away from my initial point. Even in rust/c++ where all types are nominal, it is still common to see complex nested function call sequences that are not methods. In those cases, I find |> much more readable. So I would want to keep it even with this proposal and I would expect it to still be pretty common in larger more real world code bases. We should have strictly less nominal types than those languages and they can still get bad function chains pretty often.
I guess we can just find out? haha
certainly |> would be there at the outset to avoid a breaking change
my prediction is that it will be used so infrequently (especially assuming we have .pass_to) that it will end up feeling unnecessary
but maybe it doesn't turn out that way in practice!
no real harm in leaving it in at first and finding out empirically
We'd want |> and dot calls to have the same precedence there, right?
It might be unintuitive, but the pizza is unintuitive to C-style experienced beginners anyway
Sam Mohr said:
We'd want
|>and dot calls to have the same precedence there, right?
I would find this really confusing personally. No other infix operators work that way.
also it would necessarily be whitespace-sensitive
e.g. there's only one reasonable way to parse foo.bar() |> baz().blah()
there's a case to be made that this should parse differently:
foo.bar()
|> baz()
.blah()
but I think it's confusing if those parse differently
this is one of the reasons I prefer .pass_to - it parses the same way in single-line vs multiline, and there are no special precedence rules needed:
foo.bar()
.pass_to(baz)
.blah()
Yeah, they have different precedences. So both are needed for different use cases
as in both |> and .pass_to are needed?
Yeah
that's possible, although my prediction is that people will say "why do we have both of these? why not just have .pass_to" - but again, I could be wrong and there's only one way to find out in practice! :big_smile:
|> for structural types and more complex function chaining. Though maybe if everything is nominal in practice, only .pass_to will be needed
yeah my guess is that custom records will be more common once we introduce them, but also I could be wrong about that!
And yeah, I think at this point, it is something where we really need to see in practice.
Though may still be hard to fully judge due to all roc code being very small today
.pass_to would still work with structural types, right?
Like how .is_eq would be autoderived
I guess it could work with structural types. Though having it be a single piece in a chain of mostly method calls looks natural. Having it on every single call due to using a structural type is a lot of extra noise compared to |>
@Brendan Hansknecht would you get the benefits you want from |> out of .> chaining? It feels like that has the same visual shape while still supporting autocomplete and equal precedence to method calls
Quite possibly assuming it is for both nominal and structural types
I'm not sure how .pass_to would be implemented, but .> would just be sugar, so I'd expect it'd work for both
.pass_to would be sugar that would work on any value regardless of type
it would be exactly the same as |> today except spelled differently :big_smile:
So then... can we instead/also call it .pipe_to()?
Then I'm still happy with .pass_to!
It would be nice to have one Roc Term for this concept. Is |> a pipe that passes? Does .pass_to pipe? Is the definition for one word just a link to the other?
I like that .pipe_to feels like "hey compiler, here's some syntax sugar, please make the invisible plumbing to chain this together"
what I like about .pass_to is that you're passing it to a function
foo.pass_to(bar) is so clear it practically doesn't even need documentation :big_smile:
I can't say the same of any of the alternatives we've discussed, which is one of the reasons I like it the best
Sorry, I have no time to read through the thread. The proposal is great, I also don’t very like the .(f) (it also reminds me of .call(f) from js but without “call”), but skimming the thread, I see there are already a great conversation and ideas on it.
My question is regarding abilities. First, where item.Sort looks unexpected as the dot is an accessor in the lang, but here it works as special syntax. I’d propose having where item ~ Sort so item : List means “item is a List”, and item ~ Sort means “item conforms Sort”, so you can even do this in simple cases: f : ~Sort -> Bool omitting the where block.
But more important for me is the part about abilities removal. I thought the key advantage of traits/type classes is the separation of a specific behavior. So if one ability implements method named toString and another ability implements a method with the same name, there won’t be any clashes as you have to explicitly specify what you’re using at the call site. Without this, it feels like composition is the only way. Isn’t it a more confusing and complex concept? I might be missing something, unfortunately, have no time to read the whole discussion.
Ah, there’s another thread about it.
Upd. Not quite. Can someone guide me on where I can find a relevant discussion? Or even give a summary? Thank you.
Agree. Would be bad if names clash. I suppose if names clash but types are different there will have to be another function in the module with the same name but a different type (overloading)? If the names clash but types are the same - there will be just no way to specify different implementations
Yeah, this is much more akin to go interface than abilities. If you happen to have the right function, you match the static dispatch.
Go does not have overloading and I haven't seen issues with this in practice, but it does mean you could hit a hard place where it is impossible to implement two different static dispatch interfaces on the same type
If that happened, you would have to either:
myType.toReadable()I'm sure it must happen on occasion in practice, but I'm not sure it generally becomes a real issue.
yeah I think it's really valuable to have Go as a reference point for what happens if you use method name as the only way to resolve them
and it's really good to know that it hasn't been a problem in practice there! :smiley:
The implicit interfaces is probably the feature, I love most about Go. The full type inference feature is probably the feature, I love most about Roc.
I am a bit skeptical about the static dispatch proposal, but if it would combine both of these features, it would be fantastic.
just ran into some Rust code where I would have used .pass_to if Rust had it:
NonEmptySlice::from_slice(mono_exprs.extend(fields.iter()))
.map(MonoExpr::Struct)
I would have preferred to write it like this (using proposed Roc syntax):
mono_exprs.extend(fields.iter())
.pass_to(NonEmptySlice.from_slice)
.map(MonoExpr.Struct)
I haven't successfully kept up with this thread. I will say that I was using |> localFunction quite a bit for game stuff, so I'm another vote in favor of pass_to (although maybe local nominal types would work fine too; I like defaulting to non-opaque records).
random thought: we were talking at some point earlier in the thread about how Abilities let you define multiple opaque types in the same file with their own equals etc.
I just realized that if it turns out we miss that in practice, one way we could reintroduce it in the static dispatch world would be to let you do nested modules - that is, use the module keyword more than once in a file
like Rust, OCaml, etc. allow
that way we'd have both the simple "static dispatch resolves to the function inside the module where the nominal type was defined" design and also "you can define multiple nominal types in the same file which have their own methods"
but I think regardless we should not include that at first, to see what it feels like without it - maybe it's not necessary in practice
just good to know that it's a straightforward thing we could do if it turns out we miss that aspect of the current Abilities design
My small feedback regarding the [-2, 0, 2].map(.abs().sub(1)) to [-2, 0, 2].map(\x -> x.abs().sub(1)).
What I love about the current Roc syntax is that lambda is almost always starts with the \ which brings the attention to the fact that it is a lambda, plus simplifies the parsing.
I do not particularly like the '.foo' and '&foo' syntax, as it diverges from the above.
But I still like to express simple things with fewer characters. That's why I have experimented with a closure shortcut in this idea and the related PR #ideas > Closure shortcut feature
Using my syntax, you will add a slash to indicate the lambda [-2, 0, 2].map(\.abs().sub(1)).
Plus, you can say other things in consistent way [-2, 0, 2].map(\+1), [a, b].map(\.foo * 2), etc.
One thing I didn't see mentioned is what kind of importing is required to use static dispatch. For example, if I call Dir.copyAll! "public" "build" which returns a Result, I've obviously imported Dir into my file, but would I also need to import Result to then call map_err on it? (And if Result happens to be automatically imported, then same question but on a type that does need an import statement usually)
If you can call map_err without importing Result via static dispatch, then it does seem to me like we at least lose local reasoning at the module level. As in, one can only use what is actually imported. Not a huge loss necessarily, but I would guess it makes it harder to write linting rules a la elm-review for example.
To my understanding, no import is necessary. Importing would only be necessary for typing functions and variables.
I'm a lurker here, hoping to dive in to Roc at some point soon, but for now I wanted to say I really like this proposal and it reminds me of something I read about the Koka language the other day: https://koka-lang.github.io/koka/doc/book.html#sec-dot. Wanted to mention it in case anyone else sees an interesting connection here.
Koka is a function-oriented language where functions and data form the core of the language (in contrast to objects for example). In particular, the expression
s.encode(3)does not select theencodemethod from thestringobject, but it is simply syntactic sugar for the function callencode(s,3)wheresbecomes the first argument. Similarly,c.intconverts a character to an integer by callingint(c)(and both expressions are equivalent).
I like this proposal!
Is there a summary floating around of the current alternatives considered for calling local functions in this syntax, and the arguments for/against those?
I have ideas but don't want to re-tread ground
this is where I'm at right now:
Richard Feldman said:
what I like about
.pass_tois that you're passing it to a function
foo.pass_to(bar)is so clear it practically doesn't even need documentation :big_smile:
I can't say the same of any of the alternatives we've discussed, which is one of the reasons I like it the best
Fair
My suggestion was going to be, "what if we hold off on allowing local functions in the '.' syntax for a bit, keep |> for the moment, and see how things evolve"
Easier to land on another solution later if we don't have a bunch of code to migrate
definitely want to keep |> at first for backwards compatibility and incremental migration regardless! :big_smile:
Eli Dowling said:
Just a little syntax comparison using an example from above:
....
@Joshua Warner I outlined a few syntax options here :)
Richard Feldman said:
definitely want to keep
|>at first for backwards compatibility and incremental migration regardless! :big_smile:
I'm glad, because I'd like to see how this actually works in practice before removing |>. I see all the benefits of (a) consistent idioms and (b) syntax that is familiar ... but
trim was a thing a string can do to itself) ... the underlying model is different -- trim is something that can be done to a string, not something a string can do. (If i wanted to be picky about the metaphors ... the effect of applying trim to a particular string should be str.trimmed ... so a thing that looks like a method on an object is actually telling a little white lie about the underlying model, even if it's a sugary lie .... metaphor conceals the fact that what is passed through the pipeline may be radically changing str.trim().toUtf8().first() (and forgive me if I've got the current syntax wrong) involves one function that produces a string, one that produces a list, and one that produces a U8 ... which the "tight" binding of a . does not make clear|> pipeline on the other hand carries a metaphor I like ... of a value "passed" through successive functions each of which "transforms" it in a certain way ... and. operator as a form of method call is very familiar from object-oriented and imperative models (so great for people who are coming at a ML-family functional language from those places), |> has a venerable tradition in the ML space (e.g. OCaml, F#, Elm) and losing it would be turning away from that, which I'm a tiny bit sad about ...For all those reasons, I'd like to see how it actually works in practice before putting |> to sleep.
loving this. my 2 cents: I recently had this situation:
aList
|> List.walk ...
List.walk ...
...lots of code...
|> List.dropIf ... # this is indented one tab too many and producing a strange bug hard to catch
with this new proposal it would become:
aList.walk(\...
subList.walk(...)
).dropIf() # because of the parenthesis this can no longer be misplaced
w
Yep, as much as parentheses can feel cumbersome, they are much easier to parse correctly for humans and the compiler alike
And that calling convention is just SO mainstream that it helps with adoption
Sorry for the beginner question, but how does this proposal impact back passing <- and tasks?
storeEmail=path->
url <- File.readUtf8 path |> Task.await
user <- Http.get (url,Json.codec) |> Task.await
dest = Path.fromStr "$(user.name).txt"
_ <- File.writeUtf8 (dest,user.email) |> Task.await
Stdout.line "Wrote email to $(Path.display dest)"
Tasks and backpassing are both already deprecated
Task is almost done being replaced by purity inference
Backpassing is just slated for eventual removal.
It’ll be crazy to compare the ROC of April 25 to the ROC of April 23
And my phone won’t stop changing Roc to ROC
Yeah... purity inference, snake_case, and static dispatch
Anthony Bullard said:
And my phone won’t stop changing Roc to ROC
Even the machines are excited about ROC :rock_on:
it is one of the officially-accepted spellings, to be fair
Can’t wait to see a full app (or some AOC) converted to PI, snake case, and static dispatch
Oh and with var and for
Yep...will be crazy different
Hi, I have not been around for a while and when I came I saw this proposal :sob:
I can understand where does it come from (it's so popular, let's do the same and also be popular).
It reminds me Kotlin, the language I really dislike in many aspects, but one of the biggest is that something.method(...) is so ambiguous. You NEVER know where does the method come from. And the parenthesis all over the place :sad:
Is this proposal widely accepted? I can't see much resistance when scrolling through this thread…
In the end, I will probably accept whatever Roc morphs into, but I was soooooooo excited when first saw Roc without the dot-parenthesis-madness.
One more thing why I hate Kotlin-like it.that(…) aka. the "static dispatch":
that_object.method(…). and the method is not thereNow, compare this with the beautiful world of |> SomeModule.functionXyz and all is there, just in front of you. In an instant you know you need SomeModule and the world is a wonderful place to live again.
Nicola Peduzzi said:
loving this. my 2 cents: I recently had this situation:
aList |> List.walk ... List.walk ... ...lots of code... |> List.dropIf ... # this is indented one tab too many and producing a strange bug hard to catchwith this new proposal it would become:
aList.walk(\... subList.walk(...) ).dropIf() # because of the parenthesis this can no longer be misplacedw
I had similar bug in Java once. The parenthesis mess won't protect you from such a subtle bug, because often there are so many of them, you can …walk(…).dropIf()) or …walk(…)).dropIf() as easily.
Is this proposal widely accepted?
We want to try it out and make a decision based on that experience.
Hey @witoldsz, I in a similar place to you with regards to the mix of understanding why the proposal would draw more people to the language (which is an important and big benegit), but also feeling a bit sad about it. :heart:
I also share your misgivings about Kotlin's take on this syntax, but I do not believe the proposed design would have the same downsides to auto-complete. The proposed design contains a .pass_to(OtherModule.some_method) syntax for chaining to to arbitrary non-method functions, and it expressly starts with a . so those options can be part of the same auto-complete experience as method calls.
AND IT IS IMPOSSIBLE to figure out why… which import statement is missing or what, in order to have same method.
We don't rely on imports in this proposal, so as long as the type is the same as in the example you should be good. The method can change between different version of a library but you can view the type on hover and go look in docs and release notes, same as usual it seems.
I'd be really interested to see some longer, less trivial examples converted into the proposed style. Especially with full indentation (as if auto-formatted). I would do it myself, but I'm not sure I understand some nuances.
I think the upside of whitespace calling has been underrated here. I think it may be beneficial in languages with nested lambdas.
At the bottom of the proposal linked at the top of the thread, Richard links to a couple of larger code samples converted to the new style!
witoldsz said:
I had similar bug in Java once. The parenthesis mess won't protect you from such a subtle bug, because often there are so many of them, you can
…walk(…).dropIf())or…walk(…)).dropIf()as easily.
Auto-indentation and editor-based bracket highlighting help a lot in these 'pyramid of death' situations, in my experience
I see where @witoldsz comes from. Roc would take quite a turn from "elm inspired" with these changes.
speaking of autocomplete, wouldn't be possible for an editor to show a list after typing |> with all imported functions accepting the current type as first argument?
perhaps out of scope, but I think editor ergonomics could go further for language adoption than familiar syntax.
still it'll be interesting to try out the new syntax. it will be a breaking change right?
Nicola Peduzzi said:
speaking of autocomplete, wouldn't be possible for an editor to show a list after typing
|>with all imported functions accepting the current type as first argument?
Coding in F# a lot lately. I was thinking, do people love the . because it's easy to type? What if editor would autocomplete the |> SomeModule.someFunction when user press . at the end of a variable…
Would that be OK, let's move on with ML-syntax, or people "dislike" even looking at the |> ... syntax?
well I use a font with ligatures in my editor and the arrow made by |> is very pleasant to look at :joy:
still it'll be interesting to try out the new syntax. it will be a breaking change right?
I think we planned to allow both styles during the tryout phase
Jasper Woudenberg said:
At the bottom of the proposal linked at the top of the thread, Richard links to a couple of larger code samples converted to the new style!
Thanks. Interestingly it doesn't contain many nested lambdas (link). It could be seen as a positive that the new style will discourage them, but in my opinion, it makes the code less readable where they are used
old:
parse = \str ->
Str.toUtf8 str
|> List.splitOn '\n'
|> List.walkWithIndex (Dict.empty {}) \dict, row, y ->
List.walkWithIndex row (Dict.empty {}) \rowDict, cell, x ->
Dict.insert rowDict (Num.toI16 x, Num.toI16 y) (cell - '0')
|> Dict.insertAll dict
new:
parse = \str ->
str.toUtf8
.splitOn('\n')
.walkWithIndex(Dict.empty {}, \dict, row, y ->
row.walkWithIndex(Dict.empty {}, \rowDict, cell, x ->
rowDict.insert((Num.toI16 x, Num.toI16 y), cell - '0')
)
.insertAll(dict)
)
I find it a bit harder to see the structure in the second version. It looks almost identical to javascript.
In my converted example, Isn't there a need to disambiguate the arguments to the walkWithIndex function call from the parameters of the lambda? Javascript requires round brackets when there is more than one argument to a lambda
Dict.empty {}, \rowDict, cell, x ->
needs to be
Dict.empty {}, \(rowDict, cell, x) ->
but that conflicts with tuple syntax
actually it should be:
parse = \str ->
str.toUtf8
.splitOn('\n')
.walkWithIndex(Dict.empty {}, (\dict, row, y ->
row.walkWithIndex(Dict.empty {}, (\rowDict, cell, x ->
rowDict.insert((Num.toI16 x, Num.toI16 y), cell - '0')
))
.insertAll(dict)
))
or avoiding multiple brackets on one line:
parse = \str ->
str.toUtf8
.splitOn('\n')
.walkWithIndex(
Dict.empty {},
(\dict, row, y ->
row.walkWithIndex(
Dict.empty {},
(\rowDict, cell, x ->
rowDict.insert((Num.toI16 x, Num.toI16 y), cell - '0')
)
)
.insertAll(dict)
)
)
witoldsz said:
What if editor would autocomplete the
|> SomeModule.someFunctionwhen user press.at the end of a variable…
I had this exact idea when I thought about this for my language a couple of years ago! Just have the LSP do all the work talked about here, but you still have explicit locality throughout the file
The thing is, this is very different from the normal LSP (textDidChange -> Response) model, the response of which is usually an CompletionItem (adding some text after the cursor). I don't remember if you can rewrite before the cursor easily. That's usually Code Action territory
I've also had this idea a long time ago, but nobody else liked it then :big_smile:
Discoverability of that feature would not be great though
@Anthony Bullard after reading the google doc proposal, it is clear @Richard Feldman 's main motivation is to follow the experience of Gleam's creator Louis.
CORRECTION: it was not Richard's main motivation (as explained in following messages)
This might be interesting topic to dig into, if Gleam became popular because
Gleam also has curly brace function bodies and brackets around parameters. Future-roc is a different mix
witoldsz said:
Anthony Bullard after reading the google doc proposal, it is clear Richard Feldman 's main motivation is to follow the experience of Gleam's creator Louis.
since this was not my main motivation, I'm curious what about the doc gave the misimpression that it was :thinking:
It's an easy story to remember
Richard Feldman said:
since this was not my main motivation
OK, I am sorry if I did misinterpret that part.
It was my impression, because from the very beginning you always said being friendly to newcomers is super important and I do appreciate this POV alot! So, going down that though I misinterpret you would otherwise not propose such a big shift in the syntax. My bad.
it's all good, no worries!
To be completely honest: as much as I do not like that syntax, if it was the mean to reach wide audience, I think I would have to agree. With tears in my eyes, but still.
I feel your pain @witoldsz , but I'm excited because Roc is moving syntactically to being very simiilar to my now-defunct language that I never completed.
Alex Nuttall said:
actually it should be:
parse = \str -> str.toUtf8 .splitOn('\n') .walkWithIndex(Dict.empty {}, (\dict, row, y -> row.walkWithIndex(Dict.empty {}, (\rowDict, cell, x -> rowDict.insert((Num.toI16 x, Num.toI16 y), cell - '0') )) .insertAll(dict) ))or avoiding multiple brackets on one line:
parse = \str -> str.toUtf8 .splitOn('\n') .walkWithIndex( Dict.empty {}, (\dict, row, y -> row.walkWithIndex( Dict.empty {}, (\rowDict, cell, x -> rowDict.insert((Num.toI16 x, Num.toI16 y), cell - '0') ) ) .insertAll(dict) ) )
What are people's opinion on that the formatter should do here? Should it enforce the second style? I think that's what prettier would do
I've suggested that before ( without the parenthesis) but people did not like it because it leads to more indentation
Both do not look good… :( The main problem is embedding lambdas. Kotlin tried to solve it, I don't know – maybe Kotlin's style to drop lambda out of parens would help here…
P.S.1. Elm's formatter has that one distinct feature to allow first argument in the same line, so it would be a mix first and second version.
P.S.2. I found out it is sometimes good to do exactly opposite to what "Prettier" does, to help with the layout.
I don't think the syntax is ugly, and so do many here. I do prefer the whitespace syntax too, but almost all of my programmer friends think I'm a goblin because I use a language that doesn't look like javascript or python. Maybe I am, but that's not the point.
"I don't care about these esoteric languages, I just want something that works well. C# does everything I need anyway!" -says my friend who only ever programmed in C#. A lot of people won't look into the semantics of the language if it "looks weird". The creator of gleam actually said that switching to parens and braces made a lot of people suddenly like the language. The fact that you are here in a language community says to me that you are more invested in your craft than most. For wide-spread adoption tho, I think we need this.
To be clear, I'm not bittersweet about this. Static dispatch methods completely sold the syntax for me. Without those, I wouldn't like this syntax change though.
witoldsz said:
P.S.1. Elm's formatter has that one distinct feature to allow first argument in the same line, so it would be a mix first and second version.
Could you edit to example to show the result?
Elm would allow (but not enforce) something like this:
.walkWithIndex(Dict.empty {},
(\dict, row, y ->
row.walkWithIndex(Dict.empty {},
(\rowDict, cell, x ->
witoldsz said:
Both do not look good… :( The main problem is embedding lambdas. Kotlin tried to solve it, I don't know – maybe Kotlin's style to drop lambda out of parens would help here…
I'm not familiar with this aspect of Kotlin - how does it work?
Kotlin do 2 things to help with lambdas:
abc.fn(x, { it -> it.name}) will work, but formatter suggests abc.fn(x) { it -> it.name}abc.fn(x) { it.name }, whereit is the optional, implicit name of the lambda argumentI am trying to figure out anything more I can say positive about Kotlin :thinking: well… nothing comes to my mind at the moment…
@witoldsz That's interesting, you say that. I work at a Fortune 500 that is built on Java and a LOT of Java devs pine for writing Kotlin. But a lot of the deficiencies of Kotlin you identify it shares with Java (some it may make worse with extensions). My biggest bugaboo (with Java, Kotlin, and THIS proposal) is the lack of locality in the source. Many languages aim to have features that save typing when you are using an IDE. I want a language that is easy to read whether it's in a full-featured IDE, a text editor with just syntax highlighting, or on GitHub - so I prefer _very strong_ locality.
And I do _somewhat_ worry that static dispatch will really turn into Java style "one type(class) per file" organization. Or really it'll become Scala without inheritance or Java's ecosystem.
example from Kotlin main page, "functional style" section
val frequentSender = messages
.groupBy(Message::sender)
.maxByOrNull { (_, messages) -> messages.size }
?.key
Message::sender reference to the a static method (aka plain function, Java heritage )maxByOrNull takes only a lambda, so no need to () at all?. works like in JavaScriptI can't believe I am pasting Kotlin code in Roc discussion list :open_mouth:
Anthony Bullard said:
really it'll become Scala
I think it would be extremely difficult to brake and waste Roc so many times, so it could be compared to something like Scala. Not with the Richard's attitude towards being a friendly language!
I was also about to express a worry about this being a move that might necessitate a move towards nominal typing, I read the doc again, and see that Richard has figured out the "what if I can't tell the type without an annotation" problem with methods. :rofl:
witoldsz said:
I think it would be extremely difficult to brake and waste Roc so many times, so it could be compared to something like Scala. Not with the Richard's attitude towards being a friendly language!
Let me be clear , I specifically mean a language with nominal typing that organizes application around a single type per file where the data and methods are strongly coupled and co-located. instead of a "case class" or "data class" with instance and static methods, it'll be type aliases with functions that operate as methods.
Also, I _loved_ Scala coming from writing Java, PHP, and Javascript backends before :-P
It's the reason I'm an FP guy now - it actually lead me directly to Elm - and then Haskell, Erlang, Elixir, F# - and then to trying to design and implement my own language Chakra - and now here to Roc.
It was similar enough to what I knew to make me comfortable, but led me towards functional concepts. And once you go down that rabbit hole - there is no coming back. Unless you are Dave Farley. Hell, even Uncle Bob writes in Clojure now
I don't think the new syntax is ugly (nor do I think that formatted javascript is, necessarily), but I do think whitespace calling does a better job at visually expressing the structure of callback-heavy code
Anthony Bullard said:
The thing is, this is very different from the normal LSP (textDidChange -> Response) model, the response of which is usually an CompletionItem (adding some text after the cursor). I don't remember if you can rewrite before the cursor easily. That's usually Code Action territory
And yet totally possible!!
And why shouldn't we?
I have certainly thought the same thing, particularly early on in my FP journey, before I was frankly too drowning in the coolaid to mind... It's so annoying how easy it is to forget all the paper cuts!
I think we’d be the first LSP to this in FP land
I think @Alex Nuttall's comparison perfectly captures why I personally don't love the static dispatch syntax.
Yeah, overall, I think it may be worth it, the familiar-to-the-punters syntax (which I don't really like), and the slightly increased ease of implementing abilities + inherently getting an equivalent with type parameters.
But man, that abilities code is just much harder to read. When most of what I do is read code... that kind of sucks.
And yeah, if the lsp added type annotations, that wld help a lot, but it certainly wouldn't help within a PR.
I guess my thought is, we can use an editor to make code easier to write, we can provide really intelligent completions and all that. but when I'm reviewing a PR, I don't have a language server, and that's often when quickly reading and understanding the code matters most. In that scenario, clearly tracking the flow of types and the purpose of functions, helps a lot, and static dispatch makes that meaningfully harder.
eg: inserting into a list can have totally different performance characteristics to inserting into a dict. And that is just much less clear in example two.
Anthony Bullard said:
I think we’d be the first LSP to this in FP land
I think so too, and yet it seems so obvious... maybe it's harder than we think.
I don't think it's actually possible right now with the current roc compiler. The parser not being able to handle the hanging pipe may break everything. Plus the way type errors are currently just "type is incompatible" rather than "This is type A, and later we expect type B"
But it could certainly be done, and improving both of those is something that has to be done at some point anyway.
I think it would be more “I have an identifier or literal, then I press dot after” type of thing that could then wrap it in a function call with the other parameter templated out
Like
”hello”. would give you a list of Str methods. Selecting len would result in replace the string literal with Str.len “hello”
If that existed :joy:
I think it would be more “I have an identifier or literal, then I press dot after” type of thing that could then wrap it in a function call with the other parameter templated out
Like
[1,2,3]. would give you a list of List methods. Selecting len would result in replace the list literal with List.len [1,2,3] or [1,2,3] |> List.len
And for |> you can backtrack the pipe from the text so that it parses and lets you get the type and then you could use that to get a list when the > is typed
One general comment that is not true for all code, but for a decent chunk. Related to these comments above:
Interestingly it doesn't contain many nested lambdas
I do think whitespace calling does a better job at visually expressing the structure of callback-heavy code
While I totally agree that whitespace does better with callback heavy code, I don't think that callback heavy code is general seen as good architecture even in functional languages. In some cases, it is highly important, but it is still generally advised to avoid callback heavy code. It is almost always considered harder to read and reason about. Part of this and related proposals is trying to remove the need for callback heavy code in more locations. For loops and purity inference being two great examples that remove callbacks.
Yes that's fair, I suppose I should be writing the future roc example like this:
parse = \str ->
var dict_ = Dict.empty()
for (row, y) in str.toUtf8().splitOn('\n').enumerate()
for (cell, x) in row.enumerate()
dict_ = dict_.insert((Num.toI16(x), Num.toI16(y)), cell - '0')
dict_
Wow, that code really almost feels like rust now.....weird
Or
parse = \str ->
str.toUtf8
.splitOn('\n')
.walkWithIndex(Dict.empty {}, parseRow)
parseRow = \dict, row, y ->
row.walkWithIndex(Dict.empty {}, parseCell)
.insertAll(dict)
parseCell = \rowDict, cell, x ->
rowDict.insert((Num.toI16 x, Num.toI16 y), cell - '0')
Probably want this:
parseRow = \dict, row, y ->
row.walkWithIndex(dict, parseCell)
Not sure why I feel it is necessary to fix perf in an example like this, but it feels required to me.....
That mistake is from my original example btw :embarrass: which should be
parse = \str ->
Str.toUtf8 str
|> List.splitOn '\n'
|> List.walkWithIndex (Dict.empty {}) \dict, row, y ->
List.walkWithIndex row dict \rowDict, cell, x ->
Dict.insert rowDict (Num.toI16 x, Num.toI16 y) (cell - '0')
Perf it up! I'll also try making it more Roc-thonic:
parse = \str ->
str
.toUtf8
.splitOn('\n')
.walkWithIndex(Dict.empty {}, parseRow)
parseRow = \dict, row, y -> row.walkWithIndex(dict, parseCell)
parseCell = \rowDict, cell, x -> rowDict.insert((Num.toI16 x, Num.toI16 y), cell - '0')
ah yeah, guess I missed it in that denser block of the original code
just have to delete .insertAll(dict) and that should be all good.
I really really love Roc's whitespace and |>, but I love de-anonymizing multiline lambdas even more.
Brendan Hansknecht said:
just have to delete
.insertAll(dict)and that should be all good.
fixed
I actually prefer more inlining in this narrow category. Adding function names just seems like indirection
I love de-anonymizing multiline lambdas even more.
I find that I hit a mix here. One thing I really find annoying is dealing with naming (if I make it a top level) or dealing with variable collisions (If I nest the named lambda in the current function)
Alex Nuttall said:
I actually prefer more inlining in this narrow category. Adding function names just seems like indirection
Now that your example doesn't necessarily contain multiline lambdas, I'm ambivalent in this case!
With that bug fixed, here it is inlined:
parse = \str ->
str
.to_utf8
.split_on('\n')
.walk_with_index(Dict.empty {}, (\dict, row, y ->
row.walk_with_index(dict, (\row_dict, cell, x ->
row_dict.insert((Num.to_i16 x, Num.to_i16 y), cell - '0'))
))
))
Edit: with snake case, for fun!
If those lambdas started getting longer and/or you start adding .andAlsoFoo() after them, then I prefer naming them so you can keep each function primarily at one level of abstraction.
Note: I'm not trying to bikeshed, just trying to hypothesize/test that the new syntax wouldn't introduce issues in this case.
Edit: Oops, that wasn't what you were discussing - nevermind! My comments on naming stand, but now they feel less relevant. Accidental bikeshedding is the most infectious bikeshedding!
The whole lambdas actually need to be parenthesised, so:
parse = \str ->
str.toUtf8()
.splitOn('\n')
.walkWithIndex(
Dict.empty {},
(\dict, row, y ->
row.walkWithIndex(
dict,
(\rowDict, cell, x ->
rowDict.insert((Num.toI16(x), Num.toI16(y)), cell - '0')
)
)
)
)
Ohhh gotcha, sorry I only skimmed your questions above. I'll add those missing parens and strikethrough my commentary that was answering a question you didn't ask.
Brendan Hansknecht said:
Wow, that code really almost feels like rust now.....weird
Or even Go, the way you have reassign a slice to its var after an append
with snake_case it would be:
parse = \str ->
var dict_ = Dict.empty()
for (row, y) in str.to_utf8().split_on('\n').enumerate() do
for (cell, x) in row.enumerate() do
dict_ = dict_.insert((x.to_i16(), y.to_i16()), cell - '0')
dict_
Why do lambdas need to be parenthesizsed?
incidentally, this is an example of a use case where I think the imperative style is easier to follow
I prefer reading the nested for version to the nested walkWithIndex version
Anthony Bullard said:
Why do lambdas need to be parenthesizsed?
maybe they don't. My thought was that this list of arguments is ambiguous:
Dict.empty {}, \dict, row, y
Richard Feldman said:
I prefer reading the nested
forversion to the nestedwalkWithIndexversion
I wonder if that might hold true for any foo_after = foo_before.walk... block with nesting (or without).
I expect a lot less walking in general.
maybe they don't. My thought was that this list of arguments is ambiguous:
`Dict.empty {}, \dict, row, y`
Yeah, I don't think it is strictly required, but it is a lot less confusing if it is parenthesized.
Like this only has one valid parsing. z has to be an arg to the original function. Otherwise, it would be trying to return the y, z tuple which requires parens as (y, z)
Dict.empty(), \dict, row, y -> y, z
That said, it becomes a horrid syntax for humans, so I would guess that we will require parens in practice. If we allowed avoiding them, it would probably only be for multiline llambdas passed as args (which use indentation to tell body so don't need parens)
it would actually need to be Dict.empty(), not Dict.empty {}, but yeah :big_smile:
random question about dot syntax: in a world without pipes, will it be possible to apply tags like this?
list.append(el).Ok()
or without the final brackets
This is where I fell like parens or still using |> make the most sense, but I guess it could be a method call as well...
Ok(list.append(el)) or list.append(el) |> Ok
This is something I definitely would need to play with in practice
so potentially, mixed:
thing
.method()
.pass_to(\x -> x)
|> Ok()
prefix only:
Ok (
thing
.method()
.pass_to(\x -> x)
)
or method:
thing
.method()
.pass_to(\x -> x)
.Ok()
I suppose it could be just .pass_to(Ok)
or call it .tag()
yeah certainly .pass_to would Just Work
it's an interesting idea to offer alternatives like .Ok() - seems worth a new #ideas thread to discuss!
Oh yeah, we have pass_to. That looks nice
but, .Ok() definitely could still make sense...
Or compared with come kind of pass to operator:
thing
.method()
.pass_to(\x -> x)
.pass_to(Ok)
thing
.method()
.> (\x -> x)
.> Ok
I think an operator looks cleaner.
until you decide that the line isn't that long and it want it inline:
thing.method().pass_to(\x -> x).pass_to(Ok)
thing.method().>(\x -> x).>Ok
And just for completeness sake, current syntax + loops
parse = \str ->
var dict_ = Dict.empty()
for (row, y) in str |> Str.to_utf8 |> List.split_on |> enumerate do
for (cell, x) in row |> enumerate do
dict_ = dict_ |> Dict.insert (Num.to_i64 x, Num.to_i16 y) (cell - '0')
dict_
Not half bad. I think if the hightlighting was for roc, it would look pretty good
Brendan Hansknecht said:
until you decide that the line isn't that long and it want it inline
I actually still don't mind it tbh, it's a little odd.. but so is the pipe till you get used to it
Richard Feldman said:
with snake_case it would be:
parse = \str -> var dict_ = Dict.empty() for (row, y) in str.to_utf8().split_on('\n').enumerate() do for (cell, x) in row.enumerate() do dict_ = dict_.insert((x.to_i16(), y.to_i16()), cell - '0') dict_
Wouldn’t you have to do Dict.empty({}) still? It still takes a param, I would think () would mean “the only arg is the method receiver “
@Anthony Bullard you're right, that's why some of Richard's recent examples have actually been Dict.empty()
Which opens a door to zero argument functions, which I think is a good change
How would you declare that?
I'm gonna make a topic about it
Isn't it just sugar for {}
That could work
In a purely functional language a zero argument function is a called a constant :wink:
Yes, but we have zero arg functions inpractice today, like Dict.empty {}
The only reason it takes an arg is due to monomorphization
And the arg contains no data, so it really should be zero arg
Ah, because a constant needs to have a fully specialized type?
A top-level one that is
yeah
I’m just getting a hair worried about all this sugar
Ayaz's wrote a doc on that a while back. On why we should keep it this way and require the function.
I’m just getting a hair worried about all this sugar
Yeah, roc is a simple core with a lot of evolving sugar
That it is also important to see that some sugar is being simplified and directly added to the language like early returns and a more robust try
As this seems a little wonky. So the rule is: if a function has one argument, and it is {}, then it can be called with no args when used as a function?
Yeah, just like if a function returns one value and it is {}, it can be ignored
thus you can do:
Stdout.line! "test"
instead of:
{} = Stdout.line! "test"
Ok. It’s just got to feel natural and intuitive.
Yeah, {} is our Unit type which has special handling in some functional languages like Scala, F#, and I think OCaml
I guess only Scala calls it Unit
Ayaz's wrote a doc on that a while back. On why we should keep it this way and require the function.
I'm not Ayaz, but I think that the issue is more to do with us not wanting constant values to be monomorphized, not that functions need to have at least one argument.
Meaning we should be good to go with zero arg functions if we were able to define them syntactically
Brendan Hansknecht said:
thus you can do:
Stdout.line! "test"instead of:
{} = Stdout.line! "test"
But this is a good orthogonal feature that makes swallowing the Dict.empty({}) -> Dict.empty() pill easier
us not wanting constant values to be monomorphized
constants are monomorphized. It is just that we want them to be monomorphized to exactly one specialization instead of opening the can of worms and weird bugs that come from a constant turning into multiple different specializations. Definitely saw some surprising behaviour when a number constant could be a U8, I32, and F32 at the same time.
Oh yeah, that was the idea
Okay yeah, when I say "not monomorphized" I mean "they get one specialization"
I appreciate the clarification!
well Dict is one of the ones we can make work actually
it just hasn't been done yet
Ayaz made it work for numbers, and we can do the same thing for all the other builtins
so in the future it can be dict = Dict.empty
Anthony Bullard said:
As this seems a little wonky. So the rule is: if a function has one argument, and it is {}, then it can be called with no args when used as a function?
oh I actually think once we go to parens-and-commas calling style we should just have an actual concept of zero-argument functions
zero-arg functions were rare back when we only had a concept of pure functions, but they come up all the time now that we have effectful functions :big_smile:
basically any value that used to have the type Task is now a zero-arg effectful function
and foo() is well-understood across languages to be a call to a zero-arg function
so in the future it can be
dict = Dict.empty
i actually think it would be nicer to keep it consistent with any userspace containers and just do Dict.empty().
Not like the two parens matter much at all
fair point!
there's also a weird thing of where to draw the line. You can go one way or the other and say okay, all constants can be generic, or constants have one type. Right now numbers are a special carveout in that x = 1 is generic, but x = A 1 is not. It's probably significantly easier to understand if it goes entirely one way or the other, and I'm not sure there is a need for constants to be generic.
Yeah, I think they should all be one type. I think it is fine to require x = 1 and y = 1.0.
I would assume most uses as two types are accidental
that wouldn't help, actually...1.0 is still generic :big_smile:
(it's generic over the 3 different fractional types we have)
Sure
I just mean that you have to write out the constant twice cause you are using it at two different concrete types
ah, gotcha
We don't automatically cast number types, so it is kinda strange to automatically cast numberic constants (though if we removed it, maybe I would find out I really miss it in practice)
I guess the difference is that we can make sure numeric constants are safe as a type at compile time
The other difference is they're not really casts, they are instances x_1: U32 = 1, ..., x_n: ... = 1 for all unique types of x
Yeah, it is really n copies of the value in each different type. But from a user perspective, I think it is an implicit cast
Doing the work of previewing Static Dispatch syntax (along with the proposed new lambda syntax) and I had some questions, here's what I have so far (this is only the beginning of this module):
Block : [
Free(U64),
File(U64, U64),
]
processInput : Str -> List Block
processInput = |str|
dbg "processInput"
bytes = Str.toUtf8(str.trim())
blocks = List.withCapacity(bytes.len())
List.walkWithIndex(
bytes,
blocks,
|bs, byte, index|
byteValue =
Str.fromUtf8([byte])
.withDefault("???")
.toU64
.withDefault(Num.maxU64)
if Num.isEven(index) then
List.append(bs, File(index // 2, byteValue))
else if byteValue > 0 then
List.append(bs, Free(byteValue))
else
bs
)
dbg stays a statement and not a function?|str| -> I thought we were talking about not having the arrow here, only in the type
That was a mistake, fixed
A small note, as someone predicted, when I am doing this and I go back to the call site of this function, I kinda want to wrap the List(Block) type in an opaque ref so I can change this:
getPartOne = |str|
PartOne.processInput(str)
|> PartOne.compact
|> PartOne.checksum
to this:
getPartOne = |str|
PartOne.processInput(str)
.compact()
.checksum()
And in this snippet:
Str.fromUtf8([byte])
.withDefault("???")
.toU64
.withDefault(Num.maxU64)
The actual Module that's being dispatched to changes three times - and it's not obvious reading the source alone that's happening, or what Modules are being used . No judgement, just an observation
and it's not obvious reading the source alone that's happening, or what Modules are being used
Maybe we could have a naming convention... Str.fromUtf8 might be clearer as Str.tryFromUtf8
Str.tryFromUtf8([byte])
.withDefault("???")
.tryToU64
.withDefault(Num.maxU64)
Yeah, I think we should have _some_ convention for functions not in the Result module that return a Result
I also, for some reason I don't understand, find myself now wanting when, else, and even multiline lambas wanting an end keyword. Maybe because things are becoming a little Elixir-y?
with snake_case and using methods in a few more places:
Block : [
Free(U64),
File(U64, U64),
]
process_input : Str -> List Block
process_input = |str|
dbg "processInput"
bytes = str.trim().to_utf8()
blocks = List.with_capacity(bytes.len())
bytes.walk_with_index(
blocks,
|bs, byte, index|
byte_value =
Str.from_utf8([byte])
.with_default("???")
.to_u64()
.with_default(Num.max_u64)
if index.is_even() then
bs.append(File(index // 2, byte_value))
else if byte_value > 0 then
bs.append(Free(byte_value))
else
bs
)
Oh shoot, snake case
with var and for:
Block : [
Free(U64),
File(U64, U64),
]
process_input : Str -> List Block
process_input = |str|
dbg "processInput"
bytes = str.trim().to_utf8()
var blocks_ = List.with_capacity(bytes.len())
for (index, byte) in bytes.iter().enumerate() do
byte_value =
Str.from_utf8([byte])
.with_default("???")
.to_u64()
.with_default(Num.max_u64)
if index.is_even() then
blocks_ = blocks_.append(File(index // 2, byte_value))
else if byte_value > 0 then
blocks_ = blocks_.append(Free(byte_value))
blocks_
nvm
no, but it could be:
blocks = bytes.len().pass_to(List.with_capacity)
It's probably clearer the first way, I was a little confused
Oh yeah, the magical pass_to
I don't think I fully understand the reason for that
Is it just to get rid of |>?
it's to have a |> that works with . chaining
e.g. compare:
Str.from_utf8([byte])
.with_default("???")
.to_u64()
.pass_to(transform_result_somehow)
.with_default(Num.max_u64)
Str.from_utf8([byte])
.with_default("???")
.to_u64()
|> transform_result_somehow
.with_default(Num.max_u64)
the second thing doesn't actually work because of precedence
it would actually be equivalent to
Str.from_utf8([byte])
.with_default("???")
.to_u64()
|> transform_result_somehow.with_default(Num.max_u64)
which wouldn't be what you want
So every single Module that exposes a statically dispatchable type needs to implement pass_to, or this is some free compiler magic?
free compiler magic
I guess I should read the proposal again (like the 4th time :rofl:)
I don't think it was in there, I think we arrived at it in the discussion afterwards
but basically it works exactly like |>
Oh, ok. I've definitely seen it thrown about.
it's syntax sugar that's designed to look like a method
and it can take multiple arguments, which get passed through
IMO pass_to eats a lot of weirdness budget
Roc dropping |> right as JS is adding it :smile:
so .pass_to(foo, arg1, arg2) is equivalent to |> foo arg1 arg2
I really wish we could just resolve the precedence issue
Joshua Warner said:
IMO pass_to eats a lot of weirdness budget
I really don't think it will haha
.pass_to() feels like an operator wearing a function's clothes
We shall see!
we'll see though!
We shall see
Just wanted to join in the fun ;-)
Anthony Bullard said:
I also, for some reason I don't understand, find myself now wanting
when,else, and even multiline lambas wanting anendkeyword. Maybe because things are becoming a little Elixir-y?
Is someone gonna get mad If I actually suggest this in an #ideas convo?
nope, go for it!
first rule of #ideas is that any idea is fair game :big_smile:
they're just ideas!
Ok, I'll do that tonight. After hopefully my new docs template gets merged ;-) ;-)
interestingly, if you wanted to, you could make this whole function one walk:
Block : [
Free(U64),
File(U64, U64),
]
process_input : Str -> List Block
process_input = |str|
str.trim().to_utf8().walk_with_index(
[],
|bs, byte, index|
byte_value =
Str.from_utf8([byte])
.with_default("???")
.to_u64()
.with_default(Num.max_u64)
if index.is_even() then
bs.append(File(index // 2, byte_value))
else if byte_value > 0 then
bs.append(Free(byte_value))
else
bs
)
you miss out on with_capacity that way though
Yeah, that's why I skipped it. Even though it probably doesn't really matter for AOC
oh can this be a map_with_index? :thinking:
ah not quite
because some get skipped
iterators would make this possible: (not saying it's the best way to write it or anything)
Block : [
Free(U64),
File(U64, U64),
]
process_input : Str -> List Block
process_input = |str|
str.trim().to_utf8().iter().enumerate().keep_oks(
|(index, byte)|
byte_value =
Str.from_utf8([byte])
.with_default("???")
.to_u64()
.with_default(Num.max_u64)
if index.is_even() then
Ok(File(index // 2, byte_value))
else if byte_value > 0 then
Ok(Free(byte_value))
else
Err({})
).pass_to(List.from_iter)
I think I like the for version the best of these
by a small margin
Iterators solve list capacity planning? I haven't learned about iterators since Python.
Ah, I misread what you meant by "make this possible".
No, they don't
They sometimes can guess capacity, but it isn't a general solution
For example keep_oks probably should not guess capacity
Cause it might way over allocate
Last updated: Jun 16 2026 at 16:19 UTC