Почему нет я ++ в Scala?

Мне просто интересно, почему нет i++ для увеличения числа. Как я знаю, такие языки, как Ruby или Python, не поддерживают его, потому что они динамически типизированы. Таким образом, очевидно, что мы не можем писать код типа i++, потому что, возможно, i является строкой или чем-то еще. Но Scala статически типизирован - компилятор абсолютно может сделать вывод, что если он легален или не помещает ++ за переменную.

Итак, почему не существует i++ в Scala?

Ответы

Ответ 1

Scala не имеет i++, потому что это функциональный язык, а в функциональных языках избегают операций с побочными эффектами (на чисто функциональном языке вообще никаких побочных эффектов не разрешено). Побочным эффектом i++ является то, что i теперь на 1 больше, чем раньше. Вместо этого вы должны попытаться использовать неизменяемые объекты (например, val not var).

Кроме того, Scala действительно не нуждается в i++ из-за создаваемых им конструкций потока управления. В Java и других случаях вам нужно i++ часто создавать контуры while и for, которые перебирают массивы. Однако в Scala вы можете просто сказать, что вы имеете в виду: for(x <- someArray) или someArray.foreach или что-то в этом направлении. i++ полезен в императивном программировании, но когда вы переходите на более высокий уровень, это редко необходимо (в Python я никогда не нуждался в нем один раз).

Вы заметили, что ++ может быть в Scala, но это не потому, что это не нужно, а просто забивает синтаксис. Если вам это действительно нужно, скажите i += 1, но поскольку Scala чаще всего требует программирования с неизменяемыми и богатым потоком управления, вам редко приходится это делать. Вы, конечно, могли бы определить это самостоятельно, поскольку операторы действительно являются просто методами в Scala.

Ответ 2

Конечно, вы можете иметь это в Scala, если вы действительно хотите:

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

И вот вы идете:

scala> runProgram
res26: scalaz.Id.Id[Int] = 3

Не нужно прибегать к насилию против переменных.

Ответ 3

Scala отлично подходит для синтаксического анализа i++ и, с небольшой модификацией языка, может быть изменен для изменения переменной. Но есть множество причин не делать этого.

Во-первых, он сохраняет только один символ, i++ vs. i+=1, что не очень удобно для добавления новой языковой функции.

Во-вторых, оператор ++ широко используется в библиотеке коллекций, где xs ++ ys принимает коллекции xs и ys и создает новую коллекцию, содержащую оба.

В-третьих, Scala пытается побудить вас, не заставляя вас писать код функциональным способом. i++ является изменчивой операцией, поэтому она несовместима с идеей Scala, чтобы сделать ее особенно легкой. (Аналогично с языковой функцией, которая позволяет ++ мутировать переменную.)

Ответ 4

Scala не имеет оператора ++, потому что его невозможно реализовать.

EDIT. Как было указано в ответ на этот ответ, Scala 2.10.0 может реализовать оператор инкремента с помощью макросов. Подробнее см. этот ответ и возьмите все ниже как pre- Scala 2.10.0.

Позвольте мне подробно остановиться на этом, и я буду сильно полагаться на Java, поскольку он действительно страдает от одной и той же проблемы, но людям может быть проще понять это, если я использую пример Java.

Чтобы начать, важно отметить, что одна из целей Scala заключается в том, что "встроенные" классы не должны иметь никаких возможностей, которые невозможно было бы дублировать библиотекой. И, конечно же, в Scala a Int является классом, тогда как в Java an Int является примитивным - это тип, полностью отличный от класса.

Итак, для Scala для поддержки i++ для i типа Int я должен был бы создать свой собственный класс MyInt, также поддерживающий тот же метод. Это одна из главных целей дизайна Scala.

Теперь, естественно, Java не поддерживает символы как имена методов, поэтому позвольте просто называть его incr(). Тогда мы должны попытаться создать метод incr(), так что y.incr() работает так же, как i++.

Здесь первый проход:

public class Incrementable {
    private int n;

    public Incrementable(int n) {
        this.n = n;
    }

    public void incr() {
        n++;
    }

    @Override
    public String toString() {
        return "Incrementable("+n+")";
    }
}

Мы можем проверить это следующим образом:

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

Все, похоже, тоже работает:

Incrementable(0)
Incrementable(1)

И теперь я покажу, в чем проблема. Измените нашу демо-программу и сравните ее с Incrementable с 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);
    }
}

Как мы видим на выходе, Incrementable и Int ведут себя по-другому:

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

Проблема заключается в том, что мы внедрили incr() путем мутации Incrementable, а это не так, как работают примитивы. Incrementable должен быть неизменным, а это значит, что incr() должен создать новый объект. Давайте сделаем наивное изменение:

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

Однако это не работает:

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

Проблема заключается в том, что, пока incr() создал новый объект, этот новый объект не был назначен i. Нет существующего механизма в Java - или Scala - это позволит нам реализовать этот метод с той же семантикой, что и ++.

Теперь это не означает, что невозможно сделать Scala возможным. Если Scala поддерживается параметр, передаваемый по ссылке (см. "Вызов по ссылке" в эта статья в википедии), как это делает С++, тогда мы могли бы реализовать это!

Вот фиктивная реализация, предполагающая ту же нотацию, что и в С++.

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

Это потребовало бы поддержки JVM или некоторой серьезной механики в компиляторе Scala.

