Подтвердить что ты не робот

Модель магнита и перегруженные методы

Существует значительная разница в том, как Scala разрешает неявные преобразования из "Модели магнитов" для неперегруженных и перегруженных методов.

Предположим, что существует признак Apply (вариация "Магнетического узора" ), реализованный следующим образом.

trait Apply[A] {
 def apply(): A
}
object Apply {
  implicit def fromLazyVal[A](v: => A): Apply[A] = new Apply[A] {
    def apply(): A = v
  }
}

Теперь мы создаем черту Foo, которая имеет единственный Apply, который принимает экземпляр Apply, поэтому мы можем передать ему любое значение произвольного типа A, поскольку существует неявное преобразование из A => Apply[A].

trait Foo[A] {
  def apply(a: Apply[A]): A = a()
}

Мы можем убедиться, что он работает так, как ожидалось, с помощью REPL и этот обходной путь для кода de-sugar Scala.

scala> val foo = new Foo[String]{}
foo: Foo[String] = [email protected]

scala> showCode(reify { foo { "foo" } }.tree)
res9: String =    
$line21$read.foo.apply(
  $read.INSTANCE.Apply.fromLazyVal("foo")
)

Это работает отлично, но предположим, что мы передаем сложное выражение (с ;) методу Apply.

scala> val foo = new Foo[Int]{}
foo: Foo[Int] = [email protected]

scala> var i = 0
i: Int = 0

scala> showCode(reify { foo { i = i + 1; i } }.tree)
res10: String =
$line23$read.foo.apply({
  $line24$read.`i_=`($line24$read.i.+(1));
  $read.INSTANCE.Apply.fromLazyVal($line24$read.i)
})

Как мы видим, неявное преобразование применялось только к последней части сложного выражения (т.е. i), а не ко всему выражению. Итак, i = i + 1 была строго оценена в тот момент, когда мы передаем ее методу Apply, чего мы не ожидаем.

Хорошие (или плохие) новости. Мы можем сделать scalac для использования всего выражения в неявном преобразовании. Таким образом, i = i + 1 будет оцениваться лениво, как и ожидалось. Для этого мы (удивляем, удивляем!) Добавляем метод перегрузки Foo.apply, который принимает любой тип, но не Apply.

trait Foo[A] {
  def apply(a: Apply[A]): A = a()
  def apply(s: Symbol): Foo[A] = this
}

И затем.

scala> var i = 0
i: Int = 0

scala> val foo = new Foo[Int]{}
foo: Foo[Int] = [email protected]

scala> showCode(reify { foo { i = i + 1; i } }.tree)
res11: String =
$line28$read.foo.apply($read.INSTANCE.Apply.fromLazyVal({
  $line27$read.`i_=`($line27$read.i.+(1));
  $line27$read.i
}))

Как мы видим, все выражение i = i + 1; i сделало его под неявным преобразованием, как ожидалось.

Итак, мой вопрос: почему? Почему область применения неявного преобразования зависит от того, есть ли в классе перегруженный метод.

4b9b3361

Ответ 1

Теперь это сложно. И это на самом деле довольно удивительно, я не знал, что "обходной путь" к проблеме "ленивый неявный не покрывает полный блок". Спасибо за это!

Что происходит, связано с ожидаемыми типами и как они влияют на работу типа, выводятся неявные преобразования и перегрузки.

Тип вывода и ожидаемые типы

Во-первых, мы должны знать, что вывод типа в Scala является двунаправленным. Большая часть вывода работает снизу вверх (задано a: Int и b: Int, infer a + b: Int), но некоторые вещи сверху вниз. Например, вывод параметров параметров лямбда сверху вниз:

def foo(f: Int => Int): Int = f(42)
foo(x => x + 1)

Во второй строке после определения foo как def foo(f: Int => Int): Int тип inferencer может сказать, что x должен иметь тип Int. Он делает это до того, как проведет проверку самой лямбда. Он распространяет информацию о типе из приложения-приложения до лямбда, который является параметром.

