Почему нет i++ в Scala?
Мне просто интересно, почему нет i++
увеличить число. Как я знаю, языки вроде Ruby или Python не поддерживают его, потому что они динамически типизированы. Так что очевидно, что мы не можем написать код i++
потому что возможно i
это строка или что-то еще. Но Scala статически типизирован - компилятор абсолютно может сделать вывод, что если это законно или не ставить ++
за переменной.
Итак, почему не i++
существует в Scala?
11 ответов
Скала не имеет i++
поскольку это функциональный язык, а в функциональных языках операции с побочными эффектами избегаются (на чисто функциональном языке побочные эффекты вообще не допускаются). Побочный эффект i++
в том, что i
теперь на 1 больше, чем было раньше. Вместо этого вы должны попытаться использовать неизменяемые объекты (например, val
не var
).
Кроме того, Scala на самом деле не нуждается i++
из-за конструкций потока управления, которые это обеспечивает. В Java и других вам нужно i++
часто строить while
а также for
циклы, которые перебирают массивы. Однако в Scala вы можете просто сказать, что вы имеете в виду: for(x <- someArray)
или же someArray.foreach
Или что-то вдоль этих линий. i++
полезен в императивном программировании, но когда вы переходите на более высокий уровень, это редко требуется (в Python я никогда не нуждался в этом однажды).
Вы на этом месте ++
может быть в Scala, но это не потому, что это не нужно и просто засорит синтаксис. Если тебе это действительно нужно, скажи i += 1
, но поскольку Scala чаще всего требует программирования с неизменяемостью и расширенным потоком управления, вам это редко нужно. Вы, безусловно, можете определить это сами, так как операторы действительно являются просто методами в Scala.
Конечно, вы можете иметь это в 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
Не нужно прибегать к насилию против переменных.
Scala прекрасно умеет разбирать i++
и, с небольшой модификацией языка, можно было бы изменить переменную. Но есть множество причин не делать этого.
Во-первых, он сохраняет только один символ, i++
против i+=1
, что не очень много сбережений для добавления новой языковой функции.
Во-вторых, ++
Оператор широко используется в библиотеке коллекций, где xs ++ ys
берет коллекцию xs
а также ys
и производит новую коллекцию, которая содержит оба.
В-третьих, Scala пытается побудить вас, не заставляя вас, писать код функциональным образом. i++
это изменяемая операция, поэтому она несовместима с идеей Scala, чтобы сделать ее особенно легкой. (Аналогично с языковой функцией, которая позволила бы ++
мутировать переменную.)
Скала не имеет ++
оператор, потому что это невозможно реализовать в нем.
РЕДАКТИРОВАТЬ: Как только что указывалось в ответ на этот ответ, Scala 2.10.0 может реализовать оператор приращения с помощью макросов. Смотрите подробности в этом ответе и примите все, что ниже, до версии Scala 2.10.0.
Позвольте мне подробнее остановиться на этом, и я буду в значительной степени полагаться на Java, поскольку на самом деле она страдает от той же проблемы, но людям было бы легче понять ее, если я использую пример Java.
Для начала важно отметить, что одной из целей Scala является то, что "встроенные" классы не должны иметь никаких возможностей, которые не могут быть дублированы библиотекой. И, конечно же, в Скала Int
это класс, тогда как в Java 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 поддерживает передачу параметров по ссылке (см. "Вызов по ссылке" в этой статье в Википедии), как это делает C++, то мы могли бы реализовать это!
Вот вымышленная реализация, предполагающая ту же нотацию по ссылке, что и в C++.
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, поэтому я сомневаюсь, что вызов по ссылке будет когда-либо частью этого.
Другие ответы уже правильно указали, что ++
Оператор не особенно полезен и не желателен в функциональном языке программирования. Я хотел бы добавить, что начиная с Scala 2.10, вы можете добавить ++
оператор, если вы хотите. Вот как:
Вам нужен неявный макрос, который преобразует целые числа в экземпляры чего-то, что имеет ++
метод. ++
метод "записывается" макросом, который имеет доступ к переменной (в отличие от ее значения), по которой ++
метод называется. Вот реализация макроса:
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
Ответ Рэйфа правдиво объясняет, почему что-то вроде i++ не принадлежит Scala. Однако у меня есть один придира. На самом деле невозможно реализовать i++ в Scala без изменения языка.
В Scala ++ является допустимым методом, и ни один метод не подразумевает присваивания. Только =
могу сделать это.
Такие языки, как C++ и Java относятся к ++
специально означать как приращение и назначение. Скала лечит =
специально и непоследовательно.
В Scala, когда ты пишешь i += 1
сначала компилятор ищет метод +=
на Инт. Это не там, так что в следующий раз это волшебство на =
и пытается скомпилировать строку, как будто она читает i = i + 1
, Если ты пишешь i++
тогда Scala вызовет метод ++
на i
и присвоить результат... ничего. Потому что только =
означает назначение. Вы могли бы написать i ++= 1
но такой вид побеждает цель.
Тот факт, что Scala поддерживает имена методов, такие как +=
это уже спорный, и некоторые люди думают, что это перегрузка оператора. Они могли бы добавить особое поведение для ++
но тогда это больше не будет действительным именем метода (например, =
) и это будет еще одна вещь, чтобы помнить.
Scala поощряет использование стиля FP, который i++
конечно нет.
Многие языки не поддерживают нотацию ++, такую как Lua. В тех языках, на которых он поддерживается, он часто является источником путаницы и ошибок, поэтому его качество как языковой особенности сомнительно и по сравнению с альтернативой i += 1
или даже просто i = i + 1
сохранение таких второстепенных символов довольно бессмысленно.
Это совсем не относится к системе типов языка. Несмотря на то, что большинство языков статических типов действительно предлагают, а большинство динамических типов - нет, это корреляция и определенно не причина.
Вопрос в том, почему должен быть такой оператор, а не в том, почему его не должно быть. Будет ли это улучшено Scala?
++
Оператор является одноцелевым, и наличие оператора, который может изменить значение переменной, может вызвать проблемы. Запутанные выражения легко написать, и даже если язык определяет, что i = i + i++
означает, например, это много подробных правил для запоминания.
Кстати, ваши рассуждения о Python и Ruby неверны. В Perl вы можете написать $i++
или же ++$i
просто хорошо. Если $i
оказывается что-то, что не может быть увеличено, вы получаете ошибку во время выполнения. Это не в Python или Ruby, потому что разработчики языка не думали, что это хорошая идея, а не потому, что они динамически набираются как Perl.
Вы можете смоделировать это, хотя. В качестве тривиального примера:
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 с такой функциональностью?
Как показывает другой ответ, оператор приращения, найденный в i++
, было-
предположительно... добавлено к языку B [предшественник языка C] Кеном Томпсоном специально, потому что он был способен переводить непосредственно в один код операции после компиляции
и не обязательно, потому что такой оператор так же полезен, как, скажем, общее сложение и вычитание. Хотя некоторые объектно-ориентированные языки (такие как Java и C#) также имеют оператор приращения (часто заимствованный из C), не все имеют (например, Ruby).