here's an interesting implication I just realized of the .(foo)(bar, baz) syntax: as written, it's essentially a partial application operator
if I write foo(bar, baz) - or, equivalently, (foo)(bar, baz) - then in either case foo must be a function that takes 2 arguments
which suggests that if I write arg1.(fn)(arg2, arg3) then fn is a function that takes 3 args, and arg1.(fn) by itself should return a new function which takes 2 arguments (and has applied arg1 to fn)
this is interesting because it means it can be used to concisely partially apply any function, as if it were curried, if you want to
but without having the functions actually be curried (which has beginner-friendliness downsides as well as encouraging pointfree function composition, both of which I consider undesirable)
to give a concrete example, I think in that syntax both of these would Just Work:
divide_by_2 = |numerator| numerator / 2
# ...is the same as:
divide_by_2 = .div(2)
divide_2_by = |denominator| 2 / denominator
# ...is the same as:
divide_2_by = 2.(div)
I don't love how that reads in the particular case of division, but there might be other examples out there where it looks nice :big_smile:
either way, there is some neat visual symmetry to it, and I like that it's just as concise as the method-call style
I'm not saying we should or shouldn't go with that design, but it is interesting that it would make partial application as concise as it is in curried languages, except without needing beginners to learn about it up front etc.
so kind of as an advanced concept
and you don't even need to learn about it to do the basic 1-arg foo.(bar) style, so it can really be pushed to late in the learning process as a very advanced concept
and yet it's a consistent design, in that list.(Dict.from_list) would be correct and actually list.(Dict.from_list)() would be a type mismatch
because Dict.from_list takes 1 arg, so it would be fully applied already by list.(Dict.from_list) and wouldn't return a function, so then trying to call the returned Dict with () would be a type mismatch
so in that world, there's no syntax sugar for avoiding the need to do e.g. .(Ok)()
Confirming that this is the way I expected it to work as well when reading it. If more people read it the same way it may be a very natural way to learn and think about the syntax.
yeah I don't know if it's awesome or strange (or both!) etc. but it's definitely interesting!
I have to say, the .( syntax has been growing on me a lot compared to .pass_to as more uses for it continue to naturally pop up
e.g. 4.(hours) :heart_eyes:
4 |> hours and 4.pass_to(hours) don't read nearly as nicely to me
Yeah I like it a lot better than pass_to at this point. Though maybe seeing it used poorly in practice would change my mind
haha yeah there's a question of cultural norms for sure
one of the things Evan always talked about was how the timing of introducing advanced features matters a lot
e.g. if the community is already doing things in a reasonable way, introducing an advanced feature doesn't go back in time and rewire the entire ecosystem to (ab)use it all over the place
because there are already community norms around using the basics etc.
so it's more likely to be used sparingly and less likely to be used in ways/places where it's going overboard
I think 4.(hours) is neat, but I wish the complete example had fewer special characters:
yesterday = 1.(day).ago!()
later = now!() + 8.(hours)
I'm not sure I'd type that out correctly the first go-around (I would probably forget parens after ago! for example). As compared to Ruby:
yesterday = 1.day.ago
later = 8.hours.since
I think I would be equally fine writing something like this:
yesterday = time_ago!(day(1))
yesterday = time_ago!(Day 1) # If PNC is optional in the future
yesterday = Day 1 |> ago!
even_earlier = time_ago!({ days: 1, hours: 8}) # Probably has the nicest LSP experience
I'm in favor of .( anyway, but just laying out that the other approaches could have equally nice APIs in my view (everyone has their own opinion of course!)
yeah, plus it would need to be 1.(day).(ago!) because ago! would be coming from a different module than Duration (because it's in the platform, whereas Duration would be in a platform-agnostic module)
but I think that's something people could get used to pretty quickly once you see the pattern
in Ruby I don't think people would just try 1.day.ago without having seen it somewhere else first
so I imagine similarly you see 1.(day).(ago!) and follow that pattern
Indeed, but I think after writing 1.day.ago they'd remember how to do it. If I wrote 1.(day).(ago!) once I wouldn't remember how to do that again.
With the edit I might though... 1.(day).(ago!) is quite nice. Before the edit it was 1.(day).(ago!()) I think. My worry is probably mostly that I'll forget parens after something like that ago!.
Yeah, I think 1.(day).(ago!) is neat. now!() + 8.(hours) too. Scratch my concerns!
yeah I originally had it as 1.(day).(ago!()) but that's wrong; that would only make sense if ago! were a function that returned a function, which would be bizarre :big_smile:
(fixed in the edit)
Everything is awesome! I just used Roc to create a Facebook/Instagram ad as the beginning of a new system I'm writing for a client. So many uses for Roc :) What a time to be alive. Looking forward to try Roc with all the new changes coming!
So if .() is special syntax for partial application then both .(|x| add(x, 2)) and .(add)(2) are valid, right?
yep!
of note, I think this would have to work a bit like the current "tags can be used as either functions or values" when it comes to function arity
because otherwise we'd need a new type notation for inferred types of things you give to this, e.g. when you put this into the repl:
|val, fn| val.(fn)
like I guess there could theoretically be a syntax like a, ..b -> c to mean "a function where the first argument has the type a but we don't know what the other argument types are or how many there are" but that doesn't seem worth it just for this one case, especially considering the function would only be in that state very briefly before resolving to a totally normal function type (just like with "tags are values and functions")
Is arg2.(arg1.(fn))() valid? (where fn : _ , _ -> _)
Distinct from arg2.(arg1.(fn)()), where fn : _ -> (_ -> _)
syntactically valid? I suppose, but at that point I think we're outside the realm of possible usefulness :big_smile:
I’m unsure if it is valid... At least it’s confusing :sweat_smile:
arg1.(fn) applies arg1 as the first arg of fn. Then arg2.(_) tries to apply arg2 as the first arg of _that_. What is the first arg in that case? I’m assuming it’s the “first non-applied arg” which would be the second one, so: fn(arg1, arg2). Not expecting answers, just found it curious.
Richard Feldman said:
and yet it's a consistent design, in that
list.(Dict.from_list)would be correct and actuallylist.(Dict.from_list)()would be a type mismatch
So for my own education. We are saying that x.(fn) partially applies the first argument. As such, if you have a one arg function, it completely resolves the function. If you have a 2 arg function, it returns a lambda that takes the remaining one arg.
This means if I want triple partial application, it would be
z.(y.(x.(fn_with_3_args)))
And that would be the same as fn_with_3_args(x, y, z)
And y.(x.(fn_with_3_args)) would resolve to \z -> fn_with_3_args(x, y, z) with x and y being captured variables.
yeah, although it would be pretty silly to do that in practice :big_smile:
Yeah, just making sure I understand the syntax
I'll chip in that I like this better than the special postfix number 4hours idea.
4hours is more terse, but 4.(hours) doesn't need anything special for it to work
Richard Feldman said:
like I guess there could theoretically be a syntax like
a, ..b -> cto mean "a function where the first argument has the typeabut we don't know what the other argument types are or how many there are" but that doesn't seem worth it just for this one case, especially considering the function would only be in that state very briefly before resolving to a totally normal function type (just like with "tags are values and functions")
hm, I think there would likely end up being demand for this :thinking:
the tag one is just a convenience but this would actually enable a new form of function composition as long as you didn't use type annotations
Is that a good thing? I don't know
I suspect if we did have those type annotations, it would lead to pointfree function composition libraries becoming a thing in the ecosystem, which I think would be a bad thing
That's my worry as well
If you allow point-free, I guarantee it'll happen
yeah that makes me like this design a lot less :sweat_smile:
lots to balance!
one straightforward solution is to use arg1.(fn, arg2, arg3) syntax, which doesn't have that problem, but does have the problem of being aesthetically unpopular instead :big_smile:
Doesn't arg1.(fn)(arg2, arg3) work without making it work via partial application? You just need to desugar arg1.(fn) to arg1.(fn)()
it can work, it's just strange in terms of expression boundaries
everywhere else in the language, ) immediately ends the parenthetical expression
but in this one case, what it does would be dependent on whether there's a ( immediately after it
in which case it syntactically means something different
we could totally do that, it's just inconsistent with how ) works everywhere else
You're right, but
.> looks weird.pass_to() is two wordsSo what's left?
None of this really enables point free programming. Not in any useful form. So I think worrying about tacit or point free based on this limited form of partial application is misguided. We also could simply not allow it be used for partial application and always require all args if we are worried.
We also could ... always require all args if we are worried.
That's what arg1.(fn)(arg2, arg3) is, right?
Well, depends if you allow it to be used via partial application or require all args. That is a choice we have control over.
Also, what is the worry about partial application? It is trivial to do in roc today with a wrapping lambda.
Yeah, I think we have to require all args for this to be viable
Or do explicitly with nested lambdas
The worry is that making it easy means people write arcane code with pointfree and the like
How?
If you make it awkward, the "pit of success" is more normal code
I simply don't see the concern
Let me try to write an example that type checks...
Sam Mohr said:
The worry is that making it easy means people write arcane code with pointfree and the like
I think this is an overblown concern personally. Go look at all of the pieces that Haskell or other languages have to enable good point free. This one piece simply isn't it.
Either way, we can always restrict to requiring all args. So I'm not concerned. We can do \y -> x.(fn)(y) and require it to be explicit.
Or we can open some partial application and allow simply x.(fn).
This feels pretty minor to me compared to in general enabling the x.(fn)(y, z) syntax.
I think it is a great and understandable way to enable local function method style dispatch.
More robust than any of the other suggestions.
if x.(fn) is where fn takes one arg, then that looks good to me
Also, I think that x.(hours) as sugar or as partial application will be equally understandable to most people. So it doesn't really worry me. People will just get used to it existing and how it works.
I don't write pointfree, so I'm having a hard time getting the args backwards on purpose in developing an example...
Here's the JS example I'm trying to translate
I don't think we could support compose as it's written there, but maybe we'd have to for this to work as "partial application"?
Yeah, I don't think compose could work in general in roc. Could force it with a list of closures as the first arg and a list of tagged elements as the second arg, but that is very painful to use.
Let me come back to this, I'm almost done with the unit type PR and want to stop thinking about it haha
Sam Mohr said:
If you allow point-free, I guarantee it'll happen
It'll happen if it goes through as-is but if everybody repeats that it's a terrible idea then it'll be restricted to people wanting to show off fun tricks. Clojure has macros and macro heavy code is about as good of an idea as point-free heavy programming but basically every introduction to the topic emphasizes that macros are a tool of last resort and you really should be doing functions if at all possible. The result is that I see binding macros ((let-cell [x 2] ...) where x is a cell in dataflow programming or (let-flow [a (process! "/foo") b (fetch! a)] ...) where a and b are nodes in an asynchronous action sequence) and the occasional control flow macro or top level macro to bind a DSL (e.g. core.async) but that's about it.
I still don't think this is enough for any form of significant point free code.
ok so let's say we were to embrace it as a first-class thing, such that this:
|val, fn| val.(fn)
...has this inferred type:
a, ..b -> c
I think we also need actual first-class zero-arg functions for that to work, or else b can never be empty, which in turn means you can never unify one of these with anything to get a 1-arg function
otherwise you couldn't call that function passing a fn that only takes 1 arg
Does b need to be empty? Couldn't it resolve to just the unit arg?
well there's a difference between Str, () -> Str and Str -> Str
Ah
And ..() should resolve to nothing rather than to ()
well I'm not saying what it should or shouldn't do, just that we need some separate concept of "an empty set of args" for that to work :big_smile:
that we don't currently have
Yeah, I would vote for just not doing partial application, keeping things simple, and just requiring all args when using x.(fn)(y, z) with the suger to map from x.(fn) to x.(fn)()
Way simpler and just slots right in
It also is sugar very similar to fn() to fn(())
To clarify, if the partial application was to just slot into the type system, I have no issues with it. Since it doesn't just slot in, I definitely vote for the simpler sugar solution.
yeah so in that design it's syntactically unusual in that the meaning of the) changes depending on whether there's a ( right after it, but it's more aesthetically appealing than arg1.(fn, arg2, arg3)
We can actually implement this syntax now without methods! We should try it out
Richard Feldman said:
yeah so in that design it's syntactically unusual in that the meaning of the
)changes depending on whether there's a(right after it, but it's more aesthetically appealing thanarg1.(fn, arg2, arg3)
I don't quite follow this. Isn't it just that .( is a special leading indicator?
just that in general, in the language today, whenever you see an expression that ends in ), whether it's (a + b) or fn(arg1, arg2) or (if a then b else c) it's always the case that once you hit that ) you've ended the expression no matter what comes after it
this would be the only place in the language where you have to scan ahead further after the ) to find out whether the expression has ended or not
because if it's a.(b)(c), that now means a.(b) retroactively works differently than if it's a.(b) followed by anything other than a (
nothing else in the language has that characteristic; everywhere else, as soon as you see ) it's always unconditionally the end of the expression
that's what I mean about the partial application idea making it more consistent
because it would mean that the ) is indeed still ending the expression, and returning a partially-applied function
not sure if I'm explaining it clearly though :sweat_smile:
maybe another way to think about it is that from a parser perspective, the partial application design doesn't need to do a lookahead to figure out what a.(b) means - it just already can end the AST node right there for sure
whereas in the non-partial-application design, the parser does need to lookahead by 1 byte to see if the ) in a.(b) happens to be followed by exactly (, in which case it keeps going and that "call the function" AST node will become different
And even if it's not that bad for the parser to do this, a human reading Roc having to do this every time they see a function call will add up, even if it's pretty minor
Though personally, I think that cost is so minor that it's worth avoiding partial application for
I'm sure people can get used to it, I'm just pointing out that it's an inconsistency with the rest of the grammar
I don't think it's a deal-breaker or anything
I do think this is something where it would be interesting to add it just so we can try it out and see how it feels in different scenarios
get some actual experience with it
With the one caveat that a.(b) is always invalid syntax today.
But yeah, that makes sense for why you might want it to just be partial application
126 messages were moved here from #ideas > static dispatch - pass_to alternative by Richard Feldman.
Karl said:
Sam Mohr said:
If you allow point-free, I guarantee it'll happen
It'll happen if it goes through as-is but if everybody repeats that it's a terrible idea then it'll be restricted to people wanting to show off fun tricks. Clojure has macros and macro heavy code is about as good of an idea as point-free heavy programming but basically every introduction to the topic emphasizes that macros are a tool of last resort and you really should be doing functions if at all possible. The result is that I see binding macros (
(let-cell [x 2] ...)wherexis a cell in dataflow programming or(let-flow [a (process! "/foo") b (fetch! a)] ...)whereaandbare nodes in an asynchronous action sequence) and the occasional control flow macro or top level macro to bind a DSL (e.g.core.async) but that's about it.
this is a very good point, and makes me reconsider how much of a downside it would be for this to enable that sort of thing (in the context of a culture that actively discourages it)
it does remind me that Elm is curried, and actually even ships with function composition operators, and yet pointfree function composition is culturally done very little in practice (slightly more than in Roc, where it isn't done at all, but not enough for it to be a serious problem imo)
so maybe if something like #ideas > static dispatch - tuple accessors and zero-arg functions ends up working out, and we end up with actual 0-arg functions, then this would become possible:
Richard Feldman said:
ok so let's say we were to embrace it as a first-class thing, such that this:
|val, fn| val.(fn)...has this inferred type:
a, ..b -> cI think we also need actual first-class zero-arg functions for that to work, or else
bcan never be empty, which in turn means you can never unify one of these with anything to get a 1-arg function
otherwise you couldn't call that function passing afnthat only takes 1 arg
@Ayaz Hafiz do you see any problem from a type inference perspective with :point_up: ?
(that is, the quote immediately before this)
sorry i don't quite follow.. does that desugar to fn(val) or fn(val, n1, .., nM) where n1...nM are any other number of parameters including 0?
the latter
the idea is that val.(fn) partially applies fn with val
the type (at least internally) would need to be more complex than that (you need to specify that ..b has the same length as the parameters in c - 1) but yeah looks fine
this is straight up currying though right?
like val.(fn) gives me a curried function
not completely curried
where the idea came from is that if arg1.(fn)(arg2, arg3) is the parens-and-commas syntax to replace arg1 |> fn arg2 arg3 then it would logically follow that arg1.(fn) would partially apply arg1 to fn
there seems to be consensus that foo.(bar) on its own is nice, e.g. I'm personally a big fan of 4.(hours) being possible
but then the question becomes, what happens if you want to call it with multiple arguments?
arg1.(fn, arg2, arg3) doesn't suggest that the second and third thing in the parens are being applied to the first thing in the parens
arg1.(fn)(arg2, arg3) does suggest that, but it also suggests that arg1.(fn) on its own would partially apply arg1 to fn
(my default thinking is that we shouldn't do the partial application thing, largely on the grounds that all else being equal I don't think Roc would be improved by adding an ad-hoc partial application feature, but I am curious whether the idea is even viable in case some future use case comes up)
Wouldn't this whole discussion apply to methods as well? [1, 2, 3].map could be a partially applied call to map in some other language. But as far as I know it was agreed that this isn't a good idea to avoid confusion with field access. So it doesn't work.
I feel like the same should be true for arg1.(fn). Especially because it already desugars to arg1.(fn)(). So if we allowed partial application, it would gain a different meaning based on fns type.
All that to say, I don't think adding partial application like this makes sense, and I think it can be thought very easily by adding a good error message that it isn't a possible thing to do
+1 it seems much easier to say from outset that the type of v.(f) has f: a -> b. if it turns out that it's a pain point that's not partially applied that can be added later
Last updated: Jun 16 2026 at 16:19 UTC