Stream: ideas

Topic: Single-Argument Function Syntax-Sugar


view this post on Zulip Declan Joseph Maguire (Oct 21 2023 at 02:25):

Proposal:
Introduce syntax sugar to allow abbreviated function definitions for single-variable functions. Currently, if I want to make a little anonymous helper function, e.g. I want a function that cubes the input, I need to write

\x -> x*x*x

which feels bad and has low information density on the screen, but doesn't majorly improve readability and arguably harms it. I propose the following syntax -

\x*x*x

to be valid exclusively for single-argument functions. The body of the function would be the first expression following the slash, assuming that infix operators take precedence over the slash, with the single undefined variable implicitly being the function argument.

While this could be abused by writing absurdly long expressions where the function argument only shows up somewhere in the middle, I do not think this would be a problem in practice. Most long functions are more ergonomically written over multiple lines, at which point most would name it, and at that point you'd probably follow the conventions of how that's written with an explicit argument at top (it'd look very odd otherwise, and looking bad is an underestimated force in policing language use) which would probably be enforced by opinionated IDEs.

It may be worth looking to other places where function definitions can be abbreviated (see also the .element notation for \x -> x.element), but this proposal is designed to be conservative, requiring extremely small changes to the compiler and no fundamental language changes, being mere syntax sugar.

view this post on Zulip Lakin Wecker (Oct 21 2023 at 02:27):

What will you name the variable? Always x?

view this post on Zulip Lakin Wecker (Oct 21 2023 at 02:27):

Or always the first non-const value that appears?

view this post on Zulip Brendan Hansknecht (Oct 21 2023 at 02:31):

Can you give some concrete examples of where you would expect this to come up? (preferably in both syntaxes)

view this post on Zulip Declan Joseph Maguire (Oct 21 2023 at 02:36):

The first undefined variable in the expression. If more than one appear, then it will be invalid just the same as elsewhere. Given the that the slash will clearly provide a context to the following expression, the compiler error in such a case will be able to clearly explain "You tried to use this syntax but you used multiple arguments", to paraphrase what it might say.

view this post on Zulip Lakin Wecker (Oct 21 2023 at 02:36):

I mean, in many exactly two languages (that I'm aware of) the "unnamed variable" is _

view this post on Zulip Lakin Wecker (Oct 21 2023 at 02:36):

scala, perl

view this post on Zulip Lakin Wecker (Oct 21 2023 at 02:36):

So I was just curious which name for the variable you were proposing.

view this post on Zulip Declan Joseph Maguire (Oct 21 2023 at 02:45):

Brendan Hansknecht said:

Can you give some concrete examples of where you would expect this to come up? (preferably in both syntaxes)

The biggest cases are function composition, and especially functions like List.map. So instead of

sensorOutput
|> List.map \val -> (val*val + 2.0)

you may instead have

sensorOutput
|> List.map \(val*val + 2.0)

I feel that even in this simple case, the syntax sugar is desirable. For far longer backpassing expressions (which I understand is the semi-canonical way Roc does long strings of function composition) this could significantly help readability. The main benefit ergonomically is it takes functions that act like a single expression and makes them visually match that, becoming a single visual unit. This may sound small, but I personally find it helps readability and the change is otherwise minor and very self-contained.

view this post on Zulip Declan Joseph Maguire (Oct 21 2023 at 02:47):

Lakin Wecker said:

I mean, in many exactly two languages (that I'm aware of) the "unnamed variable" is _

In Roc, _ means "discard/ignore this", and is generally a wildcard in case matching when we want a default behaviour.

view this post on Zulip Declan Joseph Maguire (Oct 21 2023 at 02:48):

Which is how it's conventionally used in dynamic languages like Python. Part of Roc's design philosophy is trying to make a statically typed, very safe language that feels as light and simple as dynamic languages, so some of the conventions flow from there

view this post on Zulip Lakin Wecker (Oct 21 2023 at 02:49):

Declan Joseph Maguire said:

Part of Roc's design philosophy is trying to make a statically typed, very safe language that feels as light and simple as dynamic languages

Out of all of the languages I've used, Scala does this extremely well. It's basically a functional python

view this post on Zulip Brendan Hansknecht (Oct 21 2023 at 02:51):

All I can say is that this looks super strange to me, but I get the base sentiment. I don't think I have any real feedback past that

view this post on Zulip Lakin Wecker (Oct 21 2023 at 02:57):

I'm fine with whatever choice. I just wanted to know if you're suggesting that \var * 2 means var is the name of the first variable. And does it work for \2 * var?

view this post on Zulip Declan Joseph Maguire (Oct 21 2023 at 02:57):

Perhaps my pure maths background is showing, but in the formal logic space it's common for expressions with unbound variables to implicitly be universally quantified, whereupon you may substitute concrete values to build concrete propositions. Plus I always just kinda resent the pomp of writing all the initial stuff for super short anonymous functions, which Roc exacerbates due to how wordy a nicely spaced arrow is (4 characters!)

view this post on Zulip Declan Joseph Maguire (Oct 21 2023 at 02:58):

Lakin Wecker said:

I'm fine with whatever choice. I just wanted to know if you're suggesting that \var * 2 means var is the name of the first variable. And does it work for \2 * var?

Assuming infix operators have priority, \2 * var ought to be interpreted as \(2*var) and thus be unambiguous.

view this post on Zulip Isaac Van Doren (Oct 21 2023 at 03:01):

While I could see this syntax being convenient in some cases, I _really_ like that there is exactly one way to define a function and I would rather that not become two. Even if small, it would then be another unfamiliar thing that a beginner has to look up when learning the language

view this post on Zulip Elias Mulhall (Oct 21 2023 at 03:04):

Second that. Elixir has & &1 * &2, which is an anonymous function that multiples two variables. I use it because it's frequently a nice shorthand, but it's one of those things you have to go over with a new dev some time in their first couple weeks.

view this post on Zulip Declan Joseph Maguire (Oct 21 2023 at 03:05):

I see that argument, however from a visual perspective the slash ends up being the "this is a function definition" marker. Also, in practice it just means that you omit the argument bit and just include the function body, so it can easily be explained as a mere abbreviation.

view this post on Zulip Declan Joseph Maguire (Oct 21 2023 at 03:07):

Elias Mulhall said:

Second that. Elixir has & &1 * &2, which is an anonymous function that multiples two variables. I use it because it's frequently a nice shorthand, but it's one of those things you have to go over with a new dev some time in their first couple weeks.

Thats why I chose to restrict it to the single variable case. Once you have multiple variables, you need to track the order, which gives janky and unintuitive notation like that. I definitely don't want that added to Roc!

view this post on Zulip Declan Joseph Maguire (Oct 21 2023 at 03:09):

Don't get me wrong, if there's an elegant and consistent way to do that, I'd love to have it. I just think that it already exists, and it's called a regular lambda expression.

view this post on Zulip Lakin Wecker (Oct 21 2023 at 03:12):

I absolutely love _ as lambda shortcut in scala.

view this post on Zulip Lakin Wecker (Oct 21 2023 at 03:13):

someList.map(_.toString)

is so much cleaner than

someList.map(x => x.toString())

view this post on Zulip Lakin Wecker (Oct 21 2023 at 03:13):

Like, why am I forced to name the variable?

view this post on Zulip Declan Joseph Maguire (Oct 21 2023 at 03:16):

_ is a name. Though I do think there's good reason to ask people to use x as the conventional name, just as i j k are conventionally loop count variable in imperative languages. While it would match scala to make it _, it conflicts with existing Roc conventions plus I think it's a magic symbol as far as the compiler is concerned. Also, who wants to write \_*_*_?

view this post on Zulip Lakin Wecker (Oct 21 2023 at 03:17):

Declan Joseph Maguire said:

_ is a name.

Yes, and I prefer it over other ones. :grinning:

view this post on Zulip Declan Joseph Maguire (Oct 21 2023 at 03:17):

Like, anyone who's done high school maths will instantly understand "oh, x is our argument/the thing you substitute into".

view this post on Zulip Declan Joseph Maguire (Oct 21 2023 at 03:18):

Lakin Wecker said:

Declan Joseph Maguire said:

_ is a name.

Yes, and I prefer it over other ones. :D

Yes, but it'd introduce a really nasty inconsistency semantically.

view this post on Zulip Anton (Oct 21 2023 at 11:43):

The first undefined variable in the expression.

I believe this would noticeably complicate the desugaring.

I also like _ from scala although I believe we've discussed that before and decided against it.
I can't find the old conversation unfortunately.

view this post on Zulip Sky Rose (Oct 23 2023 at 01:41):

Being able to pick a name would be nice. One reason I avoid using the & &1 syntax in elixir is that you can't use a name, and that makes the little functions so much more mysterious. So I usually use the longer fn bar -> foo(bar) end syntax so I can choose a good name unless it's really clear what the argument is. Something that's short and lets you pick a name would be the best of both worlds.

view this post on Zulip Brendan Hansknecht (Oct 23 2023 at 01:45):

Given lambdas are so short why is this needed? Just \x -> ... pick any single letter variable. Only save 4 characters and a few spaces.

view this post on Zulip Declan Joseph Maguire (Oct 23 2023 at 07:50):

@Anton I don't know if it would change the implications for the desugaring process, but it would be the ONLY undefined variable in the expression body, which would be the first one, because the sugar may only work on single variable lambdas.

view this post on Zulip Declan Joseph Maguire (Oct 23 2023 at 08:05):

@Brendan Hansknecht For me it's more about reading than writing. It turns the function into a single visual unit, or close to, which I find useful when dealing with little helper functions - I like the idea of having an expression, and a function which computes that expression, look basically the same. Plus, I thought it would be easy to implement and have few consequences elsewhere.

However, given more time to think on it, it might introduce a readability problem if people decide to choose their free term to have some long descriptive name that looks like it came from elsewhere - you'd need to stop and think about where the variable names (didn't) come from. This could tip things towards the scala _ thing, where it's always the function argument, although I still don't love it visually or how it conflicts with the use of underscore in match statements. Perhaps that last part could partially resolved by a shift in perspective - not "the place to throw something out", but "an unnamed value", whereever it came from.

view this post on Zulip Brian Carroll (Oct 23 2023 at 08:09):

I agree with focusing in readability but I think it's easier to read code in general if there's only one syntax for all lambdas, rather than having a convenience that only works in specific conditions. Those conditions have to be learned.
From a compiler dev point of view, I think the trickiest part would be implementing good error messages for when people do something wrong, because it's such an ad-hoc edge case.

view this post on Zulip Declan Joseph Maguire (Oct 23 2023 at 08:09):

The two big cases I see it being nice for, are when you need to pass a trivial operation to something like List.map, and when you have a long chain of pipe operators - it would help the visual flow of "do a thing, do a thing, then this, then this".

view this post on Zulip Declan Joseph Maguire (Oct 23 2023 at 08:12):

@Brian Carroll very true. I thought when I posted this that the benefits well outweighed the harms, but I'm a lot more ambivalent about it now. The reason why I was less worried about it is that I thought of it as an abbreviation, like doing an if-statement on one line when it makes sense. Then again I understand many people loathe that, and other useful tools like continue or return in a loop body

view this post on Zulip Norbert Hajagos (Oct 26 2023 at 15:43):

I think it makes simple code a little more compact (stealing the simplicity from it if someone reads it who does no know the syntax). Simple code is good. Compact code feels good to write, but not neccesarily better than simple.

There is also the case where ppl would use it to define their big, 1 arg functions. V8 had to optimize their closures after ES6. They didn't think ppl would write

const topLvlFunc = (a) => {...}

all over their codebase. But they do.
This sugar would not come with performance drawbacks, but it would get abused, just like any other feature. I also really like that top level functions have the same syntax as closures. 1 goto way to do things is nice.

view this post on Zulip Declan Joseph Maguire (Oct 26 2023 at 21:50):

Well damn. I'd have hoped that it's be too awkward for long functions and people would naturally gravitate to the unabreviated version except for little helper functions. But I guess real life experience seals it.

Still annoyed by the lack of compactness, but your ES6 example sounds good enough to throw out my proposal, or at best totally overhaul it.

Honestly breaking it over 2 lines makes it more visually compact, at the cost of hogging a line. Like this

\x ->
x*x*x + 1

looks appealing to me, but not if you stuck it in the middle of a bunch of pipes

view this post on Zulip Norbert Hajagos (Oct 27 2023 at 11:07):

I agree with the new line part. This is a good solution if you don't want to pipe.

view this post on Zulip Declan Joseph Maguire (Oct 27 2023 at 11:21):

Hmm. Maybe with some cleverness we can omit the need for extra lines

arbitraryVar |>
funcOne |>
funcTwo |> \x->
x*x*x + 1 |>
finalFunc

view this post on Zulip Declan Joseph Maguire (Oct 27 2023 at 11:23):

Hmm. The lack of alignment on |> looks pretty bad, but I like how it makes the first line obviously feed to somewhere else, while leaving the final function on the bottom without the extra operator. Works decently well with split lambda too.

view this post on Zulip Declan Joseph Maguire (Oct 27 2023 at 11:24):

Definitely would benefit from syntax highlighting. I thought Zulip already had that, I thought I got the syntax codeblock notation right.

view this post on Zulip Norbert Hajagos (Oct 27 2023 at 11:26):

So, to complain about the color of the bikeshed, I would do the same as in other languages, except we have |> here and not a dot

arbitraryVar
|> funcOne
|> funcTwo
|> \x -> x*x*x + 1
|> finalFunc

I use elm for syntax highlighting in zulip :)

view this post on Zulip Declan Joseph Maguire (Oct 27 2023 at 11:39):

Just realised my idea looks awful when you have extra arguments.

arbitraryVar
|> funcOne
|> funcTwo auxVarOne
|> \x,y ->
    x*x*x + y auxVarTwo
|> finalFunc

Okay that looks decent. The extra variable in the lambda looks kinda jank, but it's a weird scenario anyway (why wouldn't you just bake it into the lambda?). I mean by this point we've lost the ostensible topic of this thread and are just playing with lambdas and pipes.

