sobreescritura sobrecarga que programacion metodos funciones ejemplos ejemplo consiste scala implicit

scala - que - Patrón de imán y métodos sobrecargados.



sobrecarga de metodos o funciones en java (1)

Existe una diferencia significativa en la forma en que Scala resuelve las conversiones implícitas del "Patrón magnético" para los métodos sin sobrecarga y sobrecargados.

Supongamos que hay un rasgo Apply (una variación de un "Patrón magnético") implementado de la siguiente manera.

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

Ahora creamos un rasgo Foo que tiene una única apply toma una instancia de Apply para que podamos pasarle cualquier valor de tipo arbitrario A ya que existe una conversión implícita de A => Apply[A] .

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

Podemos asegurarnos de que funcione como se espera usando REPL y esta solución para eliminar el código Scala de azúcar .

scala> val foo = new Foo[String]{} foo: Foo[String] = $anon$1@3a248e6a scala> showCode(reify { foo { "foo" } }.tree) res9: String = $line21$read.foo.apply( $read.INSTANCE.Apply.fromLazyVal("foo") )

Esto funciona muy bien, pero supongamos que pasamos una expresión compleja (con ; ) al método de apply .

scala> val foo = new Foo[Int]{} foo: Foo[Int] = $anon$1@5645b124 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) })

Como podemos ver, una conversión implícita se ha aplicado solo en la última parte de la expresión compleja (es decir, i ), no en toda la expresión. Entonces, i = i + 1 se evaluó estrictamente en el momento en que lo pasamos a un método de apply , que no es lo que esperábamos.

Buenas (o malas) noticias. Podemos hacer que scalac use la expresión completa en la conversión implícita. Entonces i = i + 1 será evaluado perezosamente como se esperaba. Para hacerlo, nosotros (¡sorprendemos, sorprendemos!) Foo.apply un método de sobrecarga Foo.apply que toma cualquier tipo, pero no Apply .

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

Y entonces.

scala> var i = 0 i: Int = 0 scala> val foo = new Foo[Int]{} foo: Foo[Int] = $anon$1@3ff00018 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 }))

Como podemos ver, toda la expresión i = i + 1; i i = i + 1; i hice bajo la conversión implícita como se esperaba.

Así que mi pregunta es ¿por qué es eso? Por qué el alcance del cual se aplica una conversión implícita depende del hecho de si existe o no un método sobrecargado en la clase.


Ahora, eso es un truco. Y en realidad es bastante asombroso, no sabía que la "solución alternativa" al problema "perezoso implícito no cubre el bloqueo completo". ¡Gracias por eso!

Lo que sucede está relacionado con los tipos esperados y cómo afectan los trabajos de inferencia de tipos, conversiones implícitas y sobrecargas.

Inferencia de tipos y tipos esperados

Primero, debemos saber que la inferencia de tipos en Scala es bidireccional. La mayor parte de la inferencia funciona de abajo hacia arriba (dado a: Int y b: Int , inferir a + b: Int ), pero algunas cosas son de arriba hacia abajo. Por ejemplo, inferir los tipos de parámetros de un lambda es de arriba a abajo:

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

En la segunda línea, después de resolver foo para ser def foo(f: Int => Int): Int , el inferente de tipo puede decir que x debe ser de tipo Int . Lo hace antes de verificar la lambda. Propaga información de tipo desde la aplicación de la función hasta la lambda, que es un parámetro.

La inferencia descendente básicamente se basa en la noción de tipo esperado . Cuando se chequea un nodo del AST del programa, el typechecker no se inicia con las manos vacías. Recibe un tipo esperado de "arriba" (en este caso, el nodo de aplicación de función). Cuando se comprueba la lambda x => x + 1 en el ejemplo anterior, el tipo esperado es Int => Int , porque sabemos qué tipo de parámetro espera foo . Esto lleva a la inferencia de tipo a inferir Int para el parámetro x , que a su vez permite verificar tipicamente x + 1 .

Los tipos esperados se propagan a través de ciertas construcciones, por ejemplo, bloques ( {} ) y las ramas de if sy match es. Por lo tanto, también podría llamar a foo con

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

y el typechecker todavía es capaz de inferir x: Int . Esto se debe a que, cuando se comprueba el bloque { ... } , el tipo esperado Int => Int se pasa a la comprobación de tipo de la última expresión, es decir, x => x + y .

Conversiones implícitas y tipos esperados

Ahora, tenemos que introducir conversiones implícitas en la mezcla. Cuando la comprobación de tipos de un nodo produce un valor de tipo T , pero el tipo esperado para ese nodo es U donde T <: U es falso, el buscador de letras busca una T => U implícita (probablemente estoy simplificando un poco las cosas aquí, pero la esencia sigue siendo cierta). Por eso tu primer ejemplo no funciona. Veamos de cerca:

trait Foo[A] { def apply(a: Apply[A]): A = a() } val foo = new Foo[Int] {} foo({ i = i + 1 i })

Cuando se llama a foo.apply , el tipo esperado para el parámetro (es decir, el bloque) es Apply[Int] (ya se ha creado una instancia de A a Int ). Podemos "escribir" este "indicador de estado" de esta manera:

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

Este tipo esperado se pasa a la última expresión del bloque, que proporciona:

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

en este punto, dado que i: Int y el tipo esperado es Apply[Int] , el buscador de tipos encuentra la conversión implícita:

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

lo que hace que solo i sea ​​lazificado.

Sobrecargas y tipos esperados.

OK, es hora de lanzar sobrecargas allí! Cuando el typechecker ve una aplicación de un método de sobrecarga, tiene muchos más problemas para decidir sobre un tipo esperado. Podemos ver eso con el siguiente ejemplo:

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

da:

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

En este caso, la falla del buscador de tipos para descubrir un tipo esperado hace que el tipo de parámetro no sea inferido.

Si llevamos su "solución" a su problema, tenemos una consecuencia diferente:

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

Ahora, cuando se comprueba el bloque, el buscador de tipos no tiene un tipo esperado con el que trabajar. Por lo tanto, verificará tipicamente la última expresión sin expresión, y eventualmente chequeará todo el bloque como un Int :

{ i = i + 1 i }: Int

Solo ahora, con un argumento ya comprobado, intenta resolver las sobrecargas. Como ninguna de las sobrecargas se ajusta directamente, intenta aplicar una conversión implícita de Int para Apply[Int] o Symbol . Encuentra fromLazyVal[Int] , que se aplica a todo el argumento . Ya no lo empuja dentro del bloque, dando:

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

En este caso, todo el bloque está lazificado.

Con esto concluye la explicación. Para resumir, la principal diferencia es la presencia frente a la ausencia de un tipo esperado cuando se verifica el bloqueo por tipografía. Con un tipo esperado, la conversión implícita se reduce tanto como sea posible, hasta solo i . Sin el tipo esperado, la conversión implícita se aplica a posteriori en todo el argumento, es decir, en todo el bloque.