Simple partial application

Generally speaking when one value constrains another with dependent types you can do some value inference . This is usually what implicit arguments are used for tho in Agda and Idris.

I take the long view; full dependent typing is a very large feature that I don't expect will be accepted anytime soon, but I would like to see it in the next 10 years or so. If and when that happens, we shouldn't have painted ourselves into a corner design-wise. Besides, there have been RFCs and designs to add full dependent types to Rust. However these were not accepted.

Const generics as framed in the accepted RFC constitute Pi-types for a limited subset of values: those that can be computed at compile time. That is, a const generic function can be seen as: Π (a: A) -> B(a) with the side condition that can_ctfe_eval(a).


On the partial! macro -- while it is nice that you can do that as a macro, and I'd like to thank @Emerentius for the effort, it concerns me that it becomes longer for the common case:

partial!(some_fn, arg0, _,)
|a, b| some_fn(arg0, a)
some_fn(arg0, _)
some_fn(arg0)

Using the lambda seems better to me if I have no other choice and I don't have to use a dependency for it (or convince someone else to pull it in); Also, annotating _ is noise to me in and of itself; partial application can usually be inferred from the HoF function that consumes the lambda (because iter.map(..) requires Fn(A) -> B).

1 Like

Can you give examples? I have very little experience with such languages, and I'd like to see how such a construction solves real-world problems Rust can't solve effectively today, which judicious use of const generics can't win us. I especially think we disagree on the point that reserving syntax for a feature that we may not see for 10 years (which is a long, long time in the lifespan of a language, especially one as young as Rust) is a wise reason to present stop energy. (For the record, I don't think dependent types are a bad idea, but I don't think they are crucial to solving the problems Rust wants to solve. I really don't want Rust to turn into a kitchen sink language like C++)

Also, I'll point out that my proposed syntax, || _, allows use of value inference in expression contexts that are not inside of a closure, which I can imagine being < 95% of all expressions, not including || {..} closures (from which we should forbid from any such syntax anyways).

I think there is a problem in using || _ + 5.

Currently, we have the property that a closure always directly acknowledges how many arguments it’s receiving. This is done by the number of identifiers between the bars: |one, two, three| and so on.

Of course, you can ignore a parameter by giving it to the black hole: |_|, and there’s a proposal to allow using |..| to ignore any number of parameters, as can be done for tuples when destructuring.

However, if you allow || _ + 5, then you have a || closure that takes an argument. This seems undesirable.


Side note: I made this topic mostly to spark discussion; I’m not sure if this is actually desirable, or if it can be specified in a strong enough fashion for Rust.

3 Likes

Here’s another, not entirely serious idea:

$1 * $1 + $2

This is a closure taking two arguments and returning the square of the first arg plus the second arg.

1 Like

I wanted to collect some of the proposed syntaxes and their pros and cons, in no particular order. Throughout, we want to write the following with our shorthand syntax.

fn foo(a: i32, b: i32, c: i32) -> i32 { a + b + c }
let f = |c| foo(0, 1, c);

Haskell-style currying

let f = foo(0, 1);

There was brief discussion about introducing curried functions as part of the type system, but that’s outside the scope of this discussion (imo).

Pros:

  • No new syntax.
  • Limited to partial applications instead of complex expressions.
  • Intuitive and natural for users of functional languages.

Cons:

  • A partial application can’t be visually distinguished from the full function call.
  • std and other crates do not have their function parameters ordered to take advantage of this, like in Haskell.
  • No good way to call inherent/trait methods, except for UFCS.

Scala-style Wunderbar

let f = foo(0, 1, _);
// or even
let f = 0 + 1 + _;

Though originally proposed for partial function application, it can be extended to arbitrary closures, using semantics like Scala’s.

Pros:

  • Uses the pre-existing _ keyword.
  • Consistent with other uses of _ (in the sense of “I don’t want to name this variable because its name doesn’t matter”).