view this post on Zulip Declan Joseph Maguire (Oct 27 2023 at 11:41):

I like having the function body match where the first argument goes, it makes it 2 spaces without the pipe and 4 with. Or you can just have it always indent by 4 and the pipe case solves itself.

view this post on Zulip Declan Joseph Maguire (Oct 27 2023 at 12:26):

Do we have a good way of destructuring a function output, such that one element is passed to the next function, with others optionally bound to defs for later use? Obviously if you're just getting a single value out you can just use .foo on the record to pull out the foo field, but I'm more curious about the case where you pull other values out for later use.

view this post on Zulip Brendan Hansknecht (Oct 27 2023 at 12:46):

For tuples you also have the .0 syntax

view this post on Zulip Brendan Hansknecht (Oct 27 2023 at 12:46):

Otherwise, for tags and such, you would have to use a lambda to destructure.

view this post on Zulip Brendan Hansknecht (Oct 27 2023 at 12:47):

There is no way to bind a value. I don't think that is possible at all within a pipeline even with a verbose syntax.

view this post on Zulip Declan Joseph Maguire (Oct 27 2023 at 12:53):

Really? That seems like a strong claim. Like I can think of introducing some ugly ad hoc thing to the language that acts as a function but lets you do a binding thing. Something like

arbitraryVal
|> funcOne
|> SPECIAL_THING {$x, y, z:q}
|> funcTwo auxVar
|> fncThree q z
|> finalFunc