Вывод сверху вниз в основном основан на понятии ожидаемого типа. Когда typechecking node AST программы, typechecker не запускается с пустыми руками. Он получает ожидаемый тип от "выше" (в данном случае приложение функции node). Когда typechecking lambda x => x + 1 в приведенном выше примере, ожидаемый тип Int => Int, потому что мы знаем, какой тип параметра ожидается foo. Это приводит к выходу типа для вывода Int для параметра x, который, в свою очередь, позволяет typecheck x + 1.

Ожидаемые типы распространяются по некоторым конструкциям, например, блокам ({}) и ветвям if и match es. Следовательно, вы также можете вызвать foo с помощью

foo({
  val y = 1
  x => x + y
})

и typechecker все еще может вывести x: Int. Это связано с тем, что при проверке типа блока { ... } ожидаемый тип Int => Int передается на проверку типов последнего выражения, т.е. x => x + y.

Неявные преобразования и ожидаемые типы

Теперь мы должны ввести неявные преобразования в микс. Когда typechecking a node создает значение типа T, но ожидаемый тип для этого node равен U, где T <: U - false, typechecker ищет неявный T => U (я, вероятно, упрощаю вещи немного здесь, но суть все еще верна). Вот почему ваш первый пример не работает. Давайте посмотрим внимательно:

trait Foo[A] {
  def apply(a: Apply[A]): A = a()
}

val foo = new Foo[Int] {}
foo({
  i = i + 1
  i
})

При вызове foo.apply ожидаемый тип для параметра (т.е. блока) равен Apply[Int] (A уже был создан для Int). Мы можем "написать" это "состояние" typechecker следующим образом:

{
  i = i + 1
  i
}: Apply[Int]

Этот ожидаемый тип передается до последнего выражения блока, которое дает:

{
  i = i + 1
  (i: Apply[Int])
}

в этой точке, так как i: Int и ожидаемый тип Apply[Int], typechecker находит неявное преобразование:

{
  i = i + 1
  fromLazyVal[Int](i)
}

который вызывает только i.

Перегрузки и ожидаемые типы

ОК, время для перегрузки там! Когда typechecker видит приложение метода перегрузки, у него гораздо больше проблем при выборе ожидаемого типа. Мы видим, что в следующем примере:

object Foo {
  def apply(f: Int => Int): Int = f(42)
  def apply(f: String => String): String = f("hello")
}

Foo(x => x + 1)

дает:

error: missing parameter type
              Foo(x => x + 1)
                  ^

В этом случае отказ typechecker в определении ожидаемого типа приводит к тому, что тип параметра не должен быть выведен.

Если мы возьмем ваше решение для вашей проблемы, мы имеем другое следствие:

trait Foo[A] {
  def apply(a: Apply[A]): A = a()
  def apply(s: Symbol): Foo[A] = this
}

val foo = new Foo[Int] {}
foo({
  i = i + 1
  i
})

Теперь, когда typechecking блок, typechecker не имеет ожидаемого типа для работы. Поэтому он будет проверять последнее выражение без выражения, и в конечном итоге typecheck весь блок как Int:

{
  i = i + 1
  i
}: Int

Только теперь, с уже аргументом typechecked, он пытается разрешить перегрузки. Поскольку ни одна из перегрузок не соответствует напрямую, она пытается применить неявное преобразование от Int к Apply[Int] или Symbol. Он находит fromLazyVal[Int], который применяется ко всему аргументу. Он больше не вставляет его внутри блока, давая:

fromLazyVal({
  i = i + 1
  i
}): Apply[Int]

В этом случае весь блок остеклен.

Это завершает объяснение. Резюмируя, основное различие заключается в наличии против отсутствия ожидаемого типа при проверке типа блока. При ожидаемом типе неявное преобразование максимально приближается, вплоть до i. Без ожидаемого типа неявное преобразование применяется апостериор на весь аргумент, т.е. Весь блок.