Cons:

  • It’s unclear where the lambda starts in this expression: foo().bar(_). Is foo() inside the closure, or is its value captured as part of it?
  • How do I specify that the output closure should be move? move foo(_) doesn’t work, because now &move foo(_) is ambiguous.
  • What does foo(_, _) mean? |y| |x| foo(x, y) or |x, y| foo(x, y)?
  • Inconsistent with other uses of _ (where it either stands for “ignore this value” (patterns) or “infer this type/region” (types)). Note: given that it already has two inconsistent uses, I argue that this is not a strong argument against _.

Scala Wunderbar but with closure bars

let f = || foo(0, 1, _);
let f = |_| foo(0, 1, _);
let f = |..| foo(0, 1, _);
let f = |?| foo(0, 1, _);

Included are some ideas for what should go in the closure bars. (Disclaimer, this is my proposed syntax.)

Pros:

  • Solves all but the last con of the regular wunderbar.
  • Closure bars are still present when a closure is constructed.
  • |..| is already proposed as “ignore all parameters passed to this closure”. I think extending it to instead mean "don’t bind any of the parameters by name, but make them accessible through _" is not an awful idea (but I’d like feedback on this).

Cons:

  • || would no longer indicate a nilary closure.
  • |_| is a strawman, since the _ pattern doesn’t actually introduce bindings and would just confuse people.
  • For single-argument closures, |..| is actually a character heavier (I think arguments about character counts are dumb but I’m including this for completeness).

Scala Wunderbar but with a different sigil, with or without closure bars.

let f = foo(0, 1, $);
let f = || foo(0, 1, ?);
let f = |..| foo(0, 1, #);

Pros:

  • No mucking about with the meaning of _.

Cons:

  • New punctuation!
  • let f = #[0];, for slice access, is ambiguous with attributes (I think?)
  • Doesn’t solve any of the other cons of _ (if we don’t use the bars).
  • $ could be confused with macro captures, and ? is already mentally linked with error handling.

partial!()

let f = partial!(foo, 0, 1, _);
let f = foo.partial!(0, 1, _);

Pros:

  • Pure macro implementation without compiler support.
  • Limited to partial application.

Cons:

  • WAY more typing, and makes “closure noise” worse.
  • No good way to call inherent/trait methods, except for UFCS.

Also, I noted somewhere else in the thread that banning _ in || {..} might be a good idea, since this syntax is meant for short closures. This idea leads me to want to call this feature “trivial closures” (or some post-bikeshed equivalent), for use where you’d see Python’s single-statement lambda.

2 Likes

You said:

Don't you mean:

fn foo(a: i32, b: i32, c: i32) -> i32 { a + b + c }; 
let f = |c| foo(0, 1, c);

EDIT: Evidence of the crime preserved. :wink: :smile:

You saw nothing! =P

3 Likes

Those weren’t the droids I was looking for?

1 Like

That's essentially the Boost.Lambda _1 * _1 + _2 approach, mentioned earlier.

1 Like

Here’s another, not entirely serious idea:

$1 * $1 + $2

This is a closure taking two arguments and returning the square of the first arg plus the second arg.

This is also syntax is swift: The Swift Programming Language: Redirect

To throw another prior art into the ring: Kotlin allows a closure that takes a single argument to be written as if it takes no arguments, then treats it as if it were declared as taken with the name it. So in Kotlin our basic example would be:

fun foo(a: Int, b: Int, c: Int) = a + b + c;
val f = { foo(0, 1, it) }

(I think, I’m not sure if this behavior only works for trailing closures, and didn’t try this)

A few thoughts:

  • Kotlin, unlike Rust, is a big, big fan of contextual keywords; it is one of them. In order to make this work in Rust (without introducing incongruity like { foo(0, 1, it) } v. |x| foo(0, 1, x)) I think we’d need to make it a full keyword (ha, ha). Is the Rust2018 keyword list final yet? =P
  • As mentioned, this only works for single-argument closures, which probably isn’t enough cases to warrant the trouble (I could be wrong… someone should go grep crates.io).

This topic was automatically closed 90 days after the last reply. New replies are no longer allowed.