Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

SIP: Auto-tupling of n-ary functions. #897

Closed
odersky opened this issue Oct 31, 2015 · 20 comments
Closed

SIP: Auto-tupling of n-ary functions. #897

odersky opened this issue Oct 31, 2015 · 20 comments

Comments

@odersky
Copy link
Contributor

odersky commented Oct 31, 2015

Add the following automatic conversion:

Let

F = (p1, ..., pn) => E

for n != 1, parameters p1, ..., pn, and an expression E.
If the expected type of F is a fully defined function type or SAM-type that has a
single parameter of a subtype of ProductN[T1, ..., Tn], where each type Ti fits the corresponding
parameter pi, then F is rewritten to

x => {
   def p1 = x._1
   ...
   def pn = x._n
   E
}

A type T fits a parameter p if one of the following two cases is true:

  1. p comes without a type, i.e. it is a simple identifier or _.
  2. p is of the form x: U or _: U and T conforms to U.

Auto-tupling composes with eta-expansion. That is an n-ary function generated by eta-expansion
can in turn be adapted to the expected type with auto-tupling.

Examples:

val pairs = List(1, 2, 3).zipWithIndex
pairs.map(_ + _)

def plus(x: Int, y: Int) = x + y
pairs.map(plus) 
@retronym
Copy link
Member

retronym commented Nov 1, 2015

What's the planned interaction with eta expansion? Would the following typecheck?

def foo(a: Any, b: Any) = 0
List(1, 2, 3).zipWithIndex.map(foo)

@retronym
Copy link
Member

retronym commented Nov 1, 2015

Function literals currently influence overload resolution (their parameter types are taken as part of the "shape type"). Overload resolution might then filter out alternatives that would otherwise have been satisfied by this feature.

For example:

def foo(a: Tuple2[Int, Int] => String)
def foo(a: Any => String)
foo((a, b) => a + b)

(I think this feature is still might be a nett win, just seeking out any unfortunate feature interactions to consider.)

odersky added a commit to dotty-staging/dotty that referenced this issue Nov 1, 2015
Tests suggested by @retronym's comments on issue scala#897.
@odersky
Copy link
Contributor Author

odersky commented Nov 1, 2015

@retronym. Eta expansion: Yes, the two should be combinable. I added text to the description to say so.

Overloading resolution: Yes, that's unfortunate. But not new. The same problem arises with implicit conversions.

@SethTisue
Copy link
Member

new proto-SIP, attention @dickwall!

@retronym
Copy link
Member

retronym commented Nov 2, 2015

I guess it's also worth mentioning that this will kick in before implicits, which has the potential to change the meaning of existing code that currently use implicits to achieve the same sort of result.

One migration technique here would be to have a compiler flag that emits a warning when this feature is triggered. One could use this when compiling existing sources with the new compiler.

One might also ask: Why add a language feature if implicits could serve the same role today? The answer is that the solution using implicits requires the anon function parameter types to be explicitly provided.

scala> implicit def Function2Tupled[A, B, C](f: (A, B) => C): ((A, B)) => C = f.tupled
warning: there was one feature warning; re-run with -feature for details
Function2Tupled: [A, B, C](f: (A, B) => C)((A, B)) => C

scala> "abc".zipWithIndex.map((x: Char, i: Int) => 42)
res2: scala.collection.immutable.IndexedSeq[Int] = Vector(42, 42, 42)

scala> "abc".zipWithIndex.map((x, i) => 42)
<console>:13: error: missing parameter type
Note: The expected type requires a one-argument function accepting a 2-Tuple.
      Consider a pattern matching anonymous function, `{ case (x, i) =>  ... }`
       "abc".zipWithIndex.map((x, i) => 42)
                               ^
<console>:13: error: missing parameter type
       "abc".zipWithIndex.map((x, i) => 42)
                                  ^

@retronym
Copy link
Member

retronym commented Nov 2, 2015

I wonder if compatibility is too lenient. We don't use the same notion for functions that match the arity:

scala> implicit def i2s(i: Int): String = i.toString
warning: there was one feature warning; re-run with -feature for details
i2s: (i: Int)String

scala> ((x: String) => 42 : Int => String)
<console>:14: error: type mismatch;
 found   : Int(42)
 required: Int => String
       ((x: String) => 42 : Int => String)
                       ^

Whereas under this proposal, the following would typecheck by composing the implicit conversion with the function:

scala> ((x: String, y: String) => 42 : ((Int, Int)) => String)

@retronym
Copy link
Member

retronym commented Nov 2, 2015

I find the name of this feature confusing: isn't this about automatically tupling a function, rather than currrying it?

@retronym
Copy link
Member

retronym commented Nov 2, 2015

This desugaring will force by-name function parameters eagerly.

scala> class T[A] { def foo(f: (=> A) => Int) = f(???) }
defined class T

scala> new T[(Int, Int)].foo((ii) => 0)
res27: Int = 0
scala> new T[(Int, Int)].foo((x, y) => 0) // this would throw under the proposed scheme, even though we don't access x or y.

@retronym
Copy link
Member

retronym commented Nov 2, 2015

Similarly, accessing the product values eagerly would be suprising for mutable products:

