Stream: ideas

Topic: static dispatch - partial application syntax


view this post on Zulip Richard Feldman (Jan 12 2025 at 18:41):

here's an interesting implication I just realized of the .(foo)(bar, baz) syntax: as written, it's essentially a partial application operator

view this post on Zulip Richard Feldman (Jan 12 2025 at 18:41):

if I write foo(bar, baz) - or, equivalently, (foo)(bar, baz) - then in either case foo must be a function that takes 2 arguments

view this post on Zulip Richard Feldman (Jan 12 2025 at 18:42):

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)

view this post on Zulip Richard Feldman (Jan 12 2025 at 18:43):

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

view this post on Zulip Richard Feldman (Jan 12 2025 at 18:43):

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)

view this post on Zulip Richard Feldman (Jan 12 2025 at 18:46):

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)

view this post on Zulip Richard Feldman (Jan 12 2025 at 18:47):

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:

view this post on Zulip Richard Feldman (Jan 12 2025 at 18:48):

either way, there is some neat visual symmetry to it, and I like that it's just as concise as the method-call style

view this post on Zulip Richard Feldman (Jan 12 2025 at 18:51):

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.

view this post on Zulip Richard Feldman (Jan 12 2025 at 18:51):

so kind of as an advanced concept

view this post on Zulip Richard Feldman (Jan 12 2025 at 18:51):

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

view this post on Zulip Richard Feldman (Jan 12 2025 at 18:53):

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

view this post on Zulip Richard Feldman (Jan 12 2025 at 18:54):

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

view this post on Zulip Richard Feldman (Jan 12 2025 at 18:54):

so in that world, there's no syntax sugar for avoiding the need to do e.g. .(Ok)()

view this post on Zulip Niclas Ahden (Jan 12 2025 at 18:55):

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.

view this post on Zulip Richard Feldman (Jan 12 2025 at 18:57):

yeah I don't know if it's awesome or strange (or both!) etc. but it's definitely interesting!

view this post on Zulip Richard Feldman (Jan 12 2025 at 18:57):

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

view this post on Zulip Richard Feldman (Jan 12 2025 at 18:58):

e.g. 4.(hours) :heart_eyes:

view this post on Zulip Richard Feldman (Jan 12 2025 at 18:58):

4 |> hours and 4.pass_to(hours) don't read nearly as nicely to me

view this post on Zulip Brendan Hansknecht (Jan 12 2025 at 18:58):

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

view this post on Zulip Richard Feldman (Jan 12 2025 at 19:01):

haha yeah there's a question of cultural norms for sure

view this post on Zulip Richard Feldman (Jan 12 2025 at 19:01):

one of the things Evan always talked about was how the timing of introducing advanced features matters a lot

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

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

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

because there are already community norms around using the basics etc.

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

so it's more likely to be used sparingly and less likely to be used in ways/places where it's going overboard

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

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!)

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

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)

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

but I think that's something people could get used to pretty quickly once you see the pattern

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

in Ruby I don't think people would just try 1.day.ago without having seen it somewhere else first

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

so I imagine similarly you see 1.(day).(ago!) and follow that pattern

view this post on Zulip Niclas Ahden (Jan 12 2025 at 19:15):

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.

view this post on Zulip Niclas Ahden (Jan 12 2025 at 19:16):

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!.

view this post on Zulip Niclas Ahden (Jan 12 2025 at 19:17):

Yeah, I think 1.(day).(ago!) is neat. now!() + 8.(hours) too. Scratch my concerns!

view this post on Zulip Richard Feldman (Jan 12 2025 at 19:18):

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:

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

(fixed in the edit)

view this post on Zulip Niclas Ahden (Jan 12 2025 at 19:22):

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!

view this post on Zulip Karl (Jan 12 2025 at 19:32):

So if .() is special syntax for partial application then both .(|x| add(x, 2)) and .(add)(2) are valid, right?

view this post on Zulip Richard Feldman (Jan 12 2025 at 19:39):

yep!

view this post on Zulip Richard Feldman (Jan 12 2025 at 19:39):

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

view this post on Zulip Richard Feldman (Jan 12 2025 at 19:41):

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)

view this post on Zulip Richard Feldman (Jan 12 2025 at 19:45):

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")

view this post on Zulip Niclas Ahden (Jan 12 2025 at 20:16):

Is arg2.(arg1.(fn))() valid? (where fn : _ , _ -> _)

Distinct from arg2.(arg1.(fn)()), where fn : _ -> (_ -> _)

view this post on Zulip Richard Feldman (Jan 12 2025 at 20:34):

syntactically valid? I suppose, but at that point I think we're outside the realm of possible usefulness :big_smile:

view this post on Zulip Niclas Ahden (Jan 12 2025 at 20:51):

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.

