Why no i++ in Scala?

前端 未结 11 1874
半阙折子戏
半阙折子戏 2020-12-23 10:48

I just wonder why there is no i++ to increase a number. As what I know, languages like Ruby or Python doesn\'t support it because they are dynamically typed. So

11条回答
  •  情话喂你
    2020-12-23 11:50

    Scala doesn't have a ++ operator because it is not possible to implement one in it.

    EDIT: As just pointed out in response to this answer, Scala 2.10.0 can implement an increment operator through use of macros. See this answer for details, and take everything below as being pre-Scala 2.10.0.

    Let me elaborate on this, and I'll rely heavily on Java, since it actually suffers from the same problem, but it might be easier for people to understand it if I use a Java example.

    To start, it is important to note that one of the goals of Scala is that the "built-in" classes must not have any capability that could not be duplicated by a library. And, of course, in Scala an Int is a class, whereas in Java an int is a primitive -- a type entirely distinct from a class.

    So, for Scala to support i++ for i of type Int, I should be able to create my own class MyInt also supporting the same method. This is one of the driving design goals of Scala.

    Now, naturally, Java does not support symbols as method names, so let's just call it incr(). Our intent then is to try to create a method incr() such that y.incr() works just like i++.

    Here's a first pass at it:

    public class Incrementable {
        private int n;
    
        public Incrementable(int n) {
            this.n = n;
        }
    
        public void incr() {
            n++;
        }
    
        @Override
        public String toString() {
            return "Incrementable("+n+")";
        }
    }
    

    We can test it with this:

    public class DemoIncrementable {
        static public void main(String[] args) {
            Incrementable i = new Incrementable(0);
            System.out.println(i);
            i.incr();
            System.out.println(i);
        }
    }
    

    Everything seems to work, too:

    Incrementable(0)
    Incrementable(1)
    

    And, now, I'll show what the problem is. Let's change our demo program, and make it compare Incrementable to 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);
        }
    }
    

    As we can see in the output, Incrementable and int are behaving differently:

    i                   j                       k       l
    Incrementable(0)    Incrementable(0)        0       0
    Incrementable(1)    Incrementable(1)        1       0
    

    The problem is that we implemented incr() by mutating Incrementable, which is not how primitives work. Incrementable needs to be immutable, which means that incr() must produce a new object. Let's do a naive change:

    public Incrementable incr() {
        return new Incrementable(n + 1);
    }
    

    However, this doesn't work:

    i                   j                       k       l
    Incrementable(0)    Incrementable(0)        0       0
    Incrementable(0)    Incrementable(0)        1       0
    

    The problem is that, while, incr() created a new object, that new object hasn't been assigned to i. There's no existing mechanism in Java -- or Scala -- that would allow us to implement this method with the exact same semantics as ++.

    Now, that doesn't mean it would be impossible for Scala to make such a thing possible. If Scala supported parameter passing by reference (see "call by reference" in this wikipedia article), like C++ does, then we could implement it!

    Here's a fictitious implementation, assuming the same by-reference notation as in C++.

    implicit def toIncr(Int &n) = {
      def ++ = { val tmp = n; n += 1; tmp }
      def prefix_++ = { n += 1; n }
    }
    

    This would either require JVM support or some serious mechanics on the Scala compiler.

    In fact, Scala does something similar to what would be needed that when it create closures -- and one of the consequences is that the original Int becomes boxed, with possibly serious performance impact.

    For example, consider this method:

      def f(l: List[Int]): Int = {
        var sum = 0
        l foreach { n => sum += n }
        sum
      }
    

    The code being passed to foreach, { n => sum += n }, is not part of this method. The method foreach takes an object of the type Function1 whose apply method implements that little code. That means { n => sum += n } is not only on a different method, it is on a different class altogether! And yet, it can change the value of sum just like a ++ operator would need to.

    If we use javap to look at it, we'll see this:

    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."":(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."":(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
    

    Note that instead of creating an int local variable, it creates an IntRef on the heap (at 0), which is boxing the int. The real int is inside IntRef.elem, as we see on 25. Let's see this same thing implemented with a while loop to make clear the difference:

      def f(l: List[Int]): Int = {
        var sum = 0
        var next = l
        while (next.nonEmpty) {
          sum += next.head
          next = next.tail
        }
        sum
      }
    

    That becomes:

    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 object creation above, no need to get something from the heap.

    So, to conclude, Scala would need additional capabilities to support an increment operator that could be defined by the user, as it avoids giving its own built-in classes capabilities not available to external libraries. One such capability is passing parameters by-reference, but JVM does not provide support for it. Scala does something similar to call by-reference, and to do so it uses boxing, which would seriously impact performance (something that would most likely come up with an increment operator!). In the absence of JVM support, therefore, that isn't much likely.

    As an additional note, Scala has a distinct functional slant, privileging immutability and referential transparency over mutability and side effects. The sole purpose of call by-reference is to cause side effects on the caller! While doing so can bring performance advantages in a number of situations, it goes very much against the grain of Scala, so I doubt call by-reference will ever be part of it.

提交回复
热议问题