¿De qué sirven los métodos asociativos correctos en Scala?
operators order-of-operations (3)
Empecé a jugar con Scala y aprendí cómo los métodos se pueden asociar correctamente (a diferencia de la asociatividad de la izquierda más común en los lenguajes imperativos orientados a objetos).
Al principio, cuando vi un código de ejemplo para cons
una lista en Scala, noté que cada ejemplo siempre tenía la Lista en el lado derecho:
println(1 :: List(2, 3, 4))
newList = 42 :: originalList
Sin embargo, incluso después de ver esto una y otra vez, no lo pensé dos veces, porque no sabía (en ese momento) que ::
es un método en List
. Simplemente asumí que era un operador (nuevamente, en el sentido de operador en Java) y que la asociatividad no importaba. El hecho de que la List
siempre apareciera en el lado derecho en el código de ejemplo simplemente parecía una coincidencia (pensé que quizás era solo el "estilo preferido").
Ahora sé mejor: tiene que escribirse de esa manera porque ::
tiene una asociación correcta.
Mi pregunta es, ¿cuál es el sentido de poder definir los métodos asociativos correctos?
¿Es puramente por razones estéticas, o puede la asociatividad derecha en realidad tener algún tipo de beneficio sobre la asociatividad izquierda en ciertas situaciones?
Desde mi punto de vista (novato), realmente no veo cómo
1 :: myList
es mejor que
myList :: 1
pero obviamente es un ejemplo tan trivial que dudo que sea una comparación justa.
¿Cuál es el punto de poder definir los métodos asociativos correctos?
Creo que el objetivo del método asociativo correcto es darle a alguien la oportunidad de extender el idioma, que es el punto de anulación del operador en general.
La sobrecarga del operador es una cosa útil, entonces Scala dijo: ¿Por qué no abrirlo a cualquier combinación de símbolos? Más bien, ¿por qué hacer una distinción entre los operadores y los métodos? Ahora, ¿cómo interactúa un implementador de biblioteca con tipos incorporados como Int
? En C ++, ella habría utilizado la función de friend
en el ámbito global. ¿Qué pasa si queremos que todos los tipos implementen el operador ::
?
La asociatividad correcta proporciona una forma limpia de agregar ::
operador a todos los tipos. Por supuesto, técnicamente el operador ::
es un método del tipo de lista. Pero también es un operador de fantasía para el Int integrado y todos los otros tipos, al menos si se puede ignorar el :: Nil
al final.
Creo que refleja la filosofía de Scala de implementar tantas cosas en la biblioteca y hacer que el lenguaje sea flexible para apoyarlas. Esto le da la oportunidad a alguien de crear una SuperList, que podría llamarse así:
1 :: 2 :: SuperNil
Es un poco desafortunado que la asociatividad correcta esté codificada en el código de los dos puntos, pero creo que es bastante fácil de recordar.
La asociatividad a la derecha y la asociatividad a la izquierda desempeñan un papel importante cuando se realizan operaciones de plegado de listas. Por ejemplo:
def concat[T](xs: List[T], ys: List[T]): List[T] = (xs foldRight ys)(_::_)
Esto funciona perfectamente bien. Pero no puede realizar lo mismo con la operación foldLeft
def concat[T](xs: List[T], ys: List[T]): List[T] = (xs foldLeft ys)(_::_)
, porque ::
tiene una asociación correcta.
La respuesta corta es que la asociatividad correcta puede mejorar la legibilidad haciendo que lo que el programador escriba sea consistente con lo que realmente hace el programa.
Entonces, si escribe '' 1 :: 2 :: 3
'', obtiene una Lista (1, 2, 3), en lugar de volver a obtener una Lista en un orden completamente diferente.
Eso sería porque '' 1 :: 2 :: 3 :: Nil
'' es en realidad
List[Int].3.prepend(2).prepend(1)
scala> 1 :: 2 :: 3:: Nil
res0: List[Int] = List(1, 2, 3)
que es ambos:
- más legible
- más eficiente (O (1) para
prepend
, frente a O (n) para un método hipotético deappend
)
(Recordatorio, extracto del libro Programación en Scala )
Si se utiliza un método en la notación del operador, como a * b
, el método se invoca en el operando izquierdo, como en a.*(b)
- a menos que el nombre del método termine en dos puntos.
Si el nombre del método termina en dos puntos, el método se invoca en el operando derecho.
Por lo tanto, en 1 :: twoThree
, el método ::
se invoca en twoThree
, pasando en 1, así: twoThree.::(1)
.
Para List, desempeña el papel de una operación de adición (la lista parece adjuntarse después del ''1'' para formar '' 1 2 3
'', donde de hecho es 1 el que se antepone a la lista).
La Lista de clases no ofrece una operación de anexión verdadera, porque el tiempo que tarda en agregarse a una lista crece linealmente con el tamaño de la lista, mientras que anteponer :: toma un tiempo constante .
myList :: 1
intentaría anteponer todo el contenido de myList a ''1'', lo que sería más largo que anteponer 1 a myList (como en '' 1 :: myList
'')
Nota: Independientemente de la asociatividad que tenga un operador, sus operandos siempre se evalúan de izquierda a derecha.
Entonces, si b es una expresión que no es solo una simple referencia a un valor inmutable, entonces ::: b se trata con mayor precisión como el siguiente bloque:
{ val x = a; b.:::(x) }
En este bloque a todavía se evalúa antes de b, y luego el resultado de esta evaluación se pasa como un operando al método b :: ::.
¿Por qué hacer la distinción entre los métodos asociativos de izquierda y los asociativos de la derecha en absoluto?
Eso permite mantener la apariencia de una operación asociativa izquierda habitual ('' 1 :: myList
'') mientras se aplica la operación en la expresión correcta porque;
- es mas eficiente
- pero es más legible con un orden asociativo inverso (''
1 :: myList
'' vs. ''myList.prepend(1)
'')
Así que como dices, "azúcar sintáctico", hasta donde yo sé.
Tenga en cuenta que, en el caso de foldLeft
, por ejemplo, podrían haber ido un poco más lejos (con el operador asociativo de la derecha '' /:
'')
Para incluir algunos de sus comentarios, reformulados levemente:
si considera una función ''anexar'', asociativa a la izquierda, entonces debería escribir '' oneTwo append 3 append 4 append 5
''.
Sin embargo, si fuera a agregar 3, 4 y 5 a uno Dos (lo cual asumiría por la forma en que está escrito), sería O (N).
Lo mismo con ''::'' si fue para "agregar". Pero no lo es. En realidad es para "preceder"
Eso significa que '' a :: b :: Nil
'' es para '' List[].b.prepend(a)
''
Si ''::'' fuera anterior y permaneciera asociado a la izquierda, la lista resultante estaría en el orden incorrecto.
Esperaría que devuelva List (1, 2, 3, 4, 5), pero terminaría devolviendo List (5, 4, 3, 1, 2), lo que podría ser inesperado para el programador.
Eso es porque, lo que has hecho habría sido, en el orden asociativo de la izquierda:
(1,2).prepend(3).prepend(4).prepend(5) : (5,4,3,1,2)
Entonces, la asociatividad correcta hace coincidir el código con el orden real del valor de retorno.