what if instead of .pass_to(foo) we had just .(foo)? A really cool thing this would enable:
import Instant exposing [hours]
four_hours = 4.(hours)
(as opposed to the currently-proposed 4.pass_to(hours))
how do extra args work?
same as .pass_to
4.(sub(5))?
4.(sub, 5)?
I was thinking the latter, but wow does it look hilarious when you use it with Num.sub :joy:
(either style)
Yeah, I may have picked a particularly bad example function. The second one looks a lot like a tuple which is kinda strange.
comparing to an earlier list of examples:
.pass_to() List.range({ start: At(1), end: Length(total_pages) })
.map(view_page_link)
.pass_to(ul, [class("pagination")])
.() List.range({ start: At(1), end: Length(total_pages) })
.map(view_page_link)
.(ul, [class("pagination")])
yeah looks strange there too
Yeah, I think @Eli Dowling is probably right about the best alternatives to .pass_to() being some sort of extra symbol like .>
List.range({ start: At(1), end: Length(total_pages) })
.map(view_page_link)
.>ul([class("pagination")])
Will look a bit odd and take some time to get used to, but it is just a minor extra visual indicator to note the function is imported instead of being from the defining module of the type.
I was literally coming here to once again say that an operator still seems cleaner than this suggestion! Thanks @Brendan Hansknecht :)
I'm not totally married to .>but I do think starting with a . has a nice sense to it.
I think .>> looks more visually distinct and is essentially the same speed to type.
Some comparisons:
.>
List.range({ start: At(1), end: Length(total_pages) })
.map(view_page_link)
.>ul([class("pagination")])
.do_final(page_thingy)
.>>
List.range({ start: At(1), end: Length(total_pages) })
.map(view_page_link)
.>>ul([class("pagination")])
.do_final(page_thingy)
With a space:
List.range({ start: At(1), end: Length(total_pages) })
.map(view_page_link)
.> ul([class("pagination")])
.do_final(page_thingy)
I'm open to the possibility, but I haven't seen any yet (including these) that seem worth the strangeness budget
these look like Haskell operators :sweat_smile:
(sorry if that was too harsh)
At this point, my gut feeling is that if we want clarity it will be .pass_to and if we want brevity, it will be some sort of extra symbol.
going back to the original example, we could support this:
import Instant exposing [h]
in_24_hours! = || Instant.now! + 24h
I've wanted something like this for units in general, e.g. 42cm, 16px etc. It kinda reminds me of infix operators in that I think certain operations are easier for me to follow when they're written in this style.
if we did that, we'd probably want to change the syntax for typed number literals (probably to methods) to support that, e.g. 42u8 becoming 42.to_u8() but that might be a nice simplification in regardless
I think given we originally had |> which people had to learn as pipe. Replacing it with .> Which people have to learn as pipe is not too bad at all.
I think you've just got Haskell trauma Richard :stuck_out_tongue_wink:
a lot of my aversion to unusual symbols is seeing other people bounce off of Haskell because of the symbols, without ever making it to the semantics
I would argue the issue with symbols in Haskell is mostly when they combine together.
I can read normal Haskell code fine, spattering of symbols, all good.
But when operator precedence rules start coming in and the symbols get dense I feel like I have zero intuitive understanding of what's going on.
(I don't know Haskell but read bits occasionally)
I don't think this has that issue, mostly because the precedence is clear and we don't have any other "function application adjustment" type symbols.
I think most beginners wouldn't notice or care about this any more than |>
@Richard Feldman I don't know if I missed something, but what is || doing in that example??
Oh wait, it's a function declaration. My bad
For the question, if we should use ? or try, the idea was to have both for the moment and experiment with both ways, but that we should remove one of them before a 1.0. (See for example here )
Could we do the same for .pass_to and .>? Have both in the language when static dispatch gets implemented, experiment with both of them, and restart the discussion, somewhere before a 1.0?
I like this idea a lot, and was going to suggest it but didn't like the idea of "two ways to do one thing".
Having this experiment also end at the same time as the try ? experiment makes perfect sense and solves that problem.
Big +1 from me
I still dont see static dispatch and the existing |> operator as at odds with one another, .> is fine but not that much more understandable for someone who hasn’t seen pipelines before and eats weirdness budget for those who have.
I think the visuals when in a pipeline are worse
Just a wild thought here, but what if we could use . as a pipeline? All use cases could be achieved by both inline static_dispatch and the pipeline one, but sometimes the pipeline one could be used for legibility sake:
# lets assume both modules have an `hour` function
import TimeRange
import TimeInterval
# module function with same name
hour : Int -> String
hour = |x| Str.fromInt x
# Inline
timeRange = Now!.to_unix.(TimeRange.hour)
timeInterval = Now!.to_unix.(TimeInterval.hour)
timeString = Now!.to_unix.hour
# Pipeline
timeRange =
Now!
. to_unix
. TimeRange.hour
# Mixing both
timeRange =
Now!.to_unix
. TimeRange.hour
# If for some reason the "Num" module also had
# an `hour` function we could just disambiguate at call site
# hour is defined on module, so it is more specific
# and should take precedence over Num.hour?
timeString = Now!.to_unix.hour
timeFromNum = Now!.to_unix.(Num.hour)
First, a reminder that we're mostly (if not fully) aligned that we'll be using parens for function calls
Yeah, I think PNC _might_ stay around even if Static Dispatch doesn't work out. But others call me out if I read that wrong
And also, that syntax currently means "give me an anonymous function that gets the field abc from a record", and this would make a single space the difference between calling a local function and defining a field getter. That makes it pretty ambiguous in my eyes and I think we should prefer a syntax that's visually unambiguous for someone reading Roc.
Anthony Bullard said:
Yeah, I think PNC _might_ stay around even if Static Dispatch doesn't work out. But others call me out if I read that wrong
You're right it'll probably stay even if SD is rejected because of the phenomenon that people seemed to like Gleam more just for PNC even if everything else was the same
oh - I haven't thought about record accessors syntax. that is indeed confusing with other usages of .xxx .
But to clarify - what do you mean by using parens for function calls ? Oh - you mean that in my example Now!.to_unix.(xxx) the .(xxx) seems like it is the parans of a function call itself?
Or do you mean that all function calls should be using parens, even when there are no extra arguments in the pipeline? Now!.to_unix().(TimeRange.hour()) ?
I think if we are doing this, I'd rather @Eli Dowling 's suggestion (or just keeping Pizza around)
Folks - sorry if this is the wrong thread but I want to understand this.
The whole idea of dynamic dispatch is that by using .someFunction that would be inferred from the module of the current type I'm acting on, right?
And pass_to would be used when the pipelined function is not from that module? So something like:
# I'm ignoring `Num` for the example
(12.12).floor.to_string.pass_to(Parser.parseInt)
# Float
# Float.floor
# Int.to_string
# Parser.parseInt
Or did I get it wrong? (Because I'm thinking about issues like I showcased above when there are multiple conflicting modules that act on the same type with functions named the same)
If my reasoning is correct, wouldn't the pass_to with whatever syntax be at least as common as the inferred dispatch?
Even if a function has zero args, it should have parens
So it wouldn't be Now!.floor.to_string, it'd be Now!().floor().to_string()
Georges Boris said:
The whole idea of dynamic dispatch is that by using
.someFunctionthat would be inferred from the module of the current type I'm acting on, right?
"Static" dispatch and yes.
And
pass_towould be used when the pipelined function is not from that module?
Also, yes. It is for any local or imported function
As mentioned by others, only slightly related to static dispatch, roc is also planning to switch to parens and commas calling syntax. So your code would be
(12.12).floor().to_string().pass_to(Parser.parseInt)
(Because I'm thinking about issues like I showcased above when there are multiple conflicting modules that act on the same type with functions named the same)
So conflicts are essentially the same as today. You have to import something to use it with .pass_to. that means you have the standard conflicts of any other import.
For static dispatch, it is always from the defining module of the type. So it can never have conflicts. There is only ever a single defining module.
Just to show this in the current planned syntax
# lets assume both modules have an `hour` function
import TimeRange
import TimeInterval
# module function with same name
hour : Int -> Str
hour = |x| Str.fromInt(x)
# current accepted
timeRange = Now!().to_unix().pass_to(TimeRange.hour)
timeInterval = Now!().to_unix().pass_to(TimeInterval.hour)
timeString = Now!().to_unix().pass_to(hour)
# Syntax proposed at the top of the thread `.()` instead of pass_to
timeRange = Now!().to_unix().(TimeRange.hour)
# another example syntax
timeRange = Now!().to_unix().TimeRange::hour()
Note, I'm not actually sure how this would work with the .> syntax. How does that syntax deal with module qualifiers?
Pretty sure it'd just be standard dot-qualified module calls
timeRange = Now!().to_unix().>TimeRange.hour()
I thought of .> as just being a Binop like pizza/pipe is today. Like it just being a small change (that I think is a little silly) aesthetically and zero change to structure or semantics.
Hmm....I guess it could be qualified with dot....just looks off.
I think it looks close to normal with newlines
Yes
Just inline that is odd to read
going back to the original idea in this thread, although 4.(sub(5)) looks very silly to me, this actually doesn't:
4.(sub)(5)
revisiting the an earlier list of examples:
.pass_to() List.range({ start: At(1), end: Length(total_pages) })
.map(view_page_link)
.pass_to(ul, [class("pagination")])
.() List.range({ start: At(1), end: Length(total_pages) })
.map(view_page_link)
.(ul)([class("pagination")])
Oh yeah .(name)(...) would be fine
I still prefer pass_to
It looks slightly nicer than .> to me at this point...
I'd prefer pass_to if it was one word
yeah the thing I like about it over .> is that it doesn't look alien to me :big_smile:
no mainstream languages have have .( or .> but . and parens appear next to each other all the time, whereas . and angle brackets basically never do
Also, I think I have seen a similar syntax before....not recalling where though. For using dot method syntax on a function return by another function it would be x.(some_fn())(args)
yeah we already have (foo.bar)(baz) for getting a field off a record which happens to contain a function, and then calling that function
Where some_fn is returning a function that takes an x and args)
and now that you mention it, this is a similar situation :thinking:
I still think .pass_to is more self-descriptive, but I have to admit, whenever I've run into wanting this, I have been annoyed by how verbose .pass_to looks and I try to find some other way to write it :sweat_smile:
Probably because I'm used to space separated Roc, lots of ()easily feel noisy and messy to me
.() looks stranger to me than .> but I’d take either over pass_to
I think the context of calling a lambda in parens like seen with records makes .(some_fn)(args) make way more sense. Makes it easier to explain
Anton said:
Probably because I'm used to space separated Roc, lots of
()easily feel noisy and messy to me
this might sound strange, but I actually feel a lot differently about the noisiness of parens depending on how they're syntax highlighted.
specifically if they're sort of faded out and lower contrast, they don't look as noisy to me. So that's the highlighting scheme I used in the static dispatch doc.
As I said before, for beginners (and possibly for general readability, though not sold on this), I think .pass_to is probably the clearest.
For general use, and getting out of the way while still being delineated from local imports, I think symbols are best here. I think that .> has no explanation except that it kinda looks like |> (which any go away). I think that .() is closer to calling lambda in a record so at least has a bit of precedence.
I think that symbols get out of the way, but add clarity for when you need it. As such, I am pro trying symbols, but I ultimately think it depends on what our goals are for the total beginner experience.
Essentially anyone no matter the language background will figure out/understand .pass_to. with symbols, this will be much more of a guess as to why the symbols are needed and what they mean.
I think Richard said before that pass_to usage does not come up that often, in that case it seems best to go for something slightly more verbose and beginner friendly
It really depends on what percentage of types are nominal types. Also depends on what percentage of types the author has control over. I think that it would come up frequently with Str and List U8
If pass_to is basically treated as a keyword, then migrating to another syntax would be really easy. It seems like an easy thing to start with.
Whenever I write buffer code, I naturally write custom methods that would never be able to use static dispatch. At the same time, they aren't generally plentiful enough to make it worth defining my own nominal buffer type and module that wraps all the list methods I need
So I think there will be plenty of cases where it is common.
Anton said:
I think Richard said before that pass_to usage does not come up that often, in that case it seems best to go for something slightly more verbose and beginner friendly
My understanding was that this thread was started cause it was actually hit pretty commonly in practice when working on the real-world example with future roc syntax
Anyway, I feel like we have collectively spent a ton of time debating these alternative syntaxes. I personally feel we should just pick one and try it out. It should be pretty trivial to change later.
if we're going to try one out, I'd like to try .(foo)(bar, baz) because that way we can get data on how confusing it is (or isn't) to beginners. If it's a problem in practice, we'll find out! :big_smile:
How would the .(foo)(bar, baz) option look if you wanted to put a lambda in the pipeline? Something like this?
.(|x, y| x + y)(4)
Sorry to add another option to the mix, but I wonder what you think of this option:
List.range({ start: At(1), end: Length(total_pages) })
.map(view_page_link)
.|links| ul([class("pagination")], links)
So instead of piping directly into a function, you define a lambda that takes the current pipeline argument and then you can write a function body.
My sense is this would be reasonably intuitive to beginners, works well in a broader set of cases (i.e. when the arguments of the passed-to function aren't pipeline-perfect), but it's a bit more verbose when you want to pass to a function that does take the pipeline parameter as the first argument.
Jasper Woudenberg said:
How would the
.(foo)(bar, baz)option look if you wanted to put a lambda in the pipeline? Something like this?.(|x, y| x + y)(4)
fwiw this seems pretty readable to me! though passing extra variables as arguments to lambdas seem a bit off. So maybe we could omit the 0-arg () when passing a lambda? .(|x| x + 4)
I think it's quite understandable as to what's happening, but I think it's not always desirable to put the entire body of a function into a single pipeline. My experience with Rust pipelines has made me feel that most usages of this would be better as an assignment to a variable that gets passed to the intermediate function. The cases where long pipelines are still good are things like iterators, which are lazy. Those tend to not need this syntax. Eagerly-evaluated pipelines should be broken up IMO.
I tried out .(foo)() as opposed to .pass_to(foo) and one thing I didn't like about it was the specific scenario of having to use () at the end when it's only taking one argument
it seems like if we just said .(foo) and .(foo)() are equivalent, it's fine?
obviously it would logcally follow that if val.(foo)(bar, baz) and val.(foo)(bar) work, then val.(foo)() should work too, but it just feels unnecessarily noisy to have to put the extra () on the end
It shouldn't conflict with 1-tuples for custom types
when val.(foo) can't really meany anything else anyway
would this work? .(foo(1, 2)) or .(foo 1 2) as foo(1, 2). Sorry if this has already been discussed
I feel like .foo() and .(foo)() matching makes a lot of sense. So I much perfer that. It is consistent and simple as a rule. We don't need magic for everything to remove every level of noise.
we did - it felt weird because theoretically you should be able to put any expression inside the parens
I'd rather start with requiring the trailing () but would be open to experimenting with desugaring val.(foo) as val.(foo)()
so like .(|x| x + 1) (or whatever
so then .(foo(1,2)) would be actually calling the expression foo(1, 2)
I think Ayaz is suggesting abc.(foo(1, 2)) would go to foo(abc, 1, 2)
right
but if we do that, then abc.(|x| x + 1) no longer makes sense because we're not treating what's inside the parens as a normal expression
it's something special
Which assumes that the abc.(<expr>) isn't allowed
We currently do that for record builders
example from the realworld code base that made me think of this:
Auth.authenticate(req, now!()).and_then!(handle!).pass_to(to_resp)
Auth.authenticate(req, now!()).and_then!(handle!).(to_resp)()
Auth.authenticate(req, now!()).and_then!(handle!).(to_resp)
intellectually, I understand that the middle one is the simplest and most consistent, but I like reading it the least of these three :sweat_smile:
Maybe we shouldn't?
I think making it be a normal expr makes the most sense, and is also more useful
sometimes in pipelines today we do |> into a lambda, and .(...) allowing an expression would mean you could still do that naturally
So otherwise would be:
Auth.authenticate(req, now!()).and_then!(handle!) |> to_resp
or
to_resp(Auth.authenticate(req, now!()).and_then!(handle!))
yeah
what if we just disallow chaining like this? require assigning to an intermediate variable and see how painful is. personally i try to avoid a lot of chaining anyway because hard for me to understand what the intermediate outputs are, and i think it's sometimes a signal that things need to be factored out
I strongly agree. I think my Rust code is better because I can't do this.
it's certainly possible in the static dispatch world...this is actually the only place in the whole code base where I ended up wanting to use it :big_smile:
Imagine chaining one more function:
Auth.authenticate(req, now!()).and_then!(handle!).pass_to(to_resp).exit_code()
Auth.authenticate(req, now!()).and_then!(handle!).(to_resp)().exit_code()
Auth.authenticate(req, now!()).and_then!(handle!).(to_resp).exit_code()
to_resp(Auth.authenticate(req, now!()).and_then!(handle!)).exit_code()
(Auth.authenticate(req, now!()).and_then!(handle!) |> to_resp).exit_code()
Though I wouldn't be surprised if structurally-typed values made a pass_to equivalent necessary
I do think it's a fair point to say "let's intentionally hold off on implementing anything for it, and see what use cases come up"
like we can always add it later, and then have more examples of how different syntaxes could look
obviously this is a thing we know we can do, and we have a few different ideas about how it could look
and the easiest way to evaluate how worthwhile it is would be to just try living without it and see how much organic demand there is for it in practice
It's just sugar, it wouldn't complicate canonicalization or anything else
right
Especially if we leave in |> for a bit. Any uses of |> that say around would be cases that might match better with one of these syntaxes
Otherwise, it will just be cases of more complicated parens or splitting into many local vars in a way that feels unnecesary
Another alternative could be
List.range({ start: At(1), end: Length(total_pages) })
.map(view_page_link)
.ul(_, [class("pagination")])
_ is a placeholder to insert the value to be piped, it can be any of the positional argument._ is the visual differentiator indicating to the reader that this is not static dispatch but piping to ul functionSomething that I've seen in common between this discussion and the purity inference one is a bias for "forward-reading" features. Both val.>func(args) and val.(func)(args) are visually distinct from the method call syntax at the front of the function call. This makes it really easy to figure out whether a method is being called without having to "gather context" first.
As much as this feature has seen good success in Gleam, and I like the aesthetic of it, I think it hurts readability when you have bulky arguments.
Oh wow, I hadn’t seen the val.>func(args) suggestion. I know this wasn’t your point Sam, but I just gotta say I love the way that reads. Very clear what it is doing, and to me I like the uniformity of keeping the parens around the args immediately following the func name, as opposed to being interrupted by the parens around the func. Plus it keeps some of those roc/elm aesthetics I love…
Hey man, there are a lot of different opinions here, none of them better or worse
I think we're planning on starting with val.(func)(args) because enough people like it including Richard to warrant a test drive
But it's a pretty simple change to try something else
Secret option C: we implement both syntaxes
Haha yep, we’re all just here for the love of
!
I was talking with José Valim about this topic (and a bunch of others) on the podcast recently, and one thing that came up was that if we went with the maximally concise version of this, you could do:
yesterday = 1.(day).ago!()
later = now!() + 8.(hours)
after importing the day and hours functions unqualified
that assumes that if you have a 1-arg function you can call it with arg.(fn) and not arg.(fn)()
otherwise it would be:
yesterday = 1.(day)().ago!()
later = now!() + 8.(hours)()
with the maximally concise version of this
What would this look like for functions with multiple arguments?
:thinking: I just thought of an interesting argument for putting extra args inside the parens, like this:
arg1.(fn, arg2, arg3)
...is that you could think of it as "in this syntax, you swap the thing before the . with the first thing in the parens, so this desugars to
fn(arg1, arg2, arg3)
I'm imagining an animation where they just swap places and the . disappears, and in my head it makes it really easy to understand :big_smile:
another point in favor of that syntax is that it is exactly as concise as method calling style
the . is just in a different position
for context, we were talking about how people like to "add methods to existing types" (to use Rust terminology, but also applies to languages with classes)
and a downside of allowing anything like that (which I don't think we should support) is that it makes it harder to look at code and understand where the implementation is coming from
but an upside is that it makes the code look more stylistically consistent
a nice thing about .( is that the difference is very small, so it's different enough to tell that it's not static dispatch but not so different that it looks like a radical departure from normal style
actually considering ago! would be in the platform's Clock module, not the platform-agnostic Duration module, it would be:
import Clock exposing [ago!]
import Duration exposing [hours, day]
yesterday = 1.(day).(ago!)
later = Clock.now!() + 8.(hours)
What about the formatting of multiline calls with this?
chain =
item
.call(
with,
lots,
of("abc"),
if args then 123 else 456,
)
.(
local_func,
other,
set,
of(args),
)
We'd maybe want
chain =
item
.call(
with,
lots,
of("abc"),
if args then 123 else 456,
)
.(local_func,
other,
set,
of(args),
)
both seem reasonable, although I like the second one better
I'd still prefer
chain =
item
.call(
with,
lots,
of("abc"),
if args then 123 else 456,
)
.(local_func)(
other,
set,
of(args),
)
because it looks like a function call, and I think it's totally fine to desugar
x = 123.(func)()
from
x = 123.(func)
But I'm okay with "the second one" above
Personally, I find the .( a little unintuitive. The notation is so concise and similar to accessing a static dispatch method that 1.(day) reads to me like 1.day. When you mix in extra args, with the first "argument" in the parens actually being the function, it gets really unintuitive.
I think this may just be my ADD which makes me have to really slow down and process that, and maybe I would get used to it, but I feel like it would drastically reduce the speed I can scan and comprehend code. It also seems like it could be a bit confusing for newcomers to remember that the first element in the parens is actually the function.
I still find item.>fn(arg1, arg2) much easier to scan and model mentally than item.(fn, arg1, arg2).
yesterday = 1.>day().ago!()
later = now!() + 8.>hours()
Would this work?
I really can't stand .> :sweat_smile:
every time I see it I have this visceral "no way, never" reaction
I really try to be open minded about ideas where I have a negative first reaction because I've come around to a lot of them over the years, but I have zero percent come around on this one :sweat_smile:
Do we need to reinvent the wheel? What about |>? Maybe there's a blocker I'm not thinking of, but it actually seems like this should work fine with SD/PNC... Sorry if I'm rehashing conversations which have already happened.
I guess the .( was for the sake of easy auto complete...
the problem with |> is that you can't keep chaining things after it
really only works at the end of a chain
I definitely prefer arg1.(fn)(arg2, arg3) over arg1.(fn, arg2, arg3). I think it is much clearer to read and much closer to a standard method call. The other still looks like messed up tuple syntax to me.
I would be fine with arg1.(fn) as a sugar for arg1.(fn)().
Shouldn't type inference with static dispatch be able to infer all the following types?
res = 123.to_str
|> to_utf8
.map(|n| -> "${a}")
|> join_with(", ")
yeah but try putting that on one line :big_smile:
Touché
yeah that's the problem case
res = 123.to_str() |> to_utf8().map(|n| -> "${a}") |> join_with(", ")
Should be solved by adding the parens, right?
to me, to_utf8().map(...) reads like its own standalone thing
we could have the rule be that it doesn't, but at that point we have strangeness with either |> or .(
That's fair...
I'll admit that is pretty weird when I look at it.
I will say that I'm a big fan of 4.(hours) specifically
I might be alone on that in this thread, but I know a lot of people who have done a bunch of Ruby and love that (and miss it in other languages)
like specifically in the conversation with José he brought it up out of nowhere as a motivating example of wanting to "add methods to types after the fact" because it's such a beloved notation
for durations in particular, although I also like it for some units of measure (although it has limitations compared to an actual first-hand units of measure system like what F# has)
I remember finding it very confusing in my early Ruby days. Like, I knew that if I tried I'd probably be able to reverse-engineer where I would need to put ., _, , () in the quasi-english invocations to get it to work, but instead I would just throw some options to the wall and hope one sticked.
I still have that occasionally, wondering whether it's 4.hours.from.now or 4.hours.from_now for instance.
It's probably better with types though.
Ruby looks great, but is the Wild West
The types are definitely the difference
Richard Feldman said:
I will say that I'm a big fan of
4.(hours)specificallyI might be alone on that in this thread, but I know a lot of people who have done a bunch of Ruby and love that (and miss it in other languages)
like specifically in the conversation with José he brought it up out of nowhere as a motivating example of wanting to "add methods to types after the fact" because it's such a beloved notation
That makes sense. I can appreciate that. I think the val.(fn, arg1, arg2) is where I struggle. And honestly, even there, I can appreciate the conciseness. It currently feels difficult for me to quickly parse that now (especially in a long chain), but maybe with time it would become second nature.
As I think about it, with a good LSP that could highlight fn differently from arg, this wouldn't be bad at all. I'm more concerned with the reading than the writing, but the LSP can help there...
Is val.(fn)(arg1, arg2) easier to parse for you? I find it way easier to parse.
Yeah, in a world sans .>, val.(fn)(arg1, arg2) is definitely my preference.
yeah I also prefer that syntax for multi arg
As far as single arg function goes val.(fn) does look a lot cleaner than val.(fn)()...
Richard Feldman said:
after importing the
dayandhoursfunctions unqualified
This sound a lot like a proposal I made recently....
similar for sure!
but with the significant difference of there being a syntactically visible way to tell whether it's resolving based on static dispatch or local scope
something I realized that I haven't seen come up yet, but which is definitely a case where something along these lines would be nice, is where you're converting from one type to another, e.g.
my_list
.map(|arg| (arg, foo(arg)))
.(Dict.from_list)
(assume there might be a bunch more steps in that pipeline)
sometimes you'll happen to be able to do .to_dict() (or whatever) but other times the thing you're converting from doesn't know about the thing you're converting to (such as creating a dictionary from a list)
at which point a way to pass what you've been building up into a plain function becomes nice
I guess another thing that's actually really nice here is being able to do .(Ok)
at the end of a bunch of operations
in the realworld code base one of the things I was trying out was allowing .Ok()
which was nice in a couple of places
but if we had .(Ok) it would be equally concise and one less concept
val.(fn)(arg1, arg2) wasn't obvious to me at first, but with context I get it.
Is val.(fn(arg1, arg2)) reasonable? Perhaps it'd be easier for a newcomer to understand. It could be extended to allow a lambda in there if we wanted val.(|argX| fn(arg1, argX, arg2)). Sorry if this is out of touch, I'm not fully up-to-date with the direction.
it's an option, although my default assumption is that anything inside parens is an expression that gets evaluated on its own
so I'd assume val.(fn(arg1, arg2)) would evaluate fn(arg1, arg2) on its own
I agree, that's confusing. I think both options are a bit confusing, though, but val.(fn)(arg1, arg2) has the benefit that when I read it I go "whoah, something funky is going on with that (fn)".
What do you think of a Ruby-like solution such as val.(fn(_, arg1, arg2))?
fn is not evaluated on its ownFor non-Rubyists, Ruby would say val.then { fn _1, arg1, arg2 } or you could use it instead of _1 as per their latest release. We'd use _ instead to mirror a type hole (type hole/variable hole).
I think it's a can of worms I'd like to avoid opening right now :sweat_smile:
having _ as a placeholder value in expressions (as opposed to patterns) can be taken really far
like I think in Scala you can do stuff like (the equivalent of) when _ is which would make the entire when expression become a lambda which takes one argument and puts it where the _ is
so I've been trying to avoid using _ for things in that direction because of the "where does it end?" question
Indeed, that's totally fair! I would personally probably like val.(fn)(arg1, arg2) and it'd be good to try.
Some seem to prefer pass_to. I don't have strong feelings. Here are one-word alternatives which I didn't see while reading up on the conversations:
to: works great for ergonomic APIs like 1.to(day). Can be confused with conversions (to_str) but should never conflict with a conversion, I think?pipe: I'm sure this must've been suggested, but I didn't find itthen: used by Ruby, but takes a lot of "space" as in APIs might want to use it, async etc. However, its use here _would_ be for chaining, which is the closest thing to the word, so :man_shrugging:-- to
yesterday = 1.to(day).ago!()
later = now!() + 8.to(hours)
Auth.authenticate(req, now!()).and_then!(handle!).to(to_resp)
blocks = bytes.len().to(List.with_capacity)
-- pipe
yesterday = 1.pipe(day).ago!()
later = now!() + 8.pipe(hours)
Auth.authenticate(req, now!()).and_then!(handle!).pipe(to_resp)
blocks = bytes.len().pipe(List.with_capacity)
-- then
yesterday = 1.then(day).ago!()
later = now!() + 8.then(hours)
Auth.authenticate(req, now!()).and_then!(handle!).then(to_resp) -- API conflict, one of these usages has to give
blocks = bytes.len().then(List.with_capacity)
Sorry if these were considered already, I just didn't see them, even with searching. No need for reasoning etc. if they're not applicable, just thought I'd share them in case it helps!
something I'm noticing is that the conciseness of .( makes it useful in more situations, e.g. in the revised tutorial I have in progress for the static dispatch world, I originally wrote this:
get_input! = ||
echo!("Enter the number of birds:")
birds = input!().to_i64()?
echo!("Enter the number of iguanas:")
iguanas = input!().to_i64()?
{ birds, iguanas }
I realized I was missing an Ok on the return value, so I changed it to this:
get_input! = ||
echo!("Enter the number of birds:")
birds = input!().to_i64()?
echo!("Enter the number of iguanas:")
iguanas = input!().to_i64()?
Ok({ birds, iguanas })
this is correct, but I was a little sad about how the original focused on "here's the thing we're returning" whereas the correct one focuses on "we're returning Ok" which is the least-interesting part about that last line
but then I tried it like this:
get_input! = ||
echo!("Enter the number of birds:")
birds = input!().to_i64()?
echo!("Enter the number of iguanas:")
iguanas = input!().to_i64()?
{ birds, iguanas }.(Ok)
I like this! It makes the Ok sort of an afterthought (a parenthetical afterthought, in fact!) - which I think is appropriate given how this function is written
I never used to do |> Ok like this because it introduced more symbols and made it longer, but the property of .( that it has the same number of symbols as a direct call means I'm more open to something like this
notably, .pass_to( doesn't have that property
random thought we haven't discussed: since passing exactly 1 arg seems to be the most common case, and all the designs for passing multiple args seem awkward in different ways, one possible design would be to only have syntax for 1 arg, and then if you want multiples you use a lambda:
str.to_i64()
.(|result| foo(result, arg2))
as opposed to:
str.to_i64()
.(foo)(arg2)
or
str.to_i64()
.(foo, arg2)
it's more verbose but doesn't require separate syntax
I found the afterthought tag most useful with nested wrapping. Happens pretty often with things like walkUntil, but also comes up in other locations.
val
.transform()
.(Ok)
.(Step)
126 messages were moved from this topic to #ideas > static dispatch - partial application syntax by Richard Feldman.
Richard Feldman said:
random thought we haven't discussed: since passing exactly 1 arg seems to be the most common case, and all the designs for passing multiple args seem awkward in different ways, one possible design would be to only have syntax for 1 arg, and then if you want multiples you use a lambda:
str.to_i64() .(|result| foo(result, arg2))
related to this, I am legit struggling to come up with a non-contrived example of when this comes up in the static dispatch world :sweat_smile:
can anyone think of one?
that is, where we're calling a function that takes 2+ arguments and it would make more sense to use .( than a method call
arg reordering
:thinking: for example?
Oh, re-reading this, I guess you're asking for an example of when you'd need to calculate a function?
yeah like basically what's an example that could plausibly come up in real-world Roc code where this syntax would come up
for using .( with anything other than a function that takes exactly 1 argument
I need to clarify the syntax first
another way to put it: what's a situation where today we'd do arg1 |> fn arg2 (or even more args than 2)
and we wouldn't just use method-style calling syntax in the static dispatch world
Oh
data =
Http.get!("api.com")
.(decode_with)(Json.utf8)
Is this not what you're talking about?
It's all blending together at this point
Not sure if I can think of an example of what you're looking for
Http.get! accept the decoding format as its second argument :thinking:
our api does, yes
actually that works as an example if you're building up the URL
foo
.bar()
.to_url()
.(Http.get!)(Json.utf8)
Ideally, all types would have the right methods. pass_to is the stopgap for holes in APIs in my eyes. AKA imagine if HTTP didn't have the right type
and if we didn't have separate syntax for it, this would be:
foo
.bar()
.to_url()
.(|url| Http.get!(url, Json.utf8))
I think the argument for that being the best design is that if this comes up super rarely, both arg1.(fn, arg2, arg3) and arg1.(fn)(arg2, arg3) are more surprising to read than arg1.(|arg2, arg3| fn(arg2, arg3))
because if you're used to seeing the 1-arg form on a regular basis (e.g. I think .(Ok) will come up often)
then you read that syntax and immediately understand it
it would be annoying if it came up all the time, but if it comes up very rarely, then maybe it's best to optimize for the syntax not being surprising as opposed to being more concise
Like Ayaz, my worry is that this will be used often at all. I can't think of an example that is improved by this instead of just having smaller pipelines and multiple assignments. It's good to try stuff, but seeing foo.(|arg| bar(arg, 123)) makes me think a Roc with any usage of this will always be worse than a Roc without it at all
I guess a variation of that idea could be that .pass_to exists, since I don't think arg1.pass_to(fn, arg2, arg3) is surprising, but then we have arg1.(fn) as a shorthand for arg1.pass_to(fn) because it comes up so often
I actually think that I'd be close to wanting it even if it was just for 4.(hours) and .(Ok) on their own :big_smile:
when doing the realworld app I independently made up two other syntaxes, namely .Ok() and 4h, just because I wanted to be able to do something along those lines in those places
obviously lots of languages get away with not having something like this, so it wouldn't be the end of the world if we didn't have it
The single arg version makes sense for the reasons you say
Anything more complex is the problem
yeah
so maybe we just try it with single-arg being the only thing that's allowed, and see how it goes
That would be great!
We already do that for record builders
Seems good there so far
ha, we could refer to .( as "the empty method"
because it's like .foo(bar) with the foo deleted
and in this design it's very easy to explain: a.(b) means "pass a to b"
Lambdas would still work with this?
a.(|b| fn(b))?
I'm voting that they shouldn't
@Sam Mohr I don't see why they shouldn't?
If we have a lint in the lsp or something saying it is unnecessary, I don't see the problem
Luke Boswell said:
a.(|b| fn(b))?
Let me expand that, I was thinking a.(|b| fn(b, c, d))?
@Kilian Vounckx the goal is to disincentivize Roc devs to write code that is hard to read for others. If I come into a new codebase and see this, especially without knowing Roc, I'll be pretty confused.
In general, the longer a chain of methods is, the harder it is to tell what's happening in the middle of it. This feels like a way to allow method chains to get even longer than they already would
I wonder if other people have experience with that, or disagree that long method chains are usually an anti-pattern
I'm thinking of this chain I read today (source):
let mut captured_symbols: Vec<_> = new_output
.references
.value_lookups()
.copied()
// filter out the closure's name itself
.filter(|s| *s != symbol)
// symbols bound either in this pattern or deeper down are not captured!
.filter(|s| !new_output.references.bound_symbols().any(|x| x == s))
.filter(|s| bound_by_argument_patterns.iter().all(|(k, _)| s != k))
// filter out top-level symbols those will be globally available, and don't need to be captured
.filter(|s| {
let is_top_level = env.top_level_symbols.contains(s);
references_top_level = references_top_level || is_top_level;
!is_top_level
})
// filter out imported symbols those will be globally available, and don't need to be captured
.filter(|s| s.module_id() == env.home)
// filter out functions that don't close over anything
.filter(|s| !new_output.non_closures.contains(s))
.filter(|s| !output.non_closures.contains(s))
.map(|s| (s, var_store.fresh()))
.collect();
This would be really hard to read without comments, since all methods are just .filter(...)
If there were intermittent variables, this wouldn't need all of these comments IMO
I think I would write this with a single filter using a longer closure. Using var names within the closure. But I would still need that syntax even for a single filter
Yeah, that filter chain should be replaced with a single filter. I could not trust even Rust's compiler to inline all those functions even if an Iter is a "zero cost" abstraction.
I think this readability issue is caused by explicit types, because cost of extracting filters into separate functions is higher as you need to annotate every single function.
With implicit types and pseudo-roc syntax it could have been
captured_symbols = new_output
.value_lookups()
.discard_if(|s| s == symbol)
.discard_if(.(boundHereOrDeeper)(new_output, bound_by_argument_patterns))
.discard_if(.(isImportedOrTopLevelSymbol)(env))
.discard_if(|s| new_output.non_closures.contains(s))
.discard_if(|s| output.non_closures.contains(s))
.map(|s| (s, var_store.fresh()));
.....
boundHereOrDeeper = .....
isImportedOrTopLevelSymbol = .....
I might have gotten some syntax wrong here, but You get my point. I think that having ability to implicitly deduce types encourages small descriptive functions (instead of inline code that requires comments).
Personally that's how I'd code it, if something is not obvious at first look then extract it into separate 'private' function with descriptive name.
I've used discard_if here as I think some examples in Roc used keep_if, I'm not sure if there's supposed to be counterpart, but that makes it cleaner as I don't need to revert all booleans in filters.
Even if the code you gave wasn't using many separate filters it would still be more readable to me if some parts would be extracted into separate functions so I don't think that it's chaining that's the issue here, but I guess that comes down to preference.
Luke Boswell said:
Lambdas would still work with this?
a.(|b| fn(b))?
It would be super weird to be like "you put an expression in here...as long as it's not a lambda! That one type of expression is banned in only this place."
Everywhere else in the language, anywhere you can put an expression, you can put a lambda, and I don't think we should change that.
Personally the full syntax is much more interesting to me than only the single arg version
I regular hit code in other languages where I am wasting time with nesting and parens that would be resolved by this syntax
Bad code (like infinite filter chains) can be written in any language or syntax
I don't think that is a good example of where the x.(fn)(y, z) fails
It literally isn't using the sytac at all. It is using standard method dispatch
Just standard method syntax also makes sense to me. I think I'm mostly worried by lambdas
I have written plenty of transformation pipelines on lists that would not work with static dispatch, but currently work with |> and would work with .(fn). I guess we could keep static dispatch and |> but that feels less nice than just committing to the new syntax.
x.(fn)(y, z) is the exact same thing as a method call, but with a local/imported function
If we allow it so long as fn is just an (optionally Module-qualified) variable, it makes sense to me (e.g. exactly what we do for record builders)
Once you do x.(|n| fn(foo, n))(y, z)...
I always hate when I have to do that in pipelines. It is the ugliest, but I sometimes have to do it today
We shouldn't allow it IMO
But I think blocking the syntax for a single use that is the worst use and is probably pretty unlikely to be used is a mistake
I think we can allow the syntax, but only for named functions
It seems like a pretty safe compromise
I very strongly think that lambdas should be allowed in there
I think the bar for ever saying "you can use any expression in this position except for _____, which is banned" should be astronomically high and this is not even on the same planet that bar is on :sweat_smile:
that said, I appreciate the pushback about it (not) being a reasonable way to address the situation where you want to apply multiple arguments
I guess I'll have to hope that anyone that puts a lambda in there thinks it looks as awkward as I think it does, and that pressure makes this usage of the syntax rare
to be fair, lambdas in pipelines are already rare today
edit: ahh I tried to fix a typo on mobile and hit delete instead of edit, and there's no confirmation or undo button 🤦🏼♂️
but thankfully, Sam rewrote it below! :point_down:
Option 1 looks closest to a function call
Option 1
Now that partial application is off the table and .> looks bad enough to enough people to not be an option, these are the remaining options for this method-style local function call syntax:
foo.get_bar().(local_func)(arg1, arg2)foo.get_bar().(local_func, arg1, arg2)foo.get_bar().pass_to(local_func, arg1, arg2) (and maybe we allow .(...) as well as a shorthand?)are there any other options we haven't talked about that could be worth considering?
Sam Mohr said:
I guess I'll have to hope that anyone that puts a lambda in there thinks it looks as awkward as I think it does, and that pressure makes this usage of the syntax rare
At a minimum if people use lambdas, I would generally expect them to be single arg lambdas. The rest would be able to be captures of the function directly. So no need for other args. I expect using lambdas and adding extra arg to be exceptionally rare. Cause you can always be less verbose by using captures instead.
foo.get_bar()..local_func(arg1, arg2)Probably a non starter since it doesn't take lambdas, and also conflicts with list spreads, but it's been mentioned before
That's interesting. The only language I know with that syntax is Dart - which uses it for a very different purpose.
Which is "call a void method on an instance, but return the instance"
Not really a thing in FP, yeah
I think long chains of functions are a good fit for Roc, because it fits with ref counting. In the example from @Sam Mohr I can see without reading the code, that the argument, that gets passed around, has an ref count of 1. (maybe there is a copy on the first method call, but not on any other).
If the code would use more intermittent variables, I would need to read the code very carefully to see, if they are used more then once.
Since Roc makes the bet, that most of the time, variables have a refcount of one, the syntax should make it clear, when this bet is off.
I think, I am wrong:
["long_string1", "long_string2]
.map(|e| (e, e))
both strings will get a ref count of 2.
Instead of providing an alternate syntax for invoking methods of a module and methods intended to operate on a module, why not offer the ability to specify methods should be invoked using member syntax at defn time (sometimes called “extension methods"). This creates a pit of success by leading developers to use chaining for when it is most appropriate: a series of parameterized transformations on a single subject. However as soon as an operation involves multiple subjects of equal importance, the developer is forced to break out of the chain and pass them as arguments.
# imagine map/zip was not already defined in List module.
map = |self, fn| …
zip = |left, right| …
# use of chaining where appropriate
let x = [1,2,3].map(|elem| elem + 1)
let y = [4,5,6].map(|elem| elem - 1)
# zip does not favor one list or another, and so we don't try to chain.
let z = zip(x, y, |left, right| left + right)
Extension methods would not have access to hidden members of opaque types. They could also be exported, making it easy to provide methods for modules which were not considered by the owner of the module (ex. think itertools). For prior art, you can find use of extension methods in both C# and Kotlin. IMO they have gently encouraged a more functional style while allowing code to appear OO.
BTW love the proposed "..." syntax. I was just abbreviating before I realized that the example might actually be valid Roc code in the future :laughing:
My worry with extension methods is that they are syntactically identical to functions at the call site. The benefit of val.(local_func)(arg1, arg2) is that it's distinct from a method call without knowing even its type signature
If we could find a way to make extension methods syntactically distinct at the call site, then they could be an option here.
Though I can definitely see a Roc where now everything is desired to be written as either an extension method or custom type method, except for maybe constructors.
Meaning, y'know, if we can make normal functions Just Work like extension methods via val.(func)(...), what's the need for extensions methods?
Oskar Hahn said:
I think long chains of functions are a good fit for Roc, because it fits with ref counting. In the example from Sam Mohr I can see without reading the code, that the argument, that gets passed around, has an ref count of 1. (maybe there is a copy on the first method call, but not on any other).
While not guaranteed, I do think this will be true most of the time
One thing I like about val.(func)(args) is that auto-complete could automatically populate (func) after you type ., as long as it's a function in scope that has the right first arg type - and that auto-complete works the same way as it would for a static-dispatch function. That's not really the case for .(func(args)) or .pass_to(func, args) since in both of those cases the auto-complete should also be inserting the close-paren after your cursor
Sam Mohr said:
foo.get_bar()..local_func(arg1, arg2)Probably a non starter since it doesn't take lambdas, and also conflicts with list spreads, but it's been mentioned before
Maybe it could take lambdas if you wrap it in parens (as normal order of operation parens, not any special function syntax):
foo.get_bar()..local_func(second_arg)
foo.get_bar()..(|result, second_arg| expression)(second_arg)
foo.get_bar()..(higher_ordered_function(f))(second_arg)
In cases where you do need parens, it's only marginally clunkier than the leading option .(arbitrary function expression)(), and it doesn't require any new special case uses of parens, and it make the common case ..local_func() much simpler.
Are there cases where it's ambiguous with list spreading? Wouldn't that always have a space or comma before the .., like [a, ..b]?
I think it's more subtle than foo.(func)(args), but foo..func(args) has fewer parens, and I think anything we can do to avoid our Giles Corey crushing to death by parens is a good thing
Yeah, it'd be fine for list spreads, I was thinking of ranges, a la 0..10 which would make an iterator over integers
And it looks better than foo.>func(args)
and I think anything we can do to avoid our Giles Corey crushing to death by parens is a good thing
Maybe... It might be a good thing that it is parens heavy. It really shouldn't be used everywhere, but it is nice every once and a while. So having some natural avoidance due to parens aesthetic may be a good thing.
@Brendan Hansknecht Why shouldn't it be used everywhere? I currently do a lot of |> foo |> bar chains.
I'm actually pretty ok with it being used everywhere. I think it is preferable to anchor to static dispatch though and use this more sparingly
That said, I also add pipes all over the place, so maybe this would be everywhere
Richard Feldman said:
are there any other options we haven't talked about that could be worth considering?
Did we rule out .$ at some point after here?
foo.get_bar().$local_func(arg1, arg2)(I tried to link to my message that says "I believe with $s it would look like dot-dollar-with-nonrepeated-qualifiers" with the :money_face:s under it)
I slightly prefer this over Option 1, as it's consistent with our use of $ as an expression injector for strings. We could even allow expressions/lambdas with added parens when it's more than an existing def:
foo.get_bar().$(make a func)(arg1, arg2)
Sorry, not injector - interpolator. pass_to does feel like "method interpolation".
Should only one argument in passed lambda be allowed? So the other potential args have to come from the scope straight to the lambda body. To reduce indirection. In other words, should it be allowed to have another pair of parens immediately after the passed lambda?
a.(|x, y| y.div(x))(b)
vs
a.(|x| b.div(x))
So if you want to be abstract - go implement a local function, otherwise only straightforward lambdas are allowed. Although lambda syntax for the special case feels a bit redundant with such a constraint
Richard Feldman said:
I think the bar for ever saying "you can use any expression in this position except for _____, which is banned" should be astronomically high and this is not even on the same planet that bar is on :sweat_smile:
I suggested similar earlier, but we thought it was a premature restriction
JanCVanB said:
Richard Feldman said:
are there any other options we haven't talked about that could be worth considering?
Did we rule out
.$at some point after here?
foo.get_bar().$local_func(arg1, arg2)
Bump for my re-proposal of "method interpolation" syntax
I'm a fan because less parens
maybe it's my Perl experience with identifiers that start with $ but when I look at that I see a method call to something named $local_func :sweat_smile:
I've basically never used Perl, I take it as a blessing
foo.(local_fn)
Is one (:wait_one_second:) character less than this
foo.$local_fn()
:grinning_face_with_smiling_eyes:
But
foo.(local_fn)(bar)
is 1 more character than
foo.$local_fn(bar)
So it may balance out
That said, I still prefer foo.(local_fn)(bar)
That said, I feel like I really need to type out a project with both syntax to see how it feels in practice
I like to think about .() as of basic syntax for piping. So you can expand the brackets in foo.(Foo.method)(bar) and get foo.method(bar).
If we're talking about .$, I'll keep plugging my favorite alternative ... foo..local_fn() is also one more character than foo.(local_fn), but I think it will be even easier to type and to read. It also still supports foo..(Module.fn)() or other complex expressions if needed.
hm, so instead of this:
time = 4.(hours).(ago!)
...it would be this?
time = 4..hours()..ago!()
and then instead of:
foo
.bar()
.to_url()
.(Http.get!)(Json.utf8)
...it would be:
foo
.bar()
.to_url()
..Http.get!(Json.utf8)
I'm trying to think of a tie-in between this use of .. and the use of it in records and tag unions
something like it means "merge these" maybe?
or like "combine these" or "concatenate these" or something
in this example, I want to call it the "dramatic pause" operator :big_smile:
time = 4..hours()..ago!()
Just for side-by-side... (not disparaging)
time = 4.$hours().$ago!()
foo
.bar()
.to_url()
.$Http.get!(Json.utf8)
main! = |_|
"./input.txt"
.(Path.from_str)
.read_bytes!()?
.(Foo.from_bytes)?
.(transform)(2, Much)
.to_bytes()?
.(Path.write_bytes!)(Path.from_str("./output.txt"))
Stdout.line!("🥳 See ./output.txt")
main! = |_|
"./input.txt"
..Path.from_str()
.read_bytes!()?
..Foo.from_bytes()?
..transform(2, Much)
.to_bytes()?
..Path.write_bytes!(Path.from_str("./output.txt"))
Stdout.line!("🥳 See ./output.txt")
main! = |_|
"./input.txt"
.$Path.from_str()
.read_bytes!()?
.$Foo.from_bytes()?
.$transform(2, Much)
.to_bytes()?
.$Path.write_bytes!(Path.from_str("./output.txt"))
Stdout.line!("🥳 See ./output.txt")
(note: we would color $ however we want) (also I only want editor color themes that fade parentheses to like gray)
I really look forward to the days post-syntax-rework when we can just say '''roc and have it look great every time.
I think I've come around to preferring ..
I very much like that it's obvious how the multi-arg case works
and even once you know what the syntax is, both ..foo() and ..foo(bar) look equivalently nice, whereas .(foo) looks way better than .(foo)(bar)
(I'm just not a fan of .$ - sorry Jan!)
Do we use ... anywhere? Should that be our spread operator instead?
we plan to: #ideas > Adding an ellipsis `...` keyword
Ohhh right nvm
I think there's some symmetry to having them all use .. though
I haven't figured out quite how to express it
but like, there's some commonality in [a, b, ..c, d] -> and a..b(c, d) in that we're specifying how certain ingredients get combined together to form the final thing we want (a list in the first case and a function call in the second case)
I wonder if we should revisit consensus building on #ideas > static dispatch - dispatch on return types because it could influence topics like this (unless we achieved consensus - I'll re-read it) :check: thanks Richard for summarizing it!
Richard Feldman said:
I haven't figured out quite how to express it
"stitching"?
something like that
I guess really it's that although the uses aren't quite as similar as they are in (for example) lists, records, and tag unions, there's more similarity than I had originally noticed
Extraction
Nah
hmm. I much prefer .(Path.write_bytes) to ..Path.write_bytes. I think it feels much more semantically correct
I think that .(Path.write_bytes) makes much more sense because Path.write_bytes is an expression.
Also I wait for the bugfix: (probably would happen with a common function name like map or read)
main! = |_|
"./input.txt"
..Path.from_str()
.read_bytes!()?
..Foo.from_bytes()?
- ..transform(2, Much)
+ .transform(2, Much)
.to_bytes()?
..Path.write_bytes!(Path.from_str("./output.txt"))
Brendan Hansknecht said:
I think that
.(Path.write_bytes)makes much more sense becausePath.write_bytesis an expression.
I can appreciate that, but at the same time (as we've discussed previously) it's also inconsistent that .(foo)(bar) works, but if I take off the (bar) I don't get back a function that accepts bar
I don't think so
It is the same as static dispatch
Or I guess that means I agree, but it exists in the language anyway
hm I don't think so
well, more concretely
x.foo(bar) works, but if I take off the (bar) I don't get back a function
x.(foo)(bar) works, but if I take off the (bar) I don't get back a function
if I have ______(bar) and _____ is a valid expression on its own if I remove (bar), then everywhere else in the language, we can infer that the expression ____ on its own will be a function that accepts bar
ah, I see
yeah I guess x.foo is a valid expression on its own :thumbs_up:
that's a fair point
that said, it's definitely a problem that if you learn .(foo) you are very unlikely to realize that .(foo)(bar) is the way to add an argument :sweat_smile:
Yeah, and it may be valid too, which is scary actually
X := {
foo : Str
}
foo : X -> I64
foo = |x: X| ...
# usage
x.foo # (valid, is a Str)
x.foo() # (valid, is an I64)
I think the only variations we discussed earlier were:
x = arg1.(fn)(arg2, arg3)
x = arg1.(fn, arg2, arg3)
x = arg1.(fn(arg2, arg3))
they all have problems
it's definitely a problem that if you learn
.(foo)you are very unlikely to realize that.(foo)(bar)is the way to add an argument
I think it is easier to just teach in reverse
x.(foo)(bar) then x.(foo)() then shorthand x.(foo)
yeah, or even x.(foo)(bar, baz) because if you only know x.(foo)(bar) you might guess the way to add another argument is to follow that pattern and go x.(foo)(bar)(baz) :sweat_smile:
sure, but I don't think it would specifically be hard to learn
it's definitely a viable option, but it also feels unsatisfying to settle on a design that doesn't feel like it quite fits together right
Forgive me, but why can't the syntax be 4.days().ago!() / x.foo(bar) and have the compiler figure out if it's static dispatch or piping?
there are a few problems with that
so let's say 4.days() is changed to mean:
days function is found in the module where that type is defined (so, Num) then we look for it in local scope insteaddays in local scope, we just use that insteadnow any time a module adds a function, that is a breaking change
because it can break someone's build; they were relying on the local fallback, but now it doesn't fall back because a new function was added that uses the name they were relying on having been absent from that module's API
worse, it can not break their build
and instead silently change the behavior, because the types happen to line up but it does something different
and they have no way of ever knowing other than finding out the hard way :sweat_smile:
so yeah, it's technically feasible but I think it's best if we don't do it :big_smile:
a variation on that design is to flip it around and say the local scope gets priority
there are related problems with that, for example adding something to a particular scope (e.g. the top level) because you think it's only going to affect the couple of places in that scope you're thinking about at the time
but then it turns out it either affects other code you didn't realize was also calling that, or else someone adds new code later on and calls a method they're totally used to calling, not realizing it's being silently overridden in this case
so it has some similar downsides to monkeypatching
convenient in some scenarios, but definitely also a footgun :sweat_smile:
so I don't think we should do that either
Richard Feldman said:
I think the only variations we discussed earlier were:
x = arg1.(fn)(arg2, arg3)x = arg1.(fn, arg2, arg3)x = arg1.(fn(arg2, arg3))
I forgot one: the "only support 1 argument, and use a lambda if you want more" design
x = arg1.(|a| fn(a, arg2, arg3))
(like the others, that one has a pretty glaring downside too)
I still don't really see a glaring downside of
arg1.(fn)(arg2, arg3)
It has the same downside a vanilla static dispatch (but less bad due to 100% of the time being invalid if you pass the wrong number of args)
well, compared to (for example) arg1..fn(arg2, arg3) the downsides are:
.. in comparison that arg1..fn() naturally grows to arg1..fn(arg2) and then to arg1..fn(arg2)(arg3).arg1.(fn) would be allowed, as opposed to having to do arg1.(fn)()ok
I guess the only 2-character options we've discussed are .. and .$ and I think maybe .> at some point, but maybe there are other ones that might seem better if we tried them? e.g.
time = 4./hours()./ago!()
main! = |_|
"./input.txt"
./Path.from_str()
.read_bytes!()?
./Foo.from_bytes()?
.transform(2, Much)
.to_bytes()?
./Path.write_bytes!(Path.from_str("./output.txt"))
(I don't think that one is better than either .() or ..)
for comparison:
time = 4..hours()..ago!()
main! = |_|
"./input.txt"
..Path.from_str()
.read_bytes!()?
..Foo.from_bytes()?
.transform(2, Much)
.to_bytes()?
..Path.write_bytes!(Path.from_str("./output.txt"))
and also:
time = 4.(hours).(ago!)
main! = |_|
"./input.txt"
.(Path.from_str)
.read_bytes!()?
.(Foo.from_bytes)?
.transform(2, Much)
.to_bytes()?
.(Path.write_bytes!)(Path.from_str("./output.txt"))
or the comma version: (only last line is different)
time = 4.(hours).(ago!)
main! = |_|
"./input.txt"
.(Path.from_str)
.read_bytes!()?
.(Foo.from_bytes)?
.transform(2, Much)
.to_bytes()?
.(Path.write_bytes!, Path.from_str("./output.txt"))
Yeah, I think something about not encapsulation (SomeModule.Path.fn) as a single expression really bugs my brain.
I think so far .$ is the best 2 character example for making it still feel distinct.
for further comparison:
time = 4.$hours().$ago!()
main! = |_|
"./input.txt"
.$Path.from_str()
.read_bytes!()?
.$Foo.from_bytes()?
.transform(2, Much)
.to_bytes()?
.$Path.write_bytes!(Path.from_str("./output.txt"))
I don't like this, but an option we haven't discussed:
time = 4::hours()::ago!()
main! = |_|
"./input.txt"
::Path.from_str()
.read_bytes!()?
::Foo.from_bytes()?
.transform(2, Much)
.to_bytes()?
::Path.write_bytes!(Path.from_str("./output.txt"))
I dislike that this is almost backwards from what :: means in other languages :sweat_smile:
also I'd argue that this is more internally consistent and easier to learn, but I also don't like how it looks:
time = 4.(hours)().(ago!)()
main! = |_|
"./input.txt"
.(Path.from_str)()
.read_bytes!()?
.(Foo.from_bytes)()?
.transform(2, Much)
.to_bytes()?
.(Path.write_bytes!)(Path.from_str("./output.txt"))
I still like .> :sweat_smile:
oh yeah, I also forgot the one in the title of this thread:
time = 4.pass_to(hours).pass_to(ago!)
main! = |_|
"./input.txt"
.pass_to(Path.from_str)
.read_bytes!()?
.pass_to(Foo.from_bytes)?
.transform(2, Much)
.to_bytes()?
.pass_to(Path.write_bytes!, Path.from_str("./output.txt"))
very clear what it's doing, but a bit magical because it looks like a normal method-style call but is actually syntax sugar, and the verbosity makes cases like the first line totally unappealing
comparing with .>:
time = 4.>hours().>ago!()
main! = |_|
"./input.txt"
.>Path.from_str()
.read_bytes!()?
.>Foo.from_bytes()?
.transform(2, Much)
.to_bytes()?
.>Path.write_bytes!(Path.from_str("./output.txt"))
I think I just don't like how . followed by > looks haha
If |> is fully removed (likely), .>'s primary connotation might be a terminal prompt, which is also apt.
I like it more than .. because I think the extra dot could be harder to see, and its clearer its coming from the local scope and not associated with the previous thing. Its like we use the dot to say hey continue, then the arrow is like a pipe into the next thing.
:: is surprisingly jarring to look at...too significant in my opinion
full string interpolation metaphor mode :stuck_out_tongue:
time = 4.${hours}().${ago!}()
main! = |_|
"./input.txt"
.${Path.from_str}()
.read_bytes!()?
.${Foo.from_bytes}()?
.transform(2, Much)
.to_bytes()?
.${Path.write_bytes!}(Path.from_str("./output.txt"))
our old friend is unemployed now :stuck_out_tongue_closed_eyes:
time = 4.\hours().\ago!()
main! = |_|
"./input.txt"
.\Path.from_str()
.read_bytes!()?
.\Foo.from_bytes()?
.\transform(2, Much)
.to_bytes()?
.\Path.write_bytes!(Path.from_str("./output.txt"))
btw @Richard Feldman I intended transform as the one local function
arguably .\ is the closest you can approximate the lambda symbol... (I'm surprised that I'm only 90% 80% 70% joking lol)
Just for keyboard thoroughness, I think the remaining free real estate is
4.~hours().~ago!()4.@hours().@ago!() :eyes: 4.#hours().#ago!()Richard Feldman said:
I think I just don't like how
.followed by>looks haha
Has :> been considered? If the vertical alignment matches, > could look more balanced with : than ..
We're trying to consider options that start with . so you can just type a period and get autocomplete
Otherwise, you'd need a different character for local functions
Ok. From the :: version, I thought it needn't start with a period.
That's part of why we don't like it
It doesn't have to, but it'd have to be pretty good to overcome that benefit
someone said "turbofish"? :grinning_face_with_smiling_eyes:
I suspect this one is a no, but Reason uses ->
https://reasonml.github.io/docs/en/pipe-first
4->days->ago!
time = 4->hours()->ago!()
main! = |_|
"./input.txt"
->Path.from_str()
.read_bytes!()?
->Foo.from_bytes()?
->transform(2, Much)
.to_bytes()?
->Path.write_bytes!(Path.from_str("./output.txt"))
huh, I expected to find that confusing based on past experience with C having that mean something else, but it actually looks fine to me? :thinking:
I really like this one! Big fan.
I use it all the time in php, it's easy to type. It's clear
and actually maybe that's a feature and not a bug, because it makes me expect the same precedence as . (which addresses the problem |> would have)
plus it looks normal without spaces around it
My concern was that -> is already used in type signature and we just went through a lot of effort to remove it from lambdas
it does have the downside of not starting with .
yeah, it definitely has the downside of being function-related and yet not distinguishing between -> and => like function types do
but maybe that's okay because it would literally always be ->my_fn! so the ! immediately after it would mean there's no ambiguity as to whether you're calling an effectful function
(unlike when defining an anonymous function)
does anyone know how it has worked out for Reason?
always nice to draw on experience when other languages have directly tried something!
That's kind of a complicated question :sweat_smile:
In part because from my perspective Reason had a very small push in popularity at some point, but has generally stayed pretty obscure.
In this particular case Reason has |> as the pipe last operator, which is used for OCaml code. The pipe first operator -> is mostly meant for interop with JavaScript code, where the data structure is frequently the first arg.
Worth noting the reason stdlib is also pipe first so -> is the "main one".
Yeah reading some more I think I got that a bit backwards. I remember when I was trying reason out pipe first was new and having both was pretty confusing. Not really an issue for roc though.
I think they were trying to go for something that looked like calling a method, but didn't conflict with record fields. I remember finding is pretty ok in practice.
very interesting!
I like that it works naturally with multiple args, and that there's precedent for the syntax at least (even if it has different semantics in mainstream languages) so there's a strangeness cost advantage in terms of how the code looks visually
it stands out a lot more in a pipeline of calls than .. does, so the concern of mistakenly having an extra dot goes away
it has a lot going for it!
I think it is also a big win for "grokability".
Being able to say to a beginner "it's an arrow because it puts this thing into that thing", is really helpful. A lot of the overhead of learning a language is remembering what things do and having simple associations helps a lot IMO
"arrow calls" is a natural way to describe them
Plus it's immediately recognisable to existing FP users.
It looks close enough to |> that a curious Ocaml or Haskel user glancing at roc code could definitely Intuit what's going on.
foo->(|x| bar / x).baz()
Looks kinda good even with a simple lambda
The reason the arrow looks good is -- and please understand this is a very technical explanation -- the reason is that there's a pointy middly bit on both sides
I remember in the early static dispatch discussions, having a single symbol for triggering autocomplete, and autocomplete potentially offering both "methods" and regular functions were pretty important for folks. Is that still the case? I do really like the idea of seeing all "fitting" functions in a single auto-complete window.
yeah I think that's a significant downside of ->
but it might be the least bad downside :sweat_smile:
among the options
"only allow one arg and use a lambda for more" is surprisingly nice. The . aligns perfectly, is consistent, and easy to teach/understand. With pizza gone: if I want to pass a value to a function I use ., regardless of what function I want to pass it to (from the type's module, my module, another type's module, or just a lambda).
"Does the language have a pipe operator?"
"yup, it's .!"
or
"ew, is Roc OOP? I see dots everywhere!"
"No, young padawan, . is pipe and life is good!"
Sorry, I'm half-drunk on coffee :sweat_smile:
time = 4.(hours).(ago!)
main! = |_|
"./input.txt"
.(Path.from_str)
.read_bytes!()?
.(Foo.from_bytes)?
.transform(2, Much)
.to_bytes()?
.(|bytes| Path.write_bytes!(bytes, Path.from_str("./output.txt"))
The case where you need to pass multiple arguments is of course less ergonomic, but it is extremely clear. There's also no difference if what you're passing in isn't the first arg -- you'll use a lambda either way.
"Doesn't Roc have pizza? I thought it was a descendant of Elm!"
"Yes, it does, and it's easier to type, half the size, and even more powerful:.!"
Though I guess it kinds of puts a lot on .. All kinds of access as well as piping... Could be nice, because everything's . and easy, or perhaps it's overloaded and bloated :shrug:
There is also .|:
time = 4.|hours().|ago!()
main! = |_|
"./input.txt"
.|Path.from_str()
.read_bytes!()?
.|Foo.from_bytes()?
.|transform(2, Much)
.to_bytes()?
.|Path.write_bytes!(Path.from_str("./output.txt"))
And the | is already connected to functions in roc, and to piping in bash.
But I think it might be confusing.
side by side:
time = 4.|hours().|ago!()
main! = |_|
"./input.txt"
.|Path.from_str()
.read_bytes!()?
.|Foo.from_bytes()?
.|transform(2, Much)
.to_bytes()?
.|Path.write_bytes!(Path.from_str("./output.txt"))
...compared to arrow:
time = 4->hours()->ago!()
main! = |_|
"./input.txt"
->Path.from_str()
.read_bytes!()?
->Foo.from_bytes()?
->transform(2, Much)
.to_bytes()?
->Path.write_bytes!(Path.from_str("./output.txt"))
visually I prefer ->
although I do like that .| starts with .
I like how -> suggests flow, but .| looks like a dam to me. I think the main difference with |> is that the latter looks like a triangle, so the direction of the flow is clearer. But that's subjective understanding of aesthetics obviously
.| kinda looks like the start of a lambda in an interesting way, but otherwise I don't love it esthetically
I've mentioned this one in the past, but TS functional library - lodash uses thru for that purpose.
Personally I'm used to it, it's shorter than pass_to.
time = 4.>hours().>ago!()
main! = |_|
"./input.txt"
.thru(Path.from_str)
.read_bytes!()?
.thru(Foo.from_bytes)?
.thru(|x| transform(x, 2, Much))
.to_bytes()?
.thru(|str| Path.write_bytes!(str, Path.from_str("./output.txt"))
I think that's roughly how that would look like using roc/lodash syntax.
I don't love new symbol combinations that you'd have to know what they mean first (.. in my mind is spread/range and other symbol combinations also look weird although I could get used to them. Surprisingly the one that hurts my eyes the least is ->, probably because dash is centered in the middle of the > and other combinations look really disbalanced vertically .$ .| .>).
That's how I felt about pass_to vs thru.
image.png
I thought that it's just silly abbreviation but I just found out thru was historically used before through so I guess it's not as silly and playful word as I thought.
time = 4.thru(hours).thru(ago!)
this has the same downsides of pass_to (too verbose for this use case, looking like a method call but actually being syntax sugar) while not having pass_to's whole selling point of being obviously self-descriptive, so I think we should rule that one out. :big_smile:
If that's the case then my second most liked option were arrows.
time = 4->hours->ago!
In your example you didn't omit () parens, but I think that's legal then there's a single argument right?
To me it looks great.
I think all the options that have been discussed so far look pipeline-y to me, and give some general intuition that a value is being piped through a list of operations.
Beyond that, I don't believe any operator or keyword is going to give the reader a detailed intuitive understanding that the syntax will perform function application passing the pipeline-value as a first argument, and so will all require some learning. Unless the chosen syntax happens to be same as the one used in a language the learner is already familiar with, but I don't think any options currently under consideration meet that criterium.
I guess I'm a bit sceptical that the choice of keyword or operator will have much of an impact on the experience of learning Roc or it's day to day use. And so the auto-complete benefits of any of the options starting with a . feel way more tangible to me than the aesthetic benefits of an option like ->.
I cannot overstate how much I absolutely cannot stand looking at .>
I really want to like it because of its objective properties but I just have this extremely negative reaction to the way it looks that I can't get over :sweat_smile:
Is the “single arg or use a lambda” reasonable?
Thought: when autocomplete isn't an option, arrow is totally fine
So can autocomplete replace the dot with an arrow on selection?
That would make it pretty clearly a great option
I've hoped that autocomplete desires would eliminate ->, but if that's not guaranteed then I wanna say - I don't fully know why but I strongly dislike -> here. (As a |> :pizza: lover, I'm shocked!)
My only preference here would be to avoid .. as when I'm looking at it I feel like I'm drowning in dots.
time = 4->hours()->ago!()
main! = |_|
"./input.txt"
->Path.from_str()
.read_bytes!()?
->Foo.from_bytes()?
->transform(2, Much)
.to_bytes()?
->Path.write_bytes!(Path.from_str("./output.txt"))
vs
time = 4..hours()..ago!()
main! = |_|
"./input.txt"
..Path.from_str()
.read_bytes!()?
..Foo.from_bytes()?
..transform(2, Much)
.to_bytes()?
..Path.write_bytes!(Path.from_str("./output.txt"))
I guess other syntaxes have the benefit of clearly separating piping and method chaining.
Even worse if there were some lambdas using ranges (Roc doesn't have ranges yet right?) and ellipsis somewhere
time = 4..hours()..ago!()
read_lines = |range| ...
main! = |_|
"./input.txt"
..Path.from_str()
..read_lines(1..10)
..transform(2, Much)
.to_bytes()?
..Path.write_bytes!(Path.from_str("./output.txt"))
Super dot heavy code (and there are more dots in destructuring assignment, open records and probably many other places).
I like the idea of autocomplete replacing dot with -> depending on picked option.
JanCVanB said:
I've hoped that autocomplete desires would eliminate
->, but if that's not guaranteed then I wanna say - I don't fully know why but I strongly dislike->here. (As a|>:pizza: lover, I'm shocked!)
Would love to hear this articulated if possible
Arrow seems like it has the benefits of .> without the ickyness:
Sam Mohr said:
So can autocomplete replace the dot with an arrow on selection?
The answer seems to be that the LSP can do this! https://docs.rs/lsp-types/0.97.0/lsp_types/struct.CompletionItem.html#structfield.text_edit
Without articulating my cons, I don't see why it isn't superior to add a space to the right and rotate the - to preserve the existing glory of |> . If the method interpolation syntax doesn't look like a method, why have it?
The goal is to make long and readable chains
I don't think |> is glorious unless you're used to it, or it's used only by itself
Because when the pipes line up :ok: :ok:
One big con for me is that I see foo.bar()->baz() as a function that takes a foo.bar() and returns a baz()... or is that intentional?
That is a concern if it's not obvious where that's an expression or a type
But types always start with name : ...
the space is actually the biggest problem imo
that's what makes the precedence confusing
What if we remove the space?
time = 4|>hours()|>ago!()
main! = |_|
"./input.txt"
|>Path.from_str()
.read_bytes!()?
|>Foo.from_bytes()?
|>transform(2, Much)
.to_bytes()?
|>Path.write_bytes!(Path.from_str("./output.txt"))
the problem is, |> in other languages is always formatted with spaces around it
so it just looks like a mistake
The pipes are much taller than the dots
that's a big advantage of -> I guess, it's always formatted without spaces when it's used like this
So they look like Goliaths
and it always has the same precedence as .
With plenty of good .X options, I'd be surprised if we went with anything that doesn't look like a method, let alone to overload an existing symbol like ->.
Since autocomplete isn't a problem with the LSP being able to replace the dot with whatever we want
We could make > work
So long as there's no space
But I love exploring weird options! Bump for .\ :grinning_face_with_smiling_eyes:
time = 4>hours()>ago!()
main! = |_|
"./input.txt"
>Path.from_str()
.read_bytes!()?
>Foo.from_bytes()?
>transform(2, Much)
.to_bytes()?
>Path.write_bytes!(Path.from_str("./output.txt"))
What's the best option with a single character? I really like how everything lines up here
Lua uses : for its method call syntax. Has that been discussed? Its just, like, another dot on top
looks like this
time = 4:hours():ago!()
#// or 4:hours:ago! ??
main! = |_|
"./input.txt"
:Path.from_str()
.read_bytes!()?
:Foo.from_bytes()?
:transform(2, Much)
.to_bytes()?
:Path.write_bytes!(Path.from_str("./output.txt"))
This syntax highlighter makes them different colors (and therefore visually distinct)
If they were the same color, maybe it wouldn't be as good
We'd want to preserve that maybe?
Other way around, in my opinion - in the same way that I want parens and braces the same color, I want method symbol and method-ish symbol the same color.
How important do you think it is to be able to tell which is a method and which is a local function at a glance?
JanCVanB said:
With plenty of good
.Xoptions
the problem is I wouldn't call any of those options "good" - I've been using words like "viable" and "plausible" because I think we could ship them, but I don't think any of them are actually good :sweat_smile:
Sam Mohr said:
What's the best option with a single character? I really like how everything lines up here
In my opinion ~ \ : $ > | (sorted most-to-least favorite, with some ties)
> is definitely taken :big_smile:
| isn't taken at the expression level though, which somehow I never realized
I agree that > is weird looking, but we already have - work differently with or without a space
yeah but people write x>y all the time
Yep
I've seen programmers who never put spaces around their math operators
Let's not consider it
I've seen them too :frown:
time = 4|hours()|ago!()
# or possibly:
time = 4|hours|ago!
main! = |_|
"./input.txt"
|Path.from_str()
.read_bytes!()?
|Foo.from_bytes()?
|transform(2, Much)
.to_bytes()?
|Path.write_bytes!(Path.from_str("./output.txt"))
Morse code?
it's definitely interesting, I'm not sure how I feel about it
time = 4~hours()~ago!()
main! = |_|
"./input.txt"
~Path.from_str()
.read_bytes!()?
~Foo.from_bytes()?
~transform(2, Much)
.to_bytes()?
~Path.write_bytes!(Path.from_str("./output.txt"))
it is definitely cool that the meaning lines up so well with the meaning of | in Bash
I don't like ~ for this because I always think of that as meaning "approximate"
It visually works
If only - wasn't taken
it's only slightly taken
who uses that one, honestly
Stop being so negative!
| is definitely used with spaces around it in Bash though, and when it is used in programming languages (e.g. for bitwise operations, or in Roc patterns) it has spaces around it too
Yep
We do want to give off Lua2 (2ua) vibes, so : gets a bump from that.
Hawk
I'm down for :
Two minor downsides
Lack of direction, and tricky to distinguish in multiline chains
also : already means 2 other things in Roc, namely types and record fields
I couldn't think of any situations where this would be ambiguous as long as we require parens
but if we allowed omitting parens (e.g. 4:hours:ago!) then { x:y } would become ambiguously a record vs not
oh wait, nm it's ambiguous regardless
{ x:y() } could be a record with one field, or a block with one expression in it
before anyone asks, let's not revisit block syntax for the sake of : :stuck_out_tongue:
Well if we like | and we're really in blue sky mode... we did just drop an ASCII character that could stand alone
time = 4\hours\ago!
main! = |_|
"./input.txt"
\Path.from_str()
.read_bytes!()?
\Foo.from_bytes()?
\transform(2, Much)
.to_bytes()?
\Path.write_bytes!(Path.from_str("./output.txt"))
and we could revive our "hey it's a lambda" justification
It's weird but very readable
\ looks like a slope that funnels the arg from the line above into the next function :joy:
it's a slide, whee! :slide:
#Slidechaining #MusicProductionJoke
Daft Punk's favorite operator
I still prefer . single arg+lambda because it’s consistent “whenever I want to pipe, I .”. However, I’m down for the slide :P
@Niclas Ahden isn't that concern assuaged by the LSP letting you type a dot and replacing it with something else?
Partly? Isn’t it kind of nice that the .s line up like that though? Looks super clean and easy :) No “so one is a dot, what is that other thing?” yknow?
Yeah, I agree
But I also want to see when something different is happening, which is the case for everything else in Roc (see purity inference)
the top 3 contenders in my mind right now are:
...(->and they all have a bunch of different tradeoffs :sweat_smile:
BAM! :exploding_head:
time = 4🛝hours🛝ago!
main! = |_|
"./input.txt"
🛝Path.from_str()
.read_bytes!()?
🛝Foo.from_bytes()?
🛝transform(2, Much)
.to_bytes()?
🛝Path.write_bytes!(Path.from_str("./output.txt"))
I think if I had to pick one right now I'd pick ->
because I think the only downside compared to .. and .( is that it doesn't start with . - whereas those have (in my mind) more serious drawbacks
My top two:
.(: I've already written a whole thing about above. If I understand this whole feature correctly, then I think .( with single arg is strong (see that message).\: It's got direction, it lines up well, it's easy to type, it's a single char, LSP can help us with the . just like ->-> looks like PHP :upside_down:
.. looks like a range and doesn't line up, but I guess it's pretty neat too
: may be good? Guess it depends on the highlighting
Time for dinner. Home-made hamburgers here we gooo!
enjoy!
Sam Mohr said:
But I also want to see when something different is happening, which is the case for everything else in Roc (see purity inference)
My view (which might be flawed?) is that broadly speaking something different isn't happening. I understand that it's technically different, but when teaching or thinking about it I can go "I'm just piping" and that's true. It's just a matter of what I'm piping to, but whatever the receiver is, I'm just piping. Right?
I do think it goes haywire when it's like .(foo)(baz) because, to me, it's weird that one argument is coming from the left and the other is coming from the right, but both are applied to foo. I know it makes sense technically, but visually it's a bit jarring, and I have to think about it every time I look at it like "ah, yeah, I'm piping something in from the left and that is partially applied, then I'm applying baz to the partially-applied function".
Richard Feldman said:
I think if I had to pick one right now I'd pick
->
I would put -> and .( as relatively even. I think other options are worse in my view. While I personally lean .(, I think as a community leaning -> probably makes more sense.
time = 4\hours\ago!would we call this the "shovel operator"? Shoveling the 4 into hour etc.
We do have .accessor as a valid function shorthand on structural records that can be combined with the current pipe |> operator.
If I can do,
getX = .x
{x:2} |> getX
For consistency, I'd expect this to work
{x:2} |> .x
It is debatable whether it's good style to use piping like this, when you could just { x: 2 }.x, but my main point is that we should consider it. The shorthand should stay, since it's really valuable in higher order functions, like my_list.map(.my_field), but it feels bad to make them a "second class citizen" in piping.
unique_crops = farm
.harvest()
->.crops # Looks great IMO.
->Set.from_list()
unique_crops = farm
.harvest()
...crops # definitely a mistake, especially with the (`...` operator)
..Set.from_list()
unique_crops = farm
.harvest()
.(.crops) # Works, and If I pause on it, I even get what's happening, but I'd rather never see this
.(Set.from_list())
Why would you use that at all? Just access the record directly
x.foo().field1.bar()
One concern I have with -> is that it is also used in our types to represent a function.
calculate : I64, I64 -> I64
calculate = |a,b|
a -> calc_help(b)
Not a major concern... just wanted to flag it
Weirdly enough, seeing .fn() and ->fn() together is less taxing to my brain than
.fn() and .(fn)together. I've gotten used to .fn() having a direction, but not .(fn) having one.
->fn() naturally have it.
\ seems fine when piping into .accessor
unique_crops = farm
.harvest()
\.crops
\Set.from_list()
(Edited: removed : example cuz : was rejected for causing ambiguities)
Brendan Hansknecht said:
Why would you use that at all? Just access the record directly
Agreed. But a syntax like ..fn() would create an edge case for them, whereas the other options don't
What about .:?
time = 4.:hours().:ago()!
main! = |_|
"./input.txt"
.:Path.from_str()
.read_bytes!()?
.:Foo.from_bytes()?
.:transform(2, Much)
.to_bytes()?
.:Path.write_bytes!(Path.from_str("./output.txt"))
I find ->fn very pleasing to look at, but would also take .(fn) with the restriction that Niclas has tried out, allowing only 1 argument, not .(fn)(b ,c).
I'd rather see occasional stuff.(|s| fn(s,b)) -s, than stuff.(fn)(b) -s, mainly, because (b) feels disconnected from fn
Of the ones discussed so far .. is by far my top choice. I really dislike the way .(function) looks.
I like that .: allows you to start with a . and it seems to distinguish local function calls slightly better than .. does
Norbert Hajagos said:
Brendan Hansknecht said:
Why would you use that at all? Just access the record directly
Agreed. But a syntax like
..fn()would create an edge case for them, whereas the other options don't
Sure, but we can just require parens in that case. So not really an edge case. Same as if you put a lambda there. Requires parens
+1 to .: being more distinct than :
Brendan Hansknecht said:
Norbert Hajagos said:
Brendan Hansknecht said:
Why would you use that at all? Just access the record directly
Agreed. But a syntax like
..fn()would create an edge case for them, whereas the other options don'tSure, but we can just require parens in that case. So not really an edge case. Same as if you put a lambda there. Requires parens
True. It is really a minor concern the more you talk about it.
with different syntax highlighting:
time = 4.:hours().:ago()!
main! = |_|
"./input.txt"
.:Path.from_str()
.read_bytes!()?
.:Foo.from_bytes()?
.:transform(2, Much)
.to_bytes()?
.:Path.write_bytes!(Path.from_str("./output.txt"))
Because it's all dots, this is a great option visually, and it has basically none of the discussed downsides IMO
Could you explain once more, why they have to be different? Why is it not possible, to use the dot for both?
time = 4.hours().ago()!
main! = |_|
"./input.txt"
.Path.from_str()
.read_bytes!()?
.Foo.from_bytes()?
.transform(2, Much)
.to_bytes()?
.Path.write_bytes!(Path.from_str("./output.txt"))
Richard Feldman said:
there are a few problems with that
Start reading from that message
Highlighting . and .: the same way really makes a difference for this "staircase operator". Not a huge fan of it's aesthetics (going up, rather than right), but it seems very practical. It starts with . and kinda like how :: is used in Rust or CPP to refer to Static methods, the : signals that the function is coming from somewhere else, not directly hanging off of what's on the left hand side.
Gotta go, have a good one!
.: looks neat! Should we consider how each option looks with a lambda?
main! = |_|
"./input.txt"
.:Path.from_str()
.read_bytes!()?
.:(|bytes| "Nice: ${bytes.to_utf8()}")
main! = |_|
"./input.txt"
.(Path.from_str) # Is this viable, or does it have to be `.(Path.from_str)()`?
.read_bytes!()?
.(|bytes| "Nice: ${bytes.to_utf8()}")
main! = |_|
"./input.txt"
.|Path.from_str() # Teach `.|x| f(x)` first then explain that `.|` is a shorthand for that
.read_bytes!()?
.(|bytes| "Nice: ${bytes.to_utf8()}")
main! = |_|
"./input.txt"
\Path.from_str()
.read_bytes!()?
\(|bytes| "Nice: ${bytes.to_utf8()}")
main! = |_|
"./input.txt"
->Path.from_str()
.read_bytes!()?
->(|bytes| "Nice: ${bytes.to_utf8()}") # Here we're getting into -> and => territory
I expect we'd want to put lambdas in parens for all of these
Which has the detriment of .(func)(args) but should be a pretty rare operation
If not, then there can't be anything chained after the lambdas
I assumed that we wouldn’t be able to use lambdas with the proposed dot piping operators. I would prefer that they are not allowed to keep things simple. I usually find using a lambda within a pipeline like that to be an indication that the pipeline needs to be broken up.
Richard Feldman said:
I think the bar for ever saying "you can use any expression in this position except for _____, which is banned" should be astronomically high and this is not even on the same planet that bar is on :sweat_smile:
Proposed and shot down, but I totally agree with you
Once we have variables with foo_ I agree. Until then, pipelined lambdas are a great way to do inplace mutation without needing to rename constantly
I think it’s neat for a quick transformation in a short pipeline :man_shrugging:
It can be pretty handy every once in a while, yes
I have really conflicting feelings about .: because:
We're exploring new ground!
objectively, I like that:
.I don't think there's a precedence issue with _any_ of the options discussed is there?
name suggestion: method lookup syntax
- it also looks totally alien - like I don't think I've ever seen it in a programming language before
The functionality it provides isn't in any other languages though right? -- like this really is completley new ground
@Joshua Warner the only precedence issue is with the ones that have (or look like they should have) spaces around them because they're infix operators
Luke Boswell said:
- it also looks totally alien - like I don't think I've ever seen it in a programming language before
The functionality it provides isn't in any other languages though right? -- like this really is completley new ground
the ReasonML -> does exactly this
Sam Mohr said:
name suggestion: method lookup syntax
isn't this the one that isn't method-related though? :big_smile:
The thing I'm trying to tie together is that we're using a method-like syntax for a non-method
AKA a local function
As long as we go with dot-something, we're emulating a method call
But you're right, this doesn't say "non-method"
That would get pretty wordy...
They call it the "pipe first" operator in ReasonML: https://reasonml.github.io/docs/en/pipe-first
Sounds like we should maybe just call it the pipe operator (if we go with ->)
yeah right now in my head .: feels like the "cool option" and -> feels like the "safe option" :big_smile:
.: could be the apply operator
Like maybe the .: is like an A
I don't understand why something like .: but not .$
Anyway, I pretty strongly lean -> over those currently.
I wrote up a draft of an entry in a future language reference about function calls:
https://docs.google.com/document/d/1Orw1Ti_If45rI2GQvkZO3iTIhb7PxScx6iDJIa4PumM/edit
(I happened to use -> at the time, but obviously other options could slot in there too.)
Arrow call just Makes Sense
It's nice how the three different ways of calling a function are all named "calls"
Function application, Function piping, and Function dispatch
yeah I tried to pick names I thought would line up with the names people would likely choose independently, with the exception of "dispatch calls"
I'm calling those methods
Function -- application, pipes, methods
I chose that one over "method calls" because I've already seen confusion over the concept of whether Roc has a first-class thing called a method :sweat_smile:
I've got my brainstorming hat on... just thinking about names
Richard Feldman said:
I chose that one over "method calls" because I've already seen confusion over the concept of whether Roc has a first-class thing called a method :sweat_smile:
I understand, but I'm 99% sure people will just call it a method :laughing:
Even zig has "methods"
// Structs can have methods
// Struct methods are not special, they are only namespaced
yeah I agree, but I think it might be helpful to create a meme response of "Roc doesn't have methods"
I'll avoid bike shaving this
Richard Feldman said:
yeah I agree, but I think it might be helpful to create a meme response of "Roc doesn't have methods"
Why though... wouldn't this be a natural name that people can click with pretty easily if they want to know what it is?
and if the official language reference uses the term methods to describe the calling style, it might make it harder to correct the mental model
I'm onboard with memes, but here we're thinking about how these concepts hold together
Maybe "pass to" call could be a good description
the problem is that methods in every other language have different semantics
like in some mainstream languages you can't just pass a method to a higher-order function
because this is special
I definitely think calling them something "dispatch" something is helpful
(or self or whatever)
I'm thinking about the "pass to" thing -> and what might be a meaningful name, as compared to "arrow" which doesn't quite feel right
it's possible that this is the wrong direction for teaching though
Yeah, we're building a bit of a strawman here (as in I don't expect these words to actually be used in the tutorial) -- but it's a helpful model for putting these features side-by-side and thinking about why they exist (I'm having fun thinking about it at least)
Yeah, I definitely would lean far away from methods
There is a reason that it is a pipeline and not a method chain in most functional languages
Different model
Better model
More procedural model
another option would be to formally define something like "in Roc, a 'method' refers to a function that's defined in the same module as the type of its first argument."
the problem with that is, if you just start throwing around the term "method" in casual conversation, some people will be aware of this "official" (and unusual) definition, and others may not - they may be assuming a definition based on other languages they've used
and there's no real way to tell until after the conversation has gotten confusing
whereas "Roc doesn't have methods" is a quick way to get everyone in the conversation on the same page before that happens
It's worth a try!
In roc, there are three ways to pass arguments to a function; (1) applying, (2) piping, and (3) dispatching.
Here are examples of these three styles;
(1) Str.contains(url, "a")
(2) url->Str.contains("a")
(3) url.contains("a")
Applying arguments to a Function
You can apply a function in the normal way...
Piping arguments to a Function
You can also use a pipe operator -> ...
Dispatch to a Function
You can dispatch ...
Should maybe put dispatch first (before piping) to imply its importance?
I think it's easiest to learn them in this order because each explanation builds on the previous one
Okay, sure
like dispatch does the lookup part but also the "argument first" part
I guess the benefits of static dispatch should lead to its usage if its actually valuable
yeah I don't think its use will need to be promoted - it'll promote itself :big_smile:
Doc is looking pretty good. I can imagine this existing and it just feeling natural. Like "why doesn't every language have three ways to call a function..."
“Local dispatch”? The idea is to name both “->fn” and “.fn” after a single concept (dispatch), but distinguish by kind. maybe, “dot/arrow dispatch”. Smth like that.
I looked through the thread but didn't find & suggested:
time = 4&hours()&ago()! # Not great
main! = |_|
"./input.txt"
&Path.from_str()
.read_bytes!()?
&Foo.from_bytes()?
&transform(2, Much)
.to_bytes()?
&Path.write_bytes!(Path.from_str("./output.txt"))
&& is taken, but & might be available?Precedent:
Pipe: https://hackage.haskell.org/package/base-4.21.0.0/docs/Data-Function.html#v:-38-
Functor pipe: https://hackage.haskell.org/package/base-4.21.0.0/docs/Data-Functor.html#v:-60--38--62-
Reminds of Ruby's artists.map(&:perform)
I think most options struggle on a single line, esp. when meeting a ?:
.to_bytes()? |> Path.write_bytes()
.to_bytes()?->Path.write_bytes()
.to_bytes()?.:Path.write_bytes()
.to_bytes()?.(Path.write_bytes)
.to_bytes()?&Path.write_bytes()
.to_bytes()?\Path.write_bytes()
.to_bytes()?.|Path.write_bytes()
Pizza fares well by using spaces (cheating)
&& is technically not taken anymore since we're moving to and and or for logical operators, but we'll want to ideally parse && still and say "did you mean and?"
Anyway, I think & is a valid option!
Not sure how much people will like it, but it works
Niclas Ahden said:
I think most options struggle on a single line, esp. when meeting a
?
For completeness:
.to_bytes()?.:Path.write_bytes()
Wow I really like the aesthetics of that - I think it has the second-most transparent pixels without a whitespace character (second to ..), which helps visually break up the line. A Roc color theme that dims noncritical punctuation would help too, though that might even be more of a visual factor than pixels for aesthetics.
Sorry, I thought I included that one! Added!
As a modal text editor user, :foo slightly reminds me of executing a command. I suppose that's similar to local & imported functions being commands in the current namespace.
Here's a third idea for how to talk about the three function application syntaxes: instead of it being "three ways to call a function" or "three ways to pass arguments", we could describe "three ways a function can get its first argument", since non-first arguments are always treated boringly and we want to scope the mental model very tightly.
(I'm proposing phrasing, but idk which specific verb words are best, if any.)
These phrasings seem equally robust (as compared to the other nickname-based proposals) for the arbitrary sentence structures of casual conversation:
contains on the url."contains function."url."Potential confusion this avoids:
Sam Mohr said:
Sam Mohr said:
So can autocomplete replace the dot with an arrow on selection?
The answer seems to be that the LSP can do this! https://docs.rs/lsp-types/0.97.0/lsp_types/struct.CompletionItem.html#structfield.text_edit
I was thinking some more about this, and I think it narrows the gap but doesn't close it completely.
It's true that this means if I type . we can show autocompletions for both .: and -> in there, and then switch the operator accordingly if you select one of those.
However, if I type . and want to narrow it down to just the .: ones, I tap : and I'm there. For -> I have to start over.
that said, I guess we could show an autocomplete for -> options if you press - :thinking:
upside there would be that it filters after typing one character instead of 2, but on the flip side, it might be weird to see an autocomplete menu show up every time you're trying to do subtraction or negation
(or if you type .-? eh that's unintuitive unless you were in this discussion, nvm, continue sorry :sweat_smile:)
I don't have any conclusions, just observing :big_smile:
Agree that autocomplete is nice with .:. It also aligns quite well (not perfectly, but eh). I'd probably go so far as to use conceal in vim or ligatures to transform it to a single char that aligns perfectly: tadaa! :tada:
:: is usually used for traversing modules etc. so you could argue that adding : onto . is "reaching outside of the current type". Am I reaching? I'm reaching :sweat_smile:
JanCVanB said:
(or if you type
.-? eh that's unintuitive unless you were in this discussion, nvm, continue sorry :sweat_smile:)
I’d say the resolution of . to -> is not intuitive in itself. But once you learned about the feature, you’ll likely try to narrow it by .- because in the dropdown you’ll see the variants containing -
foo
bar
->foo
->bar
So the use of .- for autocompletion is as intuitive as .foo
For someone who did miss it "live" and try to catch up, is this all about not using |> because we want to start with a . instead? And this is because people are familiar with a dot . to kick-start an autocomplete?
If this is the case, then I would like to see this discussion happening in the (near) future when |> gets into JavaScript and suddenly whole world _LOVE_ |> and all the auto-completion tools recognize that and now we are in the Roc world with strange ..fn or .(fn) or .$fn and everybody else are living their happy lives with |>, but not us.
I would like to see this discussion happening in the (near) future when
|>gets into JavaScript
I remember the pipe operator for es proposal was introduced like 10 years ago. "near future" in this context is never future :D
I don't think it's about mainstream or hype but about consistency. The difference between chaining via static dispatch and pipe operator is that static dispatch is not an operator. So it has no complications with precedence. And having two similar features for chaining that work differently seems uncomfortable. But it's at least in my head
The difference between chaining via static dispatch and pipe operator is that static dispatch is not an operator.
OK, but on the other side, |> is already some kind of special-operator, because it applies left-side argument to the function call. So a dot . call is not that different… or maybe it is :thinking:
I'm wondering, would it be a big deal to keep separate autocompletes for methods vs functions? I kind of like the idea of pressing . and only seeing methods, pressing - and only seeing pipeable functions. It makes it easier to distinct APIs coming from some package and my own functions (also makes autocomplete list shorter, because I guess if everything would be shown the list could be reaaaaly long).
I think personally maybe I'd even disable changing . to -> automatically if LSP allowed that (hard to tell because I've never used anything like that so I don't have empirical experience if it would be nice to use).
Having separate first signs is nice in a way that it allows to experiment with UX a bit (or even allow users to set it in the settings).
EDIT. in other words
However, if I type . and want to narrow it down to just the .: ones, I tap : and I'm there. For -> I have to start over.
I think that narrowing might be nice both ways, not only from all to functions but also to only methods.
Not sure what's opinion of people here, maybe majority thinks that autocomplete should absolutely show everything, but I've feeling like you'd normally get grasp of what's method/function pretty quickly and wouldn't make many mistakes requiring to change the operator.
it makes it easier to distinct APIs
I assume "native" methods are expected to be in the beginning of the list and foreign contain leading "->" so no real problem to distinguish them?
the list could be reaaaaly long
I think it will contain only suitable interfaces from the current scope? anyway, if the list is long, it will be long even if separated.
wanted also to note, that -> takes the same number of characters as .- for narrowing
I think it will contain only suitable interfaces from the current scope?
My hope is it would find the methods from all available scopes and import them, so I would not have to :)
you mean also in not imported modules? sounds huge but why not :D
Kiryl Dziamura said:
you mean also in not imported modules? sounds huge but why not :D
I've assumed that it would suggest all available functions with matching signatures (not just imported ones) and autoimport them. I use it all the time in TS and didn't even think before that I'd have to manually find it first and import them into the scope before LSP would suggest it. If we had to do that I think that would severely limit usefulness of the feature.
I probably just got used to write some things mostly manually to avoid wrong imports that provide similar interfaces to what I want. Like, if I want to import smth, first I want to ensure that the import location is correct
So I forgot that autocompletion already works for the whole project
I'm sure it is probably decision fatigue at this point from all the discussion, but a large part of me leans that we shouldn't do any of these for now. We should leave in |> and see how it feels. I totally understand that |> has precedence issues, but I would guess that this feature is less often used than normal static dispatch.
To be a bit more concrete, in Richard's real world app, this feature is used 7 times. Of those, there are exactly two unique ways it is being used. Fundamental diffs:
- .(Ok)
+ |> Ok
- expired_time = now + 1.(hour)
+ expired_time = now + (1 |> hour)
Looking at roccibird, most things would switch to static dispatch. Probably the biggest pain for rocci bird will actually be that static dispatch is at the module level and it define multiple custom records that would want to use it (they could instead use this syntax). I guess I they also could use static dispatch as long as they functions they call have unique names.
Anyway, roccibird has 6 calls in this odd may or may not be static dispatch. Otherwise, all pipes would turn to static dispatch except for 3. All of those three could work with the single arg version of .().
This is the only hairy location with pipes. It hits bad precedence:
pipes =
prev.pipes->updatePipes().appendIfOk(pipe)
to
pipes =
prev.pipes |> updatePipes() |> |l| l.appendIfOk(pipe)
or
pipes =
(prev.pipes |> updatePipes()).appendIfOk(pipe)
Also, this is the part that maybe could use static dispatch? Would require exposing 3 custom types from the same module. So names are less nice. That or moving all 3 types to separate modules, but that feels like overkill in this case. I guess that is where it would be nice to nest a module inside another.
In this code, each prev is a bespoke record for the stage (could be custom)
when model is
TitleScreen prev ->
prev
|> updateFrameCount()
|> runTitleScreen!()
Game prev ->
prev
|> updateFrameCount()
|> runGame!()
GameOver prev ->
prev
|> updateFrameCount()
|> runGameOver!()
well the new compiler has to implement something from scratch, and I'd prefer to try one of the new options out (-> if I had to pick one right now, since it still seems like the safe option) so we can at least see how it feels in practice.
I think I've missed something. Why can we make the precedence work for -> but not |>?
I also thought this whole search was for something starting with a . for autocomplete.
I think . is a factor but not the only factor
although I hadn't considered the point from earlier about maybe wanting two separate autocomplete namespaces to narrow down the search results - one for dispatch and one for functions in local scope. I'm trying to think how that experience might be, and I'm not sure which way I'd actually prefer if I could pick between the two options.
the precedence problem is really just about "does this look like an infix operator or not"
e.g. does it have spaces around it (or would you expect it to normally)
a|>b I see as an infix operator (even if there are no spaces) because that's what it is in every language where I've seen that
a->b is not an infix operator in any language I've seen; it always works like . does in mainstream languages
Which is.... it has to touch one of the two operands?
from a parsing perspective?
I haven't thought deeply about it, but I suspect the parser can get away with allowing whitespace on either side of a . or ->
I didn't think that was common. Then I looked and it is. It's amazing the number of things that parsers allow that I would never even try to do
I just learned that ReScript has -> as their pipe operator
So rescript and reasonml both
Elias Mulhall said:
I suspect this one is a no, but Reason uses
->
https://reasonml.github.io/docs/en/pipe-first
4->days->ago!
yeah, this became the frontrunner after it was suggested!
ReasonML is the syntax part of ReScript - it's kind of complicated :sweat_smile:
I had a vague inkling of that, but wasn't confident enough to comment so
So confusing :sweat_smile:
When we were considering .., there was some intuitive similarity with the existing spread/rest operator that would become overloaded. Is the same achievable with ->? How can we explain piping as similar to the separation of args from return value in an uneffectful function's type signature?
I think it isn't confusing to have the same symbol for the 2 concepts, but not connect them, since one is in the type signature and the other is an operator. I interpret them completely differently based on where they appear. One could argue that way they are less searchable ( as in "what does -> do in roc" into duckduckgo :big_smile: ), but with llm-s it's less of an issue.
The whole ReasohnML/ReScript/Buckscript thing is SO confusing and I really believe that's the reason it hasn't taken off at all
Though someone from ReScript is on today's episode of Developer Voices, so I look forward to how he sells it and any updates on that ecosystem. The episode has a subhead of "A better Typescript?"....
yeah I was initially surprised by Louis Pilfold's experience with the Gleam syntax change making a big difference because my previous takeaway from ReasonML was "if you take a good functional language like OCaml and put a familiar syntax on it, the result is not widespread adoption"
at the time I thought it was a pretty narrow experiment, but in retrospect there were a lot of confounding variables besides just the syntax change
I forgot to mention that to Louis - just recorded a software unscripted episode with him where we talked a bunch about Roc's design changes and how Gleam came up in several of our design discussions, influenced the design we ended up, etc.
I hope he's doing well. He was very nice to me when I was a contributor in VERY early days of Gleam (late 2019/early 2020)
But yeah, with ReasonML they had the tailwind of Facebook/Meta backing, but the headwind of OCaml/Bucklescript complexity in both the toolchains and the standard library - as well as confusing messaging made even worse IMO by ReScript
Norbert Hajagos said:
I think it isn't confusing to have the same symbol for the 2 concepts, but not connect them, since one is in the type signature and the other is an operator. I interpret them completely differently based on where they appear. One could argue that way they are less searchable ( as in "what does -> do in roc" into duckduckgo :big_smile: ), but with llm-s it's less of an issue.
I like -> because it makes me think of I think PHP (and C to a lesser extent). I could get used to it. I never thought that Lua's use of : for method invocation would make sense to me, but now it's second nature to me
a thing I'd never thought about before but have been ruminating about based on all our super in-depth syntax design discussions is that:
maybe all of that seems obvious, but they aren't things I'd put together in that way before
despite the fact that if you'd asked me about them one at a time, I think I would have given the same description of them :sweat_smile:
Anthony Bullard said:
Norbert Hajagos said:
I think it isn't confusing to have the same symbol for the 2 concepts, but not connect them, since one is in the type signature and the other is an operator. I interpret them completely differently based on where they appear. One could argue that way they are less searchable ( as in "what does -> do in roc" into duckduckgo :big_smile: ), but with llm-s it's less of an issue.
I like
->because it makes me think of I think PHP (and C to a lesser extent). I could get used to it. I never thought that Lua's use of:for method invocation would make sense to me, but now it's second nature to me
If we remove -> from match statements, then it's kind of freed back up to be used for our pass_to alternative. It wouldn't be used anywhere else then except in our Types for functions List a -> Str etc
Oh yeah, good point Luke! Forgot -> was also in matches.
Is -> weird to use if the function you're dispatching to is impure?
I think it's kinda conceptually weird but I think in practice it's probably fine
Last updated: Jun 16 2026 at 16:19 UTC