view this post on Zulip Brendan Hansknecht (Jan 12 2025 at 20:56):

Richard Feldman said:

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

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.

view this post on Zulip Brendan Hansknecht (Jan 12 2025 at 20:57):

This means if I want triple partial application, it would be
z.(y.(x.(fn_with_3_args)))

view this post on Zulip Brendan Hansknecht (Jan 12 2025 at 20:58):

And that would be the same as fn_with_3_args(x, y, z)

view this post on Zulip Brendan Hansknecht (Jan 12 2025 at 21:01):

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.

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

yeah, although it would be pretty silly to do that in practice :big_smile:

view this post on Zulip Brendan Hansknecht (Jan 12 2025 at 21:04):

Yeah, just making sure I understand the syntax

view this post on Zulip Karl (Jan 12 2025 at 21:12):

I'll chip in that I like this better than the special postfix number 4hours idea.

view this post on Zulip Sam Mohr (Jan 12 2025 at 21:24):

4hours is more terse, but 4.(hours) doesn't need anything special for it to work

view this post on Zulip Richard Feldman (Jan 12 2025 at 21:59):

Richard Feldman said:

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")

hm, I think there would likely end up being demand for this :thinking:

view this post on Zulip Richard Feldman (Jan 12 2025 at 22:00):

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

view this post on Zulip Sam Mohr (Jan 12 2025 at 22:01):

Is that a good thing? I don't know

view this post on Zulip Richard Feldman (Jan 12 2025 at 22:09):

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

view this post on Zulip Sam Mohr (Jan 12 2025 at 22:10):

That's my worry as well

view this post on Zulip Sam Mohr (Jan 12 2025 at 22:11):

If you allow point-free, I guarantee it'll happen

view this post on Zulip Richard Feldman (Jan 12 2025 at 22:11):

yeah that makes me like this design a lot less :sweat_smile:

view this post on Zulip Richard Feldman (Jan 12 2025 at 22:15):

lots to balance!

view this post on Zulip Richard Feldman (Jan 12 2025 at 22:50):

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:

view this post on Zulip Sam Mohr (Jan 12 2025 at 22:52):

Doesn't arg1.(fn)(arg2, arg3) work without making it work via partial application? You just need to desugar arg1.(fn) to arg1.(fn)()

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

it can work, it's just strange in terms of expression boundaries

view this post on Zulip Richard Feldman (Jan 12 2025 at 22:54):

everywhere else in the language, ) immediately ends the parenthetical expression

view this post on Zulip Richard Feldman (Jan 12 2025 at 22:55):

but in this one case, what it does would be dependent on whether there's a ( immediately after it

view this post on Zulip Richard Feldman (Jan 12 2025 at 22:55):

in which case it syntactically means something different

view this post on Zulip Richard Feldman (Jan 12 2025 at 22:55):

we could totally do that, it's just inconsistent with how ) works everywhere else

view this post on Zulip Sam Mohr (Jan 12 2025 at 22:56):

You're right, but

view this post on Zulip Sam Mohr (Jan 12 2025 at 22:56):

So what's left?

view this post on Zulip Brendan Hansknecht (Jan 12 2025 at 22:56):

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.

view this post on Zulip Sam Mohr (Jan 12 2025 at 22:57):

We also could ... always require all args if we are worried.

That's what arg1.(fn)(arg2, arg3) is, right?

view this post on Zulip Brendan Hansknecht (Jan 12 2025 at 22:58):

Well, depends if you allow it to be used via partial application or require all args. That is a choice we have control over.

view this post on Zulip Brendan Hansknecht (Jan 12 2025 at 22:58):

Also, what is the worry about partial application? It is trivial to do in roc today with a wrapping lambda.

view this post on Zulip Sam Mohr (Jan 12 2025 at 22:58):

Yeah, I think we have to require all args for this to be viable

view this post on Zulip Brendan Hansknecht (Jan 12 2025 at 22:59):

Or do explicitly with nested lambdas

view this post on Zulip Sam Mohr (Jan 12 2025 at 22:59):

The worry is that making it easy means people write arcane code with pointfree and the like

view this post on Zulip Brendan Hansknecht (Jan 12 2025 at 22:59):

How?

view this post on Zulip Sam Mohr (Jan 12 2025 at 22:59):

If you make it awkward, the "pit of success" is more normal code

view this post on Zulip Brendan Hansknecht (Jan 12 2025 at 22:59):

I simply don't see the concern

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

Let me try to write an example that type checks...

view this post on Zulip Brendan Hansknecht (Jan 12 2025 at 23:00):

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.

view this post on Zulip Brendan Hansknecht (Jan 12 2025 at 23:03):

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.

view this post on Zulip Brendan Hansknecht (Jan 12 2025 at 23:04):

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.

view this post on Zulip Brendan Hansknecht (Jan 12 2025 at 23:04):

I think it is a great and understandable way to enable local function method style dispatch.

view this post on Zulip Brendan Hansknecht (Jan 12 2025 at 23:05):

More robust than any of the other suggestions.

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

if x.(fn) is where fn takes one arg, then that looks good to me

view this post on Zulip Brendan Hansknecht (Jan 12 2025 at 23:06):

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.

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

I don't write pointfree, so I'm having a hard time getting the args backwards on purpose in developing an example...

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

Here's the JS example I'm trying to translate

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

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"?

view this post on Zulip Brendan Hansknecht (Jan 12 2025 at 23:09):

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.

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

Let me come back to this, I'm almost done with the unit type PR and want to stop thinking about it haha

view this post on Zulip Karl (Jan 12 2025 at 23:32):

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.

view this post on Zulip Brendan Hansknecht (Jan 12 2025 at 23:34):

I still don't think this is enough for any form of significant point free code.

view this post on Zulip Richard Feldman (Jan 12 2025 at 23:37):

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

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

otherwise you couldn't call that function passing a fn that only takes 1 arg

view this post on Zulip Brendan Hansknecht (Jan 12 2025 at 23:42):

Does b need to be empty? Couldn't it resolve to just the unit arg?

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

well there's a difference between Str, () -> Str and Str -> Str

view this post on Zulip Brendan Hansknecht (Jan 12 2025 at 23:43):

Ah

view this post on Zulip Brendan Hansknecht (Jan 12 2025 at 23:43):

And ..() should resolve to nothing rather than to ()

view this post on Zulip Richard Feldman (Jan 12 2025 at 23:44):

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:

view this post on Zulip Richard Feldman (Jan 12 2025 at 23:44):

that we don't currently have

view this post on Zulip Brendan Hansknecht (Jan 12 2025 at 23:47):

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)()

view this post on Zulip Brendan Hansknecht (Jan 12 2025 at 23:47):

Way simpler and just slots right in

view this post on Zulip Brendan Hansknecht (Jan 12 2025 at 23:48):

It also is sugar very similar to fn() to fn(())

view this post on Zulip Brendan Hansknecht (Jan 12 2025 at 23:53):

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.

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

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)

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

We can actually implement this syntax now without methods! We should try it out

view this post on Zulip Brendan Hansknecht (Jan 13 2025 at 00:19):

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 than arg1.(fn, arg2, arg3)

I don't quite follow this. Isn't it just that .( is a special leading indicator?

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

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

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

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

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

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 (

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

nothing else in the language has that characteristic; everywhere else, as soon as you see ) it's always unconditionally the end of the expression

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

that's what I mean about the partial application idea making it more consistent

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

because it would mean that the ) is indeed still ending the expression, and returning a partially-applied function

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

not sure if I'm explaining it clearly though :sweat_smile:

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

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

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

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

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

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

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

Though personally, I think that cost is so minor that it's worth avoiding partial application for

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

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

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

I don't think it's a deal-breaker or anything

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

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

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

get some actual experience with it

view this post on Zulip Brendan Hansknecht (Jan 13 2025 at 02:03):

With the one caveat that a.(b) is always invalid syntax today.

view this post on Zulip Brendan Hansknecht (Jan 13 2025 at 02:03):

But yeah, that makes sense for why you might want it to just be partial application

view this post on Zulip Notification Bot (Jan 13 2025 at 04:26):

126 messages were moved here from #ideas > static dispatch - pass_to alternative by Richard Feldman.

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

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] ...) 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.

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)

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

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)

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

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 -> 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

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

@Ayaz Hafiz do you see any problem from a type inference perspective with :point_up: ?

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

(that is, the quote immediately before this)

view this post on Zulip Ayaz Hafiz (Jan 13 2025 at 05:45):

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?

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

the latter

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

the idea is that val.(fn) partially applies fn with val

view this post on Zulip Ayaz Hafiz (Jan 13 2025 at 06:03):

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

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

this is straight up currying though right?

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

like val.(fn) gives me a curried function

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

not completely curried

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

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

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

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

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

but then the question becomes, what happens if you want to call it with multiple arguments?

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

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

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

arg1.(fn)(arg2, arg3) does suggest that, but it also suggests that arg1.(fn) on its own would partially apply arg1 to fn

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

(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)

view this post on Zulip Kilian Vounckx (Jan 13 2025 at 06:41):

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.

view this post on Zulip Kilian Vounckx (Jan 13 2025 at 06:43):

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

view this post on Zulip Ayaz Hafiz (Jan 13 2025 at 06:44):

+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