software - ¿Por qué no i++ en Scala?
scala vs java (10)
Me pregunto por qué no hay i++
para aumentar un número. Por lo que sé, los lenguajes como Ruby o Python no lo admiten porque están tipeados dinámicamente. Entonces, obviamente, no podemos escribir código como i++
porque tal vez sea una cadena u otra cosa. Pero Scala está tipado estáticamente: el compilador puede inferir absolutamente que si es legal o no colocar ++
detrás de una variable.
Entonces, ¿por qué i++
existe en Scala?
La pregunta es por qué debería haber tal operador, no por qué no debería haberlo. ¿Sería Scala mejorado por eso?
El operador ++
es de propósito único y tener un operador que puede cambiar el valor de una variable puede causar problemas. Es fácil escribir expresiones confusas, e incluso si el lenguaje define lo que significa i = i + i++
, por ejemplo, hay muchas reglas detalladas para recordar.
Por cierto, tu razonamiento sobre Python y Ruby está mal. En Perl, puedes escribir $i++
o ++$i
bien. Si $i
resulta ser algo que no se puede incrementar, se obtiene un error en tiempo de ejecución. No está en Python o Ruby porque los diseñadores de idiomas no pensaron que era una buena idea, no porque tuvieran un tipeo dinámico como Perl.
La respuesta de Rafe es cierta acerca de la razón por la cual algo como i ++ no pertenece a Scala. Sin embargo, tengo un nitpick. En realidad, no es posible implementar i ++ en Scala sin cambiar el idioma.
En Scala, ++ es un método válido, y ningún método implica asignación. Solo =
puede hacer eso.
Los lenguajes como C ++ y Java tratan ++
especialmente para referirse tanto al incremento como a la asignación. Scala trata =
especialmente, y de una manera inconsistente.
En Scala cuando escribe i += 1
el compilador busca primero un método llamado +=
en el Int. No está allí, así que lo siguiente es magia on =
e intenta compilar la línea como si dijera i = i + 1
. Si escribe i++
entonces Scala llamará al método ++
en i
y asignará el resultado a ... nada. Porque solo =
significa asignación. Podrías escribir i ++= 1
pero ese tipo de derrotas el propósito.
El hecho de que Scala apoye nombres de métodos como +=
ya es controvertido y algunas personas piensan que es una sobrecarga del operador. Podrían haber agregado un comportamiento especial para ++
pero luego ya no sería un nombre de método válido (like =
) y sería una cosa más para recordar.
Otras respuestas ya han señalado correctamente que un operador ++
no es particularmente útil ni deseable en un lenguaje de programación funcional. Me gustaría agregar que desde Scala 2.10, puede agregar un operador ++
, si así lo desea. Aquí es cómo:
Necesita una macro implícita que convierta ints en instancias de algo que tenga un método ++
. El método ++
está "escrito" por la macro, que tiene acceso a la variable (en oposición a su valor) en la que se llama el método ++
. Aquí está la macro implementación:
trait Incrementer {
def ++ : Int
}
implicit def withPp(i:Int):Incrementer = macro withPpImpl
def withPpImpl(c:Context)(i:c.Expr[Int]):c.Expr[Incrementer] = {
import c.universe._
val id = i.tree
val f = c.Expr[()=>Unit](Function(
List(),
Assign(
id,
Apply(
Select(
id,
newTermName("$plus")
),
List(
Literal(Constant(1))
)
)
)
))
reify(new Incrementer {
def ++ = {
val res = i.splice
f.splice.apply
res
}
})
}
Ahora, siempre que la macro de conversión implícita esté dentro del alcance, puede escribir
var i = 0
println(i++) //prints 0
println(i) //prints 1
Podrías simularlo, sin embargo. Como un ejemplo trivial:
scala> case class IncInt(var self: Int = 0) { def ++ { self += 1 } }
defined class IncInt
scala> val i = IncInt()
i: IncInt = IncInt(0)
scala> i++
scala> i++
scala> i
res28: IncInt = IncInt(2)
Agregue algunas conversiones implícitas y listo. Sin embargo, este tipo de cambio la pregunta en: ¿por qué no hay un RichInt mutable con esta funcionalidad?
Por supuesto, puedes tener eso en Scala, si realmente quieres:
import scalaz._, Scalaz._
case class IncLens[S,N](lens: Lens[S,N], num: Numeric[N]) {
def ++ = lens.mods(num.plus(_, num.one))
}
implicit def incLens[S,N: Numeric](lens: Lens[S,N]) =
IncLens[S,N](lens, implicitly[Numeric[N]])
val i = Lens.lensu[Int,Int]((x, y) => y, identity)
val imperativeProgram = for {
_ <- i++;
_ <- i++;
x <- i++
} yield x
def runProgram = imperativeProgram exec 0
Y aquí tienes:
scala> runProgram
res26: scalaz.Id.Id[Int] = 3
No es necesario recurrir a la violencia contra las variables.
Scala es perfectamente capaz de analizar i++
y, con una pequeña modificación en el lenguaje, podría modificar una variable. Pero hay una variedad de razones para no hacerlo.
Primero, guarda solo un carácter, i++
vs. i+=1
, lo que no significa mucho ahorro para agregar una nueva función de idioma.
En segundo lugar, el operador ++
se usa ampliamente en la biblioteca de colecciones, donde xs ++ ys
toma la colección xs
e ys
y produce una nueva colección que contiene ambas.
En tercer lugar, Scala intenta alentarte, sin forzarte, a escribir código de una manera funcional. i++
es una operación mutable, por lo que es inconsistente con la idea de Scala para hacerlo especialmente fácil. (Del mismo modo, con una función de idioma que permitiría a ++
mutar una variable).
Scala fomenta el uso del estilo FP, que i++
ciertamente no lo es.
Scala no tiene i++
porque es un lenguaje funcional, y en los lenguajes funcionales, se evitan las operaciones con efectos secundarios (en un lenguaje puramente funcional, no se permiten efectos secundarios en absoluto). El efecto secundario de i++
es que ahora tengo 1 más de lo que era antes. En su lugar, debe intentar usar objetos inmutables (por ejemplo, val
not var
).
Además, Scala realmente no necesita i++
debido a las construcciones de flujo de control que proporciona. En Java y otros, necesita i++
menudo para construir while
y for
loops que iteran sobre las matrices. Sin embargo, en Scala, puede decir lo que quiere decir: for(x <- someArray)
o someArray.foreach
o algo someArray.foreach
. i++
es útil en la programación imperativa, pero cuando llegas a un nivel superior, rara vez es necesario (en Python, nunca me encontré necesitándolo).
Estás al tanto de que ++
podría estar en Scala, pero no es porque no sea necesario y simplemente obstruiría la sintaxis. Si realmente lo necesita, responda i += 1
, pero debido a que Scala llama a la programación con flujos de control inmutables y ricos más a menudo, rara vez debería necesitarlo. Definitivamente usted mismo podría definirlo, ya que los operadores son solo métodos en Scala.
Scala no tiene un operador ++
porque no es posible implementar uno en él.
EDITAR : como se señaló en respuesta a esta respuesta, Scala 2.10.0 puede implementar un operador de incremento mediante el uso de macros. Vea esta respuesta para más detalles, y tome todo lo que sigue a continuación como anterior a Scala 2.10.0.
Permítanme dar más detalles sobre esto, y me basaré mucho en Java, ya que en realidad sufre del mismo problema, pero podría ser más fácil para las personas entenderlo si utilizo un ejemplo de Java.
Para empezar, es importante tener en cuenta que una de las metas de Scala es que las clases "integradas" no deben tener ninguna capacidad que no pueda ser duplicada por una biblioteca. Y, por supuesto, en Scala, un Int
es una clase, mientras que en Java, un int
es una primitiva, un tipo completamente distinto de una clase.
Entonces, para que Scala soporte i++
para i
de tipo Int
, debería ser capaz de crear mi propia clase MyInt
también soporte el mismo método. Este es uno de los objetivos principales del diseño de Scala.
Ahora, naturalmente, Java no admite símbolos como nombres de método, así que simplemente llamemos incr()
. Nuestra intención es intentar crear un método incr()
tal que y.incr()
funcione igual que i++
.
Aquí hay un primer paso:
public class Incrementable {
private int n;
public Incrementable(int n) {
this.n = n;
}
public void incr() {
n++;
}
@Override
public String toString() {
return "Incrementable("+n+")";
}
}
Podemos probarlo con esto:
public class DemoIncrementable {
static public void main(String[] args) {
Incrementable i = new Incrementable(0);
System.out.println(i);
i.incr();
System.out.println(i);
}
}
Todo parece funcionar también:
Incrementable(0)
Incrementable(1)
Y, ahora, mostraré cuál es el problema. Cambiemos nuestro programa de demostración y hagamos que se compare Incrementable
a int
:
public class DemoIncrementable {
static public void main(String[] args) {
Incrementable i = new Incrementable(0);
Incrementable j = i;
int k = 0;
int l = 0;
System.out.println("i/t/tj/t/tk/tl");
System.out.println(i+"/t"+j+"/t"+k+"/t"+l);
i.incr();
k++;
System.out.println(i+"/t"+j+"/t"+k+"/t"+l);
}
}
Como podemos ver en la salida, Incrementable
e int
se comportan de manera diferente:
i j k l
Incrementable(0) Incrementable(0) 0 0
Incrementable(1) Incrementable(1) 1 0
El problema es que implementamos incr()
mutando Incrementable
, que no es cómo funcionan los primitivos. Incrementable
necesita ser inmutable, lo que significa que incr()
debe producir un nuevo objeto. Hagamos un cambio ingenuo:
public Incrementable incr() {
return new Incrementable(n + 1);
}
Sin embargo, esto no funciona:
i j k l
Incrementable(0) Incrementable(0) 0 0
Incrementable(0) Incrementable(0) 1 0
El problema es que, mientras que incr()
creó un nuevo objeto, ese nuevo objeto no se ha asignado a i
. No hay ningún mecanismo existente en Java (o Scala) que nos permita implementar este método con la misma semántica exacta que ++
.
Ahora, eso no significa que sería imposible para Scala hacer que tal cosa sea posible . Si Scala admite el paso de parámetros por referencia (ver "llamar por referencia" en este artículo de wikipedia ), como C ++, entonces, ¡ podríamos implementarlo!
Aquí hay una implementación ficticia, asumiendo la misma notación de referencia que en C ++.
implicit def toIncr(Int &n) = {
def ++ = { val tmp = n; n += 1; tmp }
def prefix_++ = { n += 1; n }
}
Esto requeriría soporte de JVM o algunos mecanismos serios en el compilador de Scala.
De hecho, Scala hace algo similar a lo que se necesitaría cuando crea cierres, y una de las consecuencias es que el Int
original queda encerrado, con un posible impacto en el rendimiento.
Por ejemplo, considere este método:
def f(l: List[Int]): Int = {
var sum = 0
l foreach { n => sum += n }
sum
}
El código que se pasa a foreach
, { n => sum += n }
, no es parte de este método. El método foreach
toma un objeto del tipo Function1
cuyo método de aplicación implementa ese pequeño código. Eso significa que { n => sum += n }
no solo está en un método diferente, sino que está en una clase completamente diferente. Y, sin embargo, puede cambiar el valor de la sum
tal como lo necesitaría un operador de ++
.
Si usamos javap
para verlo, veremos esto:
public int f(scala.collection.immutable.List);
Code:
0: new #7; //class scala/runtime/IntRef
3: dup
4: iconst_0
5: invokespecial #12; //Method scala/runtime/IntRef."<init>":(I)V
8: astore_2
9: aload_1
10: new #14; //class tst$$anonfun$f$1
13: dup
14: aload_0
15: aload_2
16: invokespecial #17; //Method tst$$anonfun$f$1."<init>":(Ltst;Lscala/runtime/IntRef;)V
19: invokeinterface #23, 2; //InterfaceMethod scala/collection/LinearSeqOptimized.foreach:(Lscala/Function1;)V
24: aload_2
25: getfield #27; //Field scala/runtime/IntRef.elem:I
28: ireturn
Tenga en cuenta que en lugar de crear una variable local int
, crea un IntRef
en el montón (en 0), que está encajonando el int
. El verdadero int
está dentro de IntRef.elem
, como vemos en 25. Veamos esto mismo implementado con un ciclo while para aclarar la diferencia:
def f(l: List[Int]): Int = {
var sum = 0
var next = l
while (next.nonEmpty) {
sum += next.head
next = next.tail
}
sum
}
Eso se convierte en:
public int f(scala.collection.immutable.List);
Code:
0: iconst_0
1: istore_2
2: aload_1
3: astore_3
4: aload_3
5: invokeinterface #12, 1; //InterfaceMethod scala/collection/TraversableOnce.nonEmpty:()Z
10: ifeq 38
13: iload_2
14: aload_3
15: invokeinterface #18, 1; //InterfaceMethod scala/collection/IterableLike.head:()Ljava/lang/Object;
20: invokestatic #24; //Method scala/runtime/BoxesRunTime.unboxToInt:(Ljava/lang/Object;)I
23: iadd
24: istore_2
25: aload_3
26: invokeinterface #29, 1; //InterfaceMethod scala/collection/TraversableLike.tail:()Ljava/lang/Object;
31: checkcast #31; //class scala/collection/immutable/List
34: astore_3
35: goto 4
38: iload_2
39: ireturn
No hay creación de objeto arriba, no hay necesidad de obtener algo del montón.
Entonces, para concluir, Scala necesitaría capacidades adicionales para soportar un operador de incremento que pudiera ser definido por el usuario, ya que evita dar sus propias capacidades de clases incorporadas que no están disponibles para las bibliotecas externas. Una de estas capacidades es pasar parámetros por referencia, pero JVM no proporciona soporte para ello. Scala hace algo similar a llamar por referencia, y para hacerlo utiliza el boxeo, lo que afectaría seriamente el rendimiento (¡algo que muy probablemente surgiría con un operador de incremento!). En ausencia de soporte de JVM, por lo tanto, eso no es muy probable.
Como nota adicional, Scala tiene una inclinación funcional distinta, privilegiando la inmutabilidad y la transparencia referencial sobre la mutabilidad y los efectos secundarios. ¡El único propósito de llamar por referencia es causar efectos secundarios en la persona que llama ! Si bien esto puede brindar ventajas de rendimiento en una serie de situaciones, va muy en contra del grano de Scala, por lo que dudo que call-reference alguna vez forme parte de él.
Varios lenguajes no admiten la notación ++, como Lua. En los idiomas en los que es compatible, con frecuencia es una fuente de confusión y errores, por lo que su calidad es una característica del lenguaje dudosa, y en comparación con la alternativa de i += 1
o incluso solo i = i + 1
, el ahorro de tales personajes menores son bastante inútiles.
Esto no es relevante para el sistema de tipos del lenguaje. Si bien es cierto que la mayoría de los lenguajes de tipo estático ofrecen y la mayoría de los tipos dinámicos no, eso es una correlación y definitivamente no es una causa.