Некоторое время назад я анонсировал курс по Scala. Он стартовал и выкладывается на MOOC-платформу UDEMY — «Scala for Java Developers». Больше о курсе вы можете прочитать в конце статьи.

Сейчас я бы хотел представить материал по одной из тем курса — перегрузке операторов в Scala.





Введение


в Scala нет перегрузки операторов, так как нет операторов (как сущностей отличных от методов). Есть методы с символическими (операторными) именами вида '+', '/', '::', '<~' и префиксная/инфиксная/посфиксная форма записи. Однако для удобства далее будет использовать термин оператор.


Infix operators



В Scala методы одного аргумента можно записывать в так называемой инфиксной форме (infix operations). А именно
  • без использования точки между ссылкой и методом
  • без скобок, обрамляющих аргумент


Пример:
object Demo {
  // "normal" notation
  val x0 = I(1).add(I(2))
  // infix notation
  val x1 = I(1) add I(2)
}

case class I(k: Int) {
  def add(that: I): I = I(this.k + that.k)
}

Далее в примерах будет фигурировать case-класс I, который я буду в каждом конкретном случае наделять разными методами. Он сделан case исключительно для краткости кода (автоматически генерируются и инициализируются поля по primary constructor + автоматически генерируется companion object с методом apply с сигнатурой идентичной primary constructor, что позволяет создавать экземпляры через I(k), а не new I(k). Напомню, что I(k) эквивалентно I.apply(k), а метод apply в Scala можно опускать). Класс I представляет собой «обертку» вокруг одного Int и может рассматриваться как прототип для полноценного класса комплексных чисел, полиномов, матриц.

Все становится интереснее, если методу давать «символическое» / «операторное» имя
object Demo {
  // "normal" notation
  val x0 = I(1).+(I(2))
  // infix notation
  val x1 = I(1) + I(2)
}

case class I(k: Int) {
  def +(that: I): I = I(this.k + that.k)
}

JVM (class file format) не поддерживает имена из «операторных символов», потому при компиляции генерируются синтетические имена.

Запустим по классу
class I {
  def +(that: I): I = new I
  def -(that: I): I = new I
  def *(that: I): I = new I
  def /(that: I): I = new I
  def \(that: I): I = new I
  def ::(that: I): I = new I
  def ->(that: I): I = new I
  def <~(that: I): I = new I
}


джавовскую рефлексию
import java.lang.reflect.Method;

public class Demo {
    public static void main(String[] args) {
        for (Method m: I.class.getDeclaredMethods()) {
            System.out.println(m);
        }
    }
}

>> public I.$plus(I)
>> public I.$minus(I)
>> public I.$times(I)
>> public I.$div(I)
>> public I.$bslash(I)
>> public I.$colon$colon(I)
>> public I.$minus$greater(I)
>> public I.$less$tilde(I)


Да, из Java методы и видны с такими именами (как в class file)
public class Demo {
    public static void main(String[] args) {
        new I().$plus(new I());
        new I().$minus(new I());
        new I().$times(new I());
        new I().$div(new I());
        new I().$bslash(new I());
        new I().$colon$colon(new I());
        new I().$minus$greater(new I());
        new I().$less$tilde(new I());
    }
}

Вы же помните про прозрачную интеграцию всех компилируемых под JVM языков?

Да вообще половина «синтаксических фокусов» Scala состоит их смеси инфиксной нотации и implicit conversions.

Пример #1:
object Demo {
  for (k <- 1 to 10) {
    println(k)
  }
}