Like obviously that's awful, but it's technically a syntax that would allow record destructuring in a pipeline

view this post on Zulip Declan Joseph Maguire (Oct 27 2023 at 12:54):

Here I'm imagining the $ as a special marker that tells SPECIAL_THING which value to pass. Fugly, verbose, but technically possible.

view this post on Zulip Declan Joseph Maguire (Oct 27 2023 at 12:54):

Wait unless you meant not possible at all within Roc's existing syntax

view this post on Zulip Brendan Hansknecht (Oct 27 2023 at 13:51):

So like you can do:

someVal
|> func
|> .someField
|> OtherFunc

view this post on Zulip Brendan Hansknecht (Oct 27 2023 at 13:52):

But imagine that you have the same pipeline, but there is also a field .x that you want to bind for use after the pipeline.

view this post on Zulip Brendan Hansknecht (Oct 27 2023 at 13:52):

That is not possible without breaking the pipeline

view this post on Zulip Declan Joseph Maguire (Oct 27 2023 at 13:53):

Do you mean within Roc as it presently exists, or in general?

view this post on Zulip Brendan Hansknecht (Oct 27 2023 at 13:54):

someVal
|> func
|> \val ->
    # x is a local, fails to bind in a way to escape the pipeline
    x = val.x
    val.someField