Фактически, Scala делает что-то похожее на то, что было бы необходимо, чтобы при создании замыканий, и одним из последствий является то, что исходный Int становится в коробке с возможным серьезным воздействием на производительность.

Например, рассмотрим этот метод:

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

Код, передаваемый в foreach, { n => sum += n }, не является частью этого метода. Метод foreach принимает объект типа Function1, метод apply реализует этот маленький код. Это означает, что { n => sum += n } - это не только другой метод, но и совсем другой класс! И все же, он может изменить значение sum так же, как и оператор ++.

Если мы используем javap, чтобы посмотреть на него, мы увидим следующее:

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

Обратите внимание, что вместо создания локальной переменной Int она создает IntRef в куче (в 0), которая боксирует Int. Вещественный Int находится внутри IntRef.elem, как мы видим на 25. Давайте посмотрим на это же, реализованное с циклом while, чтобы прояснить разницу:

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

Это будет:

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

Нет создания объекта выше, нет необходимости получать что-то из кучи.

Итак, чтобы заключить, что Scala потребует дополнительных возможностей для поддержки оператора инкремента, который может быть определен пользователем, так как он избегает предоставления своих встроенных классов, недоступных для внешних библиотек. Одной из таких возможностей является передача параметров по ссылке, но JVM не поддерживает ее. Scala делает что-то похожее на вызов по ссылке, и для этого он использует бокс, что серьезно повлияет на производительность (что, скорее всего, придумает оператор инкремента!). Поэтому в отсутствие поддержки JVM это маловероятно.

В качестве дополнительной заметки Scala имеет четкий функциональный наклон, неизменность привилегий и ссылочную прозрачность над изменчивостью и побочными эффектами. Единственная цель вызова по ссылке - вызвать побочные эффекты для вызывающего абонента! Хотя это может привести к преимуществам производительности в ряде ситуаций, оно очень сильно влияет на зерно Scala, поэтому я сомневаюсь, что ссылка на вызов будет когда-либо частью этого.

Ответ 5

Другие ответы уже правильно указали, что оператор ++ не является ни особенно полезным, ни желательным на языке функционального программирования. Я хотел бы добавить, что с Scala 2.10 вы можете добавить оператор ++, если хотите. Вот как:

Вам нужен неявный макрос, который преобразует int в экземпляры того, что имеет метод ++. Метод ++ "записывается" макросом, который имеет доступ к переменной (в отличие от ее значения), по которой вызывается метод ++. Вот реализация макроса:

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

Теперь, пока макрос неявного преобразования находится в области видимости, вы можете написать

var i = 0
println(i++) //prints 0
println(i) //prints 1

Ответ 6

Ответ Rafe верен относительно обоснования того, почему что-то вроде я ++ не принадлежит Scala. Однако у меня есть один нитпик. На самом деле невозможно реализовать я ++ в Scala без изменения языка.

В Scala, ++ является допустимым методом, и никакой метод не подразумевает назначение. Только = может это сделать.

Языки, такие как С++ и Java, рассматривают ++ специально для обозначения как прироста, так и назначения. Scala рассматривает = специально и непоследовательно.

В Scala при написании i += 1 компилятор сначала ищет метод под названием += в Int. Это не так, так что теперь это волшебство на = и пытается скомпилировать строку так, как будто она читает i = i + 1. Если вы напишете i++, то Scala вызовет метод ++ на i и присвойте результат... ничего. Потому что только = означает назначение. Вы можете написать i ++= 1, но этот вид поражения цели.

Тот факт, что Scala поддерживает имена методов, такие как +=, уже вызывает противоречие, и некоторые люди считают его перегрузкой оператора. Они могли бы добавить специальное поведение для ++, но тогда оно больше не будет корректным именем метода (например, =), и это будет еще одна вещь, о которой нужно помнить.

Ответ 7

Довольно много языков не поддерживают обозначение ++, например Lua. В языках, на которых он поддерживается, он часто является источником путаницы и ошибок, поэтому качество как языковой функции сомнительно и сравнивается с альтернативой i += 1 или даже просто i = i + 1, сохранение таких второстепенных символов довольно бессмысленно.

Это совсем не относится к системе типов языка. Хотя верно, что большинство языков статического типа предлагают, а большинство динамических типов этого не делают, это корреляция и определенно не причина.

Ответ 8

Scala рекомендует использовать стиль FP, который i++, конечно, не является.

Ответ 9

Вопрос: почему должен быть такой оператор, а не почему этого не должно быть. Улучшится ли Scala?

Оператор ++ является одноцелевым и имеет оператор, который может изменить значение переменной, может вызвать проблемы. Легко писать запутанные выражения, и даже если язык определяет, что означает i = i + i++, например, много подробных правил для запоминания.

Ваши рассуждения о Python и Ruby, кстати, ошибочны. В Perl вы можете просто написать $i++ или ++$i. Если $i оказывается чем-то, что не может быть увеличено, вы получите ошибку во время выполнения. Это не в Python или Ruby, потому что разработчики языка не думали, что это хорошая идея, а не потому, что они динамически набираются как Perl.

Ответ 10

Вы могли бы имитировать это. В качестве тривиального примера:

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)

Добавьте некоторые неявные преобразования, и вам хорошо идти. Однако этот вопрос меняет вопрос: почему нет такой изменчивой RichInt с этой функциональностью?