scala> val f = (param: Muple2[Int, Int]) => {val temp1 = param._1; val temp2 = param._2; {m2._1 = -1; temp1}}
f: Muple2[Int,Int] => Int = $$Lambda$2763/938970667@479b206b

scala> case class Muple2[A, B](var _1: A, var _2: B); val m2 = Muple2(1, 1)
defined class Muple2
m2: Muple2[Int,Int] = Muple2(1,1)

// proposed desugaring of
// val f: Muple2[Int, Int] => Int = (x, y) => m2._1 = -1; x}} 
scala> val f = (param: Muple2[Int, Int]) => {val temp1 = param._1; val temp2 = param._2; {m2._1 = -1; temp1}}
f: Muple2[Int,Int] => Int = $$Lambda$2764/1642844889@54c8a898

scala> f(m2)
res44: Int = 1 // expected -1

@retronym
Copy link
Member

retronym commented Nov 2, 2015

The prototype implementation in dotty allows an expected type of ProductN, whereas this proposal seems to limit itself to TupleN ("a type of form (T1, ..., Tn)")

odersky added a commit to dotty-staging/dotty that referenced this issue Nov 17, 2015
odersky added a commit to dotty-staging/dotty that referenced this issue Nov 17, 2015
Tests suggested by @retronym's comments on issue scala#897.
@nafg
Copy link

nafg commented Jan 10, 2016

As @retronym said, isn't the name wrong? This doesn't seem related to uncurrying ((A => B => C) => (A, B) => C)

@SethTisue SethTisue changed the title SIP: Auto-uncurry n-ary functions. SIP: Auto-tupling of n-ary functions. Jan 11, 2016
@SethTisue
Copy link
Member

(I've updated the ticket name.)

@odersky
Copy link
Contributor Author

odersky commented Feb 16, 2016

@retronym

I wonder if compatibility is too lenient.

Good example! I think we should strengthen requirement to "conforms" instead of "is compatible" . That avoids the inconsistency you found.

@odersky
Copy link
Contributor Author

odersky commented Feb 16, 2016

@retronym I changed the encoding so that parameters are unpackaged with def not val.

odersky added a commit to dotty-staging/dotty that referenced this issue Feb 16, 2016
odersky added a commit to dotty-staging/dotty that referenced this issue Feb 16, 2016
Tests suggested by @retronym's comments on issue scala#897.
odersky added a commit to dotty-staging/dotty that referenced this issue Feb 16, 2016
Was: corresponding parameter types "are compatible".
Now: corresponding parameter types "conform".

This avoids the inconsistency mentioned by @retronym in scala#897.
odersky added a commit to dotty-staging/dotty that referenced this issue Feb 16, 2016
As retronym noted on scala#897, `val` forces to early.
@odersky
Copy link
Contributor Author

odersky commented Feb 16, 2016

I think all reviewers comments so far are now addressed in the SIP and in #897.

@julienrf
Copy link
Collaborator

julienrf commented Jan 20, 2022

I couldn’t find the specification of this feature in the language specification, and I couldn’t find the SIP either in the SIP list, is it documented somewhere?

I noticed the following surprising behavior (inspired from the example given in the first post of this issue):

val pairs = List(1, 2, 3).zipWithIndex
pairs.map(_ + _)

​def plus(x: Int, y: Int) = x + y
pairs.map(plus) 

val plus2: (Int, Int) => Int = _ + _
pairs.map(plus2) // Error
// Found:    (Int, Int) => Int
// Required: ((Int, Int)) => Any

See it live: https://scastie.scala-lang.org/0Lzr7BFvTaehiRAEpjbvFQ

I would expect the third form to work fine, since it is very similar to the first form. Should I open a bug?

@som-snytt
Copy link
Contributor

@julienrf https://docs.scala-lang.org/scala3/reference/other-new-features/parameter-untupling-spec.html

@nafg
Copy link

nafg commented Jan 20, 2022 via email

@odersky
Copy link
Contributor Author

odersky commented Jan 20, 2022

I agree with @nafg. Technically, the adaptation is made when we see a formal parameter list of the wrong arity. In the third case, there is no such list; the function has been defined already.

@som-snytt
Copy link
Contributor

som-snytt commented Jan 20, 2022

One fix is to change the migration doc from

Though it is possible that someone has written an implicit conversion form (T1, ..., Tn) => R to TupleN[T1, ..., Tn] => R for some n.

to

Users may wish to upgrade their implicit conversion from (T1, ..., Tn) => R to TupleN[T1, ..., Tn] => R to a given Conversion[(T1, ..., Tn) => R, TupleN[T1, ..., Tn] => R.

Actually, the naive implicit transparent inline def conversion does the obvious, efficient thing.

It's OK to transparent inline given Conversion but you can't "nest" a transparent inline def apply, so there is an extra apply.

TIL TIL stands for transparent inline love.

Update: I did PR the explanation on the doc page. Unfortunately, it is not as droll as my comment here. Thanks @julienrf for raising awareness.

This issue or edge case is similar to the issue about rewrapping context functions: the adaptation is syntax-driven, or is a function of syntax, so it's a surprise that it is not type-driven.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

6 participants