|> otherFunc

view this post on Zulip Brendan Hansknecht (Oct 27 2023 at 13:55):

I mean anything can be done with changes, though not sure that some way to bind in the middle of a function is likely to get added.

view this post on Zulip Brendan Hansknecht (Oct 27 2023 at 13:56):

If you wanted to save x in this case, you would have to split the pipeline or use a subpipline to update the other values while keeping around x.

I guess you could also make all following functions just pass through x, ignoring it

view this post on Zulip Declan Joseph Maguire (Oct 27 2023 at 13:57):

I'm not saying add changes, I was just confused because I thought your earlier comment was about pipelines in any concievable form and not just Roc.

While part of me now wants to figure out what would need to be done to make it work, I suspect it'd take some radical changes to the syntax and cost a whole lot to gain a whole little.

view this post on Zulip Brendan Hansknecht (Oct 27 2023 at 14:00):

To me it kinda feels like a pipeline with side effects, which makes me nervous, but idk, probably not really a concern cause we don't have shadowing, so all names would be newly bound

view this post on Zulip Declan Joseph Maguire (Oct 27 2023 at 14:03):

What's a "prime" in this context? And yes I was only imagining new bindings

view this post on Zulip Brendan Hansknecht (Oct 27 2023 at 14:11):

Typo, meant pipeline