Инфиксная нотация преобразовывается в нормальную
object Demo {
  for (k <- 1.to(10)) {
    println(k)
  }


У Int нет метода 'to', поэтому ищется такое implicit conversion, которое позволяет преобразовать Int в какой-то тип с методом 'to' и подходящей сигнатурой.

И находится в Predef.scala (напомню, что в каждый файл перед компиляцией неявно импортируются java.lang.* + scala.* + Predef.*)

// сокращенные исходники Predef.scala
package scala

object Predef extends LowPriorityImplicits with DeprecatedPredef {...}

private[scala] trait DeprecatedPredef {...}

private[scala] abstract class LowPriorityImplicits {
  ...
  @inline implicit def byteWrapper(x: Byte)       = new runtime.RichByte(x)
  @inline implicit def shortWrapper(x: Short)     = new runtime.RichShort(x)
  @inline implicit def intWrapper(x: Int)         = new runtime.RichInt(x)
  @inline implicit def charWrapper(c: Char)       = new runtime.RichChar(c)
  @inline implicit def longWrapper(x: Long)       = new runtime.RichLong(x)
  @inline implicit def floatWrapper(x: Float)     = new runtime.RichFloat(x)
  @inline implicit def doubleWrapper(x: Double)   = new runtime.RichDouble(x)
  @inline implicit def booleanWrapper(x: Boolean) = new runtime.RichBoolean(x)   
  ...
}


А вот у RichInt уже есть метод 'to' с одним аргументом типа Int.
// сокращенный исходники scala.runtime.RichInt
package scala.runtime

import scala.collection.immutable.Range

final class RichInt(val self: Int) ... {
  ...
   def to(end: Int): Range.Inclusive = Range.inclusive(self, end)
  ...
}


И потому при компиляции «раскручивается» в нечто типа
import scala.runtime.RichInt

object Demo {
  val tmp: Range = new RichInt(1).to(10)
  for (k <- tmp) {
    println(k)
  }
}


После «раскрутки» for в map/flatMap/foreach имеем
import scala.runtime.RichInt

object Demo {
  val tmp: Range = new RichInt(1).to(10)
  tmp.foreach(elem => println(elem))
}


Пример #2:
object Demo {
  var map = Map("France" -> "Paris")
  map += "Japan" -> "Tokyo"
}


После перехода от инфиксной формы вызова методов '->' и '+' к нормальной
object Demo {
  var map = Map("France".->("Paris"))
  map = map.+("Japan".->("Tokyo"))
}


и поиска подходящего implicit conversion String до какого-то типа с методом '->' (опять же находят в Predef.scala) получают «десахаризированную форму» (String в Scala — это, по сути, java.lang.String и у него нет метода '->')
object Demo {
  var map: Map[String, String] = Map.apply(new ArrowAssoc("France").->("Paris"))
  map = map.+((new ArrowAssoc("Japan").->("Tokyo")))
}


Из забавного: вот исходный код (сокращенный) класса ArrowAssoc из Predef.scala
  implicit class ArrowAssoc[A](private val self: A) extends AnyVal {
    def -> [B](y: B): Tuple2[A, B] = Tuple2(self, y)
  }

благодаря generics мы можем ставить стрелку между представителями ЛЮБЫХ ДВУХ ТИПОВ! Если вы сделаете 1 -> true, то type variable A будет принят за Int, а type variable B — за Boolean!


«Pointless style» (infix notation) это не «point-free style» (tacit programming)



Не надо путать pointless style (infix notation), который мы рассматриваем, с так называемым point-free стилем или же по другому — tacit programming.

Point-free стиль предполагает, что вы строите новые функции из неких примитивов и других функций не указывая явно аргументы, не вводя формальные имена параметров. Название произошло из топологии, где часто ведут размышления в терминах окрестностей а не конкретных точек.

Рассмотрим простой пример: функция Int => Int, которая возвращает аргумент увеличенный на 1.

Вот НЕ pointless и НЕ point-free стиль
object Demo {
  val f: Int => Int = x => 1.+(x)
}

Напомню, что в Scala '+' — это метод, принадлежащий типу Int, а не оператор. Хотя при компиляции под JVM преобразуется таки в оператор '+' над примитивом int.

Вот pointless и НЕ point-free стиль
object Demo {
  val f: Int => Int = x => 1 + x
}


Вот НЕ pointless и point-free стиль (f — с placeholder, g — без placeholder)
object Demo extends App {
  val f: Int => Int = 1.+(_)
  val g: Int => Int = 1.+
}


Вот и pointless и и point-free стиль (f — с placeholder, g — без placeholder)
object Demo {
  val f: Int => Int = 1 + _
  val g: Int => Int = 1 +
}


Далее мы не будем рассматривать point-free/tacit-programming, это может быть тематикой отдельной статьи.


Приоритет операторов



Если мы начнем определять свои «операторы», то мы можем столкнуться с отсутствием приоритетов
object Demo extends App {
  println(I(1) add I(2) mul I(3))
}

case class I(k: Int) {
  def add(that: I): I = I(this.k + that.k)
  def mul(that: I): I = I(this.k * that.k)
}

>> 9


мы бы хотели, что бы у умножения (mul) был приоритет перед сложением (add) (то есть мы хотим 1 + (2 * 3) = 7, а не (1 + 2) * 3 = 9). Однако запись вида
I(1) add I(2) mul I(3)


Эквивалентна следующей
I(1).add.(I(2)).mul(I(3))


Которая эквивалентна такой
( I(1).add.(I(2)) ).mul(I(3))


А не
I(1).add( I(2).mul(I(3)) )


Так как вызов метода — это лево-ассоциативная операция, то есть идет расстановка скобок (свертка) слева на право.

Это можно исправить явной расстановкой скобок
object Demo extends App {
  println(I(1) add ( I(2) mul I(3) ))
}
case class I(k: Int) {
  def add(that: I): I = I(this.k + that.k)
  def mul(that: I): I = I(this.k * that.k)
}

>> 7


либо пользуясь приоритетом обычных вызовов перед инфиксными (не рекомендуемый стиль, не смешивайте инфиксную и нормальные формы вызовов, скобки — лучше)
object Demo extends App {
  println(I(1) add I(2).mul(I(3)))
}
case class I(k: Int) {
  def add(that: I): I = I(this.k + that.k)
  def mul(that: I): I = I(this.k * that.k)
}

>> 7


Однако, если мы переименуем методы ('mul' -> '*', 'add' -> '+'), то произойдет немножко магии без всякого указания приоритета '*' над '+'!
object Demo extends App {
  println(I(1) + I(2) * I(3))
}

case class I(k: Int) {
  def +(that: I): I = I(this.k + that.k)
  def *(that: I): I = I(this.k * that.k)
}

>> 7


Откроем Священную Книгу на разделе «6.12.3 Infix Operations» и прочитаем:
The precedence of an infix operator is determined by the operator's first character. Characters are listed below in increasing order of precedence, with characters on the same line having the same precedence.
(all letters)
|
^
&
= !
< >
:
+ -
* / %
(all other special characters)



Итак, если наш метод начинается с '*', то он имеет приоритет над методом начинающимся с '+'. Который, в свою очередь, имеет приоритет над любым именем начинающимся с «обычной буквы».

Значит вот так тоже будет работать (не рекомендуется схожие операторы (умножение, сложение) называть и строковыми и операторными именами)
object Demo extends App {
  println(I(1) add I(2) * I(3))
}

case class I(k: Int) {
  def add(that: I): I = I(this.k + that.k)
  def *(that: I): I = I(this.k * that.k)
}

>> 7


Рассмотрим следующее выражение: 1 * 2 * 3 + 4 * 5 * 6 + 7 * 8 * 9.

Если операторам «сложить» и «умножить» дать строковые имена add и mul
object Demo extends App {
  println(I(1) mul I(2) mul I(3) add I(4) mul I(5) mul I(6) add I(7) mul I(8) mul I(9))
}

case class I(k: Int) {
  def add(that: I): I = I(this.k + that.k)
  def mul(that: I): I = I(this.k * that.k)
}

то все подвергнется свертке слева
1 * 2 * 3 + 4 * 5 * 6 + 7 * 8 * 9 -> (((((((1 * 2) * 3) + 4) * 5) * 6) + 7) * 8) * 9

Но в случае имен '+' и '*'
object Demo extends App {
  println(I(1) * I(2) * I(3) + I(4) * I(5) * I(6) + I(7) * I(8) * I(9))
}

case class I(k: Int) {
  def +(that: I): I = I(this.k + that.k)
  def *(that: I): I = I(this.k * that.k)
}

строка будет разбита на группы по равным приоритетам
1 * 2 * 3 + 4 * 5 * 6 + 7 * 8 * 9 -> 
(1 * 2 * 3) + (4 * 5 * 6) + (7 * 8 * 9)


внутри каждой группы (группы берутся слева направо) будет свертка слева-направо
(1 * 2 * 3) + (4 * 5 * 6) + (7 * 8 * 9) ->
((1 * 2) * 3) + ((4 * 5) * 6) + ((7 * 8) * 9)


после чего будут свернуты слева-направо операнды по сложению
((1 * 2) * 3) + ((4 * 5) * 6) + ((7 * 8) * 9) ->
(((1 * 2) * 3) + ((4 * 5) * 6)) + ((7 * 8) * 9)



Ассоциативность операторов



Читаем Священную Книгу, радел «6.12.3 Infix Operations» дальше:
The associativity of an operator is determined by the operator's last character. Operators ending in a colon `:' are right-associative. All other operators are left-associative.

The right-hand operand of a left-associative operator may consist of several arguments enclosed in parentheses, e.g. e;op;(e1,…,en). This expression is then interpreted as e.op(e1,…,en).

A left-associative binary operation e1;op;e2 is interpreted as e1.op(e2). If op is right-associative, the same operation is interpreted as { val x=e1; e2.op(x) }, where x is a fresh name.


Что это значит практически? Значит, что левоассоциативность сверток стоит по умолчанию, но для методов в инфиксной форме, оканчивающихся на двоеточие работает — правоассоциативность. Более того, аргументы оператора меняются местами.

Это значит, что в следующем коде
object Demo {
  println(I(1) ++ I(2) ++ I(3) ++ I(4))
  println(I(1) +: I(2) +: I(3) +: I(4))
}

case class I(k: Int) {
  def ++(that: I): I = I(this.k + that.k)
  def +:(that: I): I = I(this.k + that.k)
}

строка
I(1) ++ I(2) ++ I(3) ++ I(4)

сворачивается до (левоассоциативность)
((I(1) ++ I(2)) ++ I(3)) ++ I(4)

и потом до
(((I(1).++(I(2))).++(I(3)) ++ I(4)


а строка
I(1) +: I(2) +: I(3) +: I(4)

сворачивается до (правоассоциативность)
I(1) +: (I(2) +: (I(3) +: I(4)))

и при переходе от инфиксной формы к обычной происходит инверсия аргументов оператора (магия ':' в конце имени оператора)
I(1) +: (I(2) +: (I(3) +: I(4))) -> 
I(1) +: (I(2) +: (I(4).+:(I(3)))) ->
I(1) +: ((I(4).+:(I(3))).+:(I(2))) ->
((I(4).+:(I(3))).+:(I(2))).+:(I(1))


Вопрос: какому больному разуму это может быть полезно?

Ну… вот пример из стандартной библиотеки (создание List)
object Demo {
  val list =   0 ::  1 ::  2 ::  Nil
}

Вопрос: как произошла эта магия? И почему в конце стоит пустой список Nil?
Все крайне просто: '::' — это метод класса List! С правоассоциативностью и реверсом операндов.

List определен примерно вот так (сокращенная и измененная версия)
sealed abstract class List[+A] {
  def head: A
  def tail: MyList[A]
  def isEmpty: Boolean
  def ::[B >: A](x: B): List[B] = new Node(x, this)
}

final case class Node[A](head: A, tail: List[A]) extends List[A] {
  override def isEmpty: Boolean = false
}
object Nil extends List[Nothing] {
  override def head: Nothing = throw new Error
  override def tail: MyList[Nothing] = throw new Error
  override def isEmpty: Boolean = true
}


И код
object Demo {
  val list =   0 ::  1 ::  2 ::  Nil
}


разворачивается компилятором в
object Demo {
  val list =  ( ( Nil.::(2) ).::(1) ).::(1)
}

То есть мы просто набиваем элементы в односвязный список (стек) начиная с пустого списка (Nil).


Infix types



Инфиксные типы (infix types) — это просто запись type constructors от двух аргументов в инфиксной форме.

Итак, по порядку. Что такое type constructor от двух аргументов? Это просто generic class/trait с двумя type variable. Имея такой класс (назовем его 'ab'), даем ему два типа, например Int и String и получаем (конструируем) тип ab[Int, String]

Смотрим
object Demo extends App {
  val x0: ab[Int, String] = null
  val x1: Int ab String = null
}

case class ab[A, B](a: A, b: B)

Тип ab[Int, String] просто можно записать в инфиксной форме как Int ab String.

Все становится веселее, если мы type constructor называем не банально 'ab' а волшебно, например '++'.
object Demo extends App {
  val x0: ++[Int, String] = null

  val x1: Int ++ String = null

  val x2: List[Int ++ String] = null

  val f: Int ++ String => String ++ Int = null
}
case class ++[A, B](a: A, b: B)


Если Вы встретите где-то магию вида
def f[A, B](x: A <:< B)

или
def f[A, B](x: A =:= B)


То просто знайте, что в Predef.scala есть пара классов с именами '=:=' и '<:<'
object Predef extends ... {
  ..
  ... class <:<[-From, +To] extends ...
  ... class =:=[From, To] extends ...
  ..
}



Prefix operators



Из спецификации Scala
A prefix operation op;e consists of a prefix operator op, which must be one of the identifiers ‘+’, ‘-’, ‘!’ or ‘~’. The expression op;e is equivalent to the postfix method application e.unary_op.

Prefix operators are different from normal function applications in that their operand expression need not be atomic. For instance, the input sequence -sin(x) is read as -(sin(x)), whereas the function application negate sin(x) would be parsed as the application of the infix operator sin to the operands negate and (x).


В Scala программист может определять только 4 префиксных оператора с именами '+', '-', '!', '~'. Они задаются как методы без аргументов с именами 'unary_+', 'unary_-', 'unary_!', 'unary_~'.

object Demo {
  val x0 = +I(0)
  val x1 = -I(0)
  val x2 = !I(0)
  val x3 = ~I(0)
}

case class I(k: Int) {
  // не ищите логики в реализации
  def unary_+(): I = I(2 * this.k) 
  def unary_-(): I = I(3 * this.k)
  def unary_!(): I = I(4 * this.k)
  def unary_~(): I = I(5 * this.k)
}


Если мы посмотрим с помощью Java Redflection API, то увидим, во что эти методы компилируются
import java.lang.reflect.Method;

public class Demo {
    public static void main(String[] args) {
        Class clazz = I.class;
        for (Method m : clazz.getDeclaredMethods()) {
            System.out.println(m);
        }
    }
}

>> public I I.unary_$plus()
>> public I I.unary_$minus()
>> public I I.unary_$bang()
>> public I I.unary_$tilde()
...


Надо заметить, что наравне с префиксной формой сохранились и оригинальные названия (то есть краткая форма '+'/'-'/'!'/'~' — это просто синтаксический сахар к существующей и после компиляции полной форме 'unary_+'/'unary_-'/'unary_!'/'unary_~')
object Demo extends App {
  val x0 = +I(0)
  val x1 = -I(0)
  val x2 = !I(0)
  val x3 = ~I(0)

  // не рекомендуемая практика
  val y0 = I(0).unary_+()
  val y1 = I(0).unary_-()
  val y2 = I(0).unary_!()
  val y3 = I(0).unary_~()
}

case class I(k: Int) {
  // не ищите логики в реализации
  def unary_+(): I = I(2 * this.k)
  def unary_-(): I = I(3 * this.k)
  def unary_!(): I = I(4 * this.k)
  def unary_~(): I = I(5 * this.k)
}



Postfix operators



Методы в постфиксной форме — это методы без аргумента, которые вызвали без точки. По ряду причин методы в посфиксной нотации — это причина множества ошибок (посмотрите для начала тут и тут).
object Demo {
  val tailList0 = List(0, 1, 2).tail // "normal" notation
  val tailList1 = List(0, 1, 2) tail // postfix/suffix notation
}


Попробуем определить факториал на целых числах (def !).
Для начала обратим внимание на 100500 способов вызвать метод в Scala
object Demo extends App {
  val a = I(0).!()
  val b = I(0).!
  val c = I(0) !()
  val d = I(0) !   // postfix notation
}

case class I(k: Int) {
  def !(): I = I(2 * this.k) // не ищите тут логики, ее пока нет
}


Сделаем метод '!' на классе-обертке
object Demo extends App {
  val x: I = I(5)!;
  println(x)
}

case class I(k: Int) {
  def !(): I = if (k == 0) I(1) else I(k) * (I(k - 1)!)
  def *(that: I): I = I(this.k * that.k)
}

>> I(120)

Обратите внимание, точка с запятой — обязательны в конце первой строчки, иначе — НЕ КОМПИЛИРУЕТСЯ (постфикс — это боль, да)!

Спрячем явное наличие класса-обертки под implicit-ами (Int -> I, I -> Int)
object Demo extends App {

  implicit class I(val k: Int) {
    def !(): I = if (k == 0) I(1) else I(k) * (I(k - 1)!)
    def *(that: I): I = I(this.k * that.k)
  }

  implicit def toInt(x: I): Int = x.k

  val x: Int = 5!;
  println(x)
}

>> 120


А теперь спрячем и сами implicit-ы
object Demo extends App {
  import MathLib._

  val x: Int = 5!;
  println(x)
}

object MathLib {

  implicit class I(val k: Int) {
    def !(): I = if (k == 0) I(1) else I(k) * (I(k - 1)!)
    def *(that: I): I = I(this.k * that.k)
  }

  implicit def toInt(x: I): Int = x.k
}

>> 120



О курсе



Анонсированный курс по Scala выкладывается на MOOC-платформу UDEMY — «Scala for Java Developers». Первоначальная идея написать всеобъемлющий курс по всем аспектам языка и наиболее популярным «type acrobatic» библиотекам (scalaz, shapeless) сохранилась, но претерпела небольшие изменения.
Оригинальный большой 32-часовой курс за 399$ решено «разрезать» на два 16-часовых курса по 199$ (если вы введете на UDEMY код купона HABR-OPERATOR или просто зайдете по ссылке udemy.com/scala-for-java-developers-ru/?couponCode=HABR-OPERATOR, то цена со скидкой будет 179$, количество и срок действия скидочных купонов ограничено). Решено насытить курс тестами (будет более 50 тестов по 5-15 вопросов с примерами кода на каждую часть курса).
Первый курс снят на 75% (12 часов из 16) и выложен на UDEMY на 50% (8 часов из 16), так как часть видео находится в обработке.

В первую часть входят такие темы
  • Intro: HelloWorld, Scala and JVM, Scala and Reflection
  • OOP — I: class, object, trait, case class, package, method, constructor, field
  • OOP — II: operator overloading (prefix, infix, postfix)
  • Types — I: Scala type hierarchy, top and bottom types, Unit
  • Types — II: tuples, structural types
  • Generics — I: covariance/contravariance, Scala vs Java
  • Collections — I: Array, List, Set, Map
  • Implicits/type classes: conversions, arguments, view bounds
  • Functional Programming — I: functional literals, closures, eta-expansions, curring, partial application
  • Lazyness: call-by-name, keyword 'lazy', trait DelayedInit
  • Control — I: build-in control flow constructions, expression-oriented programming — if, while, for, case, try, throw
  • Control — II: create your own control flow constructions
  • Pattern matching: case classes, extractors
  • List-comprehentions: trait MonadicFilter, translation to composition of higher-order functions, monads
  • Комбинаторика: порождение комбинаторных объектов (перестановки, разбиения, подмножества, деревья)
  • Алгебра — I: магма, полугруппа, моноид, группа
  • Теория множеств: бинарные отношения (эквивалентности, порядка, частичного порядка, предпорядка), классы эквивалентности, фактор-множества
  • Теория множеств: морфизмы, автоморфизмы, гомоморфизмы, отображение структуры множества


Во вторую часть (пока на стадии проработки) входят такие темы
  • Annotations
  • OOP — III: inheritance, inheritance linearization, cake pattern
  • Types — III: abstract type members, singleton types, shapeless:HList
  • Generics — II: existential types, higher-kind types
  • Collections — II: internals and architecture
  • Collections — III: parallel collections
  • Concurrency — I: Futures and Promises
  • Concurrency — II: actors, supervisors
  • Streams: recursion and co-recursion, lazy data structures
  • Functional Programming — II: Functional patterns and pearls
  • Введение в теорию категорий: Scalaz:Monad, Scalaz:ApplicativeFunctor
  • Введение в математическую логику
  • Алгебра — II
  • Path dependent types
  • Formal languages — I: theory
  • Formal languages — II: parser combinators (scala.util.parsing.{ast, combinator, input, json})
  • Metaprogramming — I: Reflection
  • Metaprogramming — II: Macroses


Замечание #1: ряд тем (OOP, Generics, Scala types, ...) решено разбить на 2 или даже 3 части в виду сложности и важности вопроса (первые части располагаются в первой части курса, последние — во второй части курса («OOP-III: наследование, Cake Pattern», «Generics-II: existential types, higher-king types», ...)).

Замечание #2: в виду того, что у многих программистов есть определенные проблемы с математикой (учим 3 семестра «матан», но не более полезные для программиста «дискретные дисциплины» — теорию множеств, дискретную математику, математическую логику, алгебру, комбинаторику, теорию категорий, формальные языки/грамматики, ...) и потому, что функциональное программирование сильно использованием математических концепций, в курс введено несколько разделов математики (вся математика «кодируется на Scala», так что без университетских «сферических коней в вакууме»).

P.S. На все вопросы отвечу в комментариях / в сообщениях в «личку» или по контактам
skype: GolovachCourses
email: GolovachCourses@gmail.com

P.P.S Одновременно с разработкой курса по Scala автор ведет внутренние тренинги по Scala в IT-компаниях, тренинги в рамках конференций, делает доклады по Scala и оказывает консультационные услуги при переводе проектов с Java на Scala.
С каким утверждением Вы согласны в наибольшей мере?

Проголосовало 129 человек. Воздержалось 26 человек.

Какое утверждение в наибольшей мере отражает ваше отношение к ФП?

Проголосовало 144 человека. Воздержалось 28 человек.

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

Комментарии (28)


  1. eld0727
    22.04.2015 18:51

    И потому при компиляции «раскручивается» в нечто типа
    import scala.runtime.RichInt
    
    object Demo {
      val tmp: Range = new RichInt(1).to(10)
      for (k <- tmp) {
        println(k)
      }
    }
    


    Ну на самом деле (но это не 100%) компилятор делает оптимизацию, и превращает def to(that: Int) в static def to(x1: Int, x2: Int), так как не имеет смысла создавать лишние объекты


    1. dougrinch
      22.04.2015 21:41

      А можно ссылку? Последний раз когда я смотрел, оно именно так и компилировалось.


      1. dougrinch
        22.04.2015 21:51

        docs.scala-lang.org/overviews/core/value-classes.html

        Позор мне, забыл. Только их использование сильно ограничено. И реально объект создается значительно чаще, чем хотелось бы.


        1. darkdimius
          23.04.2015 00:39

          Через пол-годика-годик будет в этой области улучшение:
          github.com/lampepfl/dotty/pull/411
          Скорее всего будут — массивы value-classов, value-class-ы поверх value-class-ов, отсуствие боксинга в аннонимных функциях.
          Маловероятно, но возможно — специализация для value-class-ов.


    1. IvanGolovach Автор
      22.04.2015 22:25

      Хз, надо разбираться, может надо это включать, но вот такой класс

      class Demo {
        def f(x: Int) = 1 to x
      }
      


      у меня компилируется вот в это
      public class tmp.Demo {
        public scala.collection.immutable.Range$Inclusive f(int);
          Code:
             0: getstatic     #16  // Field scala/runtime/RichInt$.MODULE$:Lscala/runtime/RichInt$;
             3: getstatic     #21  // Field scala/Predef$.MODULE$:Lscala/Predef$;
             6: iconst_1
             7: invokevirtual #25  // Method scala/Predef$.intWrapper:(I)I
            10: iload_1
            11: invokevirtual #29  // Method scala/runtime/RichInt$.to$extension0:(II)Lscala/collection/immutable/Range$Inclusive;
            14: areturn
      
        public tmp.Demo();
          Code:
             0: aload_0
             1: invokespecial #37  // Method java/lang/Object."<init>":()V
             4: return
      }
      


      т.е. ходят в
      @inline implicit def intWrapper(x: Int)         = new runtime.RichInt(x)
      /source>
      и, судя по всему, возвращаются со свежей ссылкой на RichInt (так как дальше вызывают через invokevirtual)
      
      почему метод называется RichInt$.to$extension0, надо копать.


      1. IvanGolovach Автор
        22.04.2015 22:32

        Хотя сигнатура метода intWrapper (I)I — как будто int -> int, а RichInt$ — может быть companion object с «статическими методами» (на самом деле методами одного синглетонного экземпляра, что лежит в статическом scala.runtime.RichInt$.MODULE$)


      1. IvanGolovach Автор
        22.04.2015 22:34

        Вот все, надо смотреть

        
        D:\Program Files\Java\jdk1.8.0\bin>javap -p -c -v Demo
        Classfile /D:/Program Files/Java/jdk1.8.0/bin/Demo.class
          Last modified Apr 22, 2015; size 1076 bytes
          MD5 checksum 7f7e8e8353c9bf7f6bf70c6c4858c5b8
          Compiled from "Demo.scala"
        public class tmp.Demo
          SourceFile: "Demo.scala"
          InnerClasses:
               public static #42= #39 of #41; //Inclusive=class scala/collection/immutable/Range$Inclusive of class scala/collection/immutable/Range
          RuntimeVisibleAnnotations:
            0: #6(#7=s#8)
        Error: unknown attribute
            ScalaSig: length = 0x3
             05 00 00
          minor version: 0
          major version: 50
          flags: ACC_PUBLIC, ACC_SUPER
        Constant pool:
           #1 = Utf8               tmp/Demo
           #2 = Class              #1             //  tmp/Demo
           #3 = Utf8               java/lang/Object
           #4 = Class              #3             //  java/lang/Object
           #5 = Utf8               Demo.scala
           #6 = Utf8               Lscala/reflect/ScalaSignature;
           #7 = Utf8               bytes
           #8 = Utf8               ¦O§2A!OOO>\t!A)Z7p§¦v¦!%s7\r\;tSZ,'BO0^<§\t!
        ¦1O#¦CA¦$!\t!¦BAOJ]R¦
           #9 = Utf8               f
          #10 = Utf8               (I)Lscala/collection/immutable/Range$Inclusive;
          #11 = Utf8               scala/runtime/RichInt$
          #12 = Class              #11            //  scala/runtime/RichInt$
          #13 = Utf8               MODULE$
          #14 = Utf8               Lscala/runtime/RichInt$;
          #15 = NameAndType        #13:#14        //  MODULE$:Lscala/runtime/RichInt$;
          #16 = Fieldref           #12.#15        //  scala/runtime/RichInt$.MODULE$:Lscala/runtime/RichInt$;
          #17 = Utf8               scala/Predef$
          #18 = Class              #17            //  scala/Predef$
          #19 = Utf8               Lscala/Predef$;
          #20 = NameAndType        #13:#19        //  MODULE$:Lscala/Predef$;
          #21 = Fieldref           #18.#20        //  scala/Predef$.MODULE$:Lscala/Predef$;
          #22 = Utf8               intWrapper
          #23 = Utf8               (I)I
          #24 = NameAndType        #22:#23        //  intWrapper:(I)I
          #25 = Methodref          #18.#24        //  scala/Predef$.intWrapper:(I)I
          #26 = Utf8               to$extension0
          #27 = Utf8               (II)Lscala/collection/immutable/Range$Inclusive;
          #28 = NameAndType        #26:#27        //  to$extension0:(II)Lscala/collection/immutable/Range$Inclusive;
          #29 = Methodref          #12.#28        //  scala/runtime/RichInt$.to$extension0:(II)Lscala/collection/immutable/Range$Inclusive;
          #30 = Utf8               this
          #31 = Utf8               Ltmp/Demo;
          #32 = Utf8               x
          #33 = Utf8               I
          #34 = Utf8               <init>
          #35 = Utf8               ()V
          #36 = NameAndType        #34:#35        //  "<init>":()V
          #37 = Methodref          #4.#36         //  java/lang/Object."<init>":()V
          #38 = Utf8               scala/collection/immutable/Range$Inclusive
          #39 = Class              #38            //  scala/collection/immutable/Range$Inclusive
          #40 = Utf8               scala/collection/immutable/Range
          #41 = Class              #40            //  scala/collection/immutable/Range
          #42 = Utf8               Inclusive
          #43 = Utf8               Code
          #44 = Utf8               LocalVariableTable
          #45 = Utf8               LineNumberTable
          #46 = Utf8               SourceFile
          #47 = Utf8               InnerClasses
          #48 = Utf8               RuntimeVisibleAnnotations
          #49 = Utf8               ScalaSig
        {
          public scala.collection.immutable.Range$Inclusive f(int);
            descriptor: (I)Lscala/collection/immutable/Range$Inclusive;
            flags: ACC_PUBLIC
            Code:
              stack=3, locals=2, args_size=2
                 0: getstatic     #16                 // Field scala/runtime/RichInt$.MODULE$:Lscala/runtime/RichInt$;
                 3: getstatic     #21                 // Field scala/Predef$.MODULE$:Lscala/Predef$;
                 6: iconst_1
                 7: invokevirtual #25                 // Method scala/Predef$.intWrapper:(I)I
                10: iload_1
                11: invokevirtual #29                 // Method scala/runtime/RichInt$.to$extension0:(II)Lscala/collection/immutable/Range$Inclusive;
                14: areturn
              LocalVariableTable:
                Start  Length  Slot  Name   Signature
                    0      15     0  this   Ltmp/Demo;
                    0      15     1     x   I
              LineNumberTable:
                line 4: 3
        
          public tmp.Demo();
            descriptor: ()V
            flags: ACC_PUBLIC
            Code:
              stack=1, locals=1, args_size=1
                 0: aload_0
                 1: invokespecial #37                 // Method java/lang/Object."<init>":()V
                 4: return
              LocalVariableTable:
                Start  Length  Slot  Name   Signature
                    0       5     0  this   Ltmp/Demo;
              LineNumberTable:
                line 5: 0
        }
        


    1. darkdimius
      23.04.2015 00:37

      scala компилятор никогда не делает статические методы. Он только делает статические forwarders для методов в синглтон-обьектах.


      1. eld0727
        23.04.2015 04:51

        Ну я это утрировано сказал


  1. ShadowsMind
    22.04.2015 20:10
    +2

    Спасибо автору за данную статью. Да и вообще побольше бы статей про Scala на хабре.
    Вообще довольно таки странно, что есть постоянные рубрики Дайджест + php / python / веб-разработки / мобильной разработки и т.д. Но при этом нет не то чтобы дайджеста по Java / Scala / Groovy / Clojure etc., а хотя бы по JVM в целом. Имхо, такая рубрика бы взлетела… Разные подкасты(например «Разбор полетов») очень хорошо заходят, вот бы еще более подробную инфу в виде статьи-сборки последних событий получать. Может кто-то из людей в теме попробует организовать «Дайджест из мира JVM»? )


    1. IvanGolovach Автор
      22.04.2015 20:53
      +1

      А что Вы имеете в виду под 'дайджестом'?
      1) Глубокий обзор фич всех времен и народов («Все что только можно делать c gc, java.lang.references.*, ...», «Все 100500 способов работать с многопоточностью», ...)?
      2) Обзор новинок («Все новые трюки с Java 8», «Что нового в Scala 2.11», ...)?
      3) Просто новости рынка (по примеру ленты у theserverside.com, infoq.com/java/news/)?


      1. ShadowsMind
        22.04.2015 21:05

        Обзор новых фич, фрэймворков/либ(и мажорных изменениях в наиболее важных из них), нововведений и т.д… Сбор ссылок на хорошие статьи — тоже весьма удобная штука. Все это можно в интернете и так найти, но не всегда удается за всем следить.


        1. IvanGolovach Автор
          22.04.2015 21:27

          Мне кажется, это уровень целого «сетевого издательства» или какого-то безумного гения. Отслеживать и публиковать изменения в 3-6 языках (Java, Scala, Clojure, Groovy, JRuby, Jython) с развитой системой фреймворков — это безумный труд. И странная специализация для одного человека.

          Я сидел и в java 10 лет, но только местами узнавал, что Log4j2 догнал, а то и перегнал по фичам slf4j, или что асинхронное логирование в Log4J2 сделано на основе сверхбыстродействующего биржевого фреймворка Desruptor.

          А остслеживать такое постоянно и на всех «фронтах» — респект такому мозгу!


          1. darkdimius
            23.04.2015 08:52

            Пока я жил в Москве, тоже не знал о таких вещах.
            Как оказалось это вопрос «удаления» от тех людей, которые вершат изменения. Я не припоминаю чтобы кто-либо из моих знакомых в Москве ездил на конференции по другим странам, а тем более был докладчиком.

            Сейчас, находясь в Швейцарии, работая над Scala я обычно узнаю все Scala-новости в течении недели.

            сверхбыстродействующего биржевого фреймворка Desruptor


            Disruptor это очень быстрая реализация фиксированного по размеру кольцевого буфера. Быстрая она не из за фундаментальных нововведений, а из за того что люди полезли в железо и учли мелкие детали, например порядок полей, выравнивание, конфликт строк памяти при запуске Cache Coherence Protocol, итд.


            1. IvanGolovach Автор
              23.04.2015 13:29

              Ну это я так, для красного словца сказал.


        1. Optik
          23.04.2015 00:41

          www.cakesolutions.net/teamblogs обзоры еженедельные.


  1. Aivean
    22.04.2015 20:20

    Спасибо за статью, узнал кое-что новое (Infix types) и повторил старое, но редкоиспользуемое.

    Парочка замечаний:

    • В примере с факториалом точка с запятой нужна, чтобы компилятор не искал «второй» операнд на следующей строке. Альтернатива странно смотрящейся точке с запятой будет просто одна пустая строка перед «println».
    • Почему-то в начале статьи вы пишете implicit с «d» на конце
    • Я знаю, что в комментариях не принято писать о TYPO, но все же, исправьте «type constcructor»


    1. IvanGolovach Автор
      22.04.2015 20:55

      Спасибо, исправил.


  1. darkdimius
    22.04.2015 23:58
    +3

    Как один из разработчиков компилятора говорю — не нужно бездумно совать всюду `@inline`. Тут — это не помогает компилятору никак, но замусоривает ваш код. Каждая такая аннотация должна быть хорошо продумана.


  1. darkdimius
    23.04.2015 00:01
    +3

    DelayedInit тоже не стоит описовать в курсе вообще! Oн deprecated и не будет поддерживаться в будущем компиляторе — Dotty.


    1. IvanGolovach Автор
      23.04.2015 01:08

      Спасибо, не знал. Да, во второй части из ленивого, конечно, будут Streams как пример ленивых структур данных.


      1. darkdimius
        23.04.2015 08:40
        +2

        Streams тоже to-be deprecated, потому что они дают ложное впечатление контроля.
        Они не deprecated потому что мы замену еще не сделали — она будет основана на инфраструктуре Dotty и будет иметь много общего со Stream Fusion из Haskell.


    1. Optik
      23.04.2015 09:03
      +1

      Насколько близко будущее с Dotty и есть уже более конкретный список различий (относительно stackoverflow.com/questions/20130546/how-to-program-in-scala-to-be-forward-compatible-with-dotty), который может потребовать переписывания кода приложений?


      1. darkdimius
        23.04.2015 09:34
        +5

        Слушайте доклад Мартина на scaladays.org 8 июня.
        Мое мнение — скоро оба компилятора будут сосуществовать. Основные отличия для меня(это мое мнение и не факт что все выйдет так как я думаю):

        • появится другая екосистема, дружественная к оптимизаторам(моя область) и метапрограммированию(Женя Бурмако), если коротко — теперь scala будет компилироваться в некое промежуточное представление, а оно уже будет компилироваться в JVM bytecode\javascript\LLVM. LLVM — под вопросом. Но один активист делает титанические усилия в эту сторону.
        • Выдадим пользователю типа объединения(union) и типы пересечения(intersection). Для меня это заменяет enum-ы на чтото что разумно.
        • Изменится схема компиляции lazy val-ов. Теперь lazy val без `@volatile` не thread-safe, и доступ к таким быстрее раз в 10. `@volatile` lazy val будет быстрее на 10-20%, позволит инициализовывать несколько lazy полей одновременно в одном объекте(он почти не синхронизуется на this), и вообще в большинстве случаев будет lock-free и значит без дедлоков.
        • метапрограммирование — scalameta.org обычные макросы не поддерживаются.
        • частью экосистемы станет dotty linker: штукенция которая делает глобальный анализ программы, смотрит как вы используете библиотеки, тюнит их для вас и встраивает в ваше приложение. Это реинкарнация scala-blitz.github.io. Позволяет писать высокоуровневый код, зная что компилятор сможет заменить его на эффективный. Из спорных вещей — это аналог статической линковки, те при изменении зависимостей нужна перекомпиляция. Это не часть Dotty, тк в некоторых ситуациях люди привыкли менять один jar чтобы пофиксить баги в зависимостях. Это только для тех кому нужна максимальная производительность, или минимальный размер(Android например). Он же будет делать specialization только для тех классов которые используются. В теории это может позволить нам сделать reified generics но мы не решили хорошо ли это.
        • более быстрый компилятор — мы еще не тюнили его, но даже сейчас он примерно такойже по скорости компиляции как scalac
        • arrays of value classes
        • value classes over value classes
        • под вопросом — multifield value classes.


        Как видите, мы хотим улучшить многое. А любое улучшение приходит с багами. Потому и будут два компилятора.
        В последнее время, с момента появления Typesafe, scala стала намного более консервативной, и это понятно и разумно.
        Нужно понимать что в dotty мы(лаборатория Мартина) хотим проводить исследования того, каким язык должен быть.
        Нужна стабильность — scalac. Нужны новые фичи — Dotty.
        Сколько продлится сосуществование — мне самому любопытно. Это уже вопрос граничащий с коммерческими вопросами для Typesafe, тк они продают многолетние контракты поддержки.


  1. darkdimius
    23.04.2015 00:04

    В список тем для курса ИМХО стоит добавить default arguments,
    а в Pattern matching добавить name-based pattern matching.


    1. IvanGolovach Автор
      23.04.2015 01:07

      Да, named и default аргументы я читаю в ООП/методах+конструкторах. Достаточно краткая тема. Или я чего-то не знаю?


      1. darkdimius
        23.04.2015 08:45

        Зависит от того насколько глубокий курс. Если вы рассказываете теорию множеств то можно пояснить почему default arguments не дружат с overload resolution.

        Кстати, если курс предназначен для разработчиков, то из стандартного инструментария — macros, pickling(самый быстрый сериализатор, тк 90% работы делает во время компиляции и по возможности избегает reflection), spores(убеждается что лябмды не захватили чтото случайно в окружении, помогает с memory leak-ами бороться).

        Из вещей которые стоит не рассказывать(точнее расказать что не стоит использовать) — xml как часть языка. Уже давно есть сильное намерение вынести его в string interpolation. В Dotty эта часть языка не поддерживается как таковая.


  1. darkdimius
    23.04.2015 00:07
    +3

    и, ИМХО, любой толковый курс по Scala сейчас должен включать String Interpolation,
    А также лучше б ему включать хотябы минимум про blackbox macros.
    Иначе вы упомянули shapeless, но не то на чем он построен.