view this post on Zulip Declan Joseph Maguire (Oct 27 2023 at 14:14):

If I ever think of a good way to do it I'll be sure to post it, but I doubt there's a worthwhile solution. I think part of what bugs me is the assymetry - I can put extra arguments into my function, so why can't I pull them back out of an output?

view this post on Zulip Brendan Hansknecht (Oct 27 2023 at 18:34):

Totally fair. Yeah, no great way today, just some ok versions.

view this post on Zulip Brendan Hansknecht (Oct 27 2023 at 18:34):

But nothing really ergonomic in many cases

view this post on Zulip Brendan Hansknecht (Oct 27 2023 at 18:34):

Probably the most common would just be to break the pipe, save the temporary, and then start a new pipe

view this post on Zulip Declan Joseph Maguire (Oct 27 2023 at 23:28):

I have a lot of experience with diagrammatic notation, mathematics where the elements are boxes with a series of upwards and downwards pointing lines sticking out from it, and lines can be hooked up. Think visual programming, except it's mathematically rigourous and the boxes and lines constitute the mathematics in the same way symbols do in algebra. And when I see a pipe, I see a bunch of boxes hooked together in a row, with optional up-pointing extra legs that can be "contracted" with extra values. But what about when some legs stick down out of intermediate functions?

view this post on Zulip Declan Joseph Maguire (Oct 27 2023 at 23:29):

In a general loopless diagram, it should end up looking like a braid, with some legs terminating in a discard.


Last updated: Jun 16 2026 at 16:19 UTC