В этом уроке вы узнаете:

Что такое статические типы? Почему они полезны?

По словам Пирса: “Система типов – это синтаксический способ для автоматической проверки отсутствия определенного ошибочного поведения, путем классификации фраз программы по видам значений, которые они вычисляют”.

Типы позволяют определить область определения функции и ее множество значений. Например, из математики, мы привыкли видеть:

f: R -> N

это говорит нам, что функция “f” отображает значения из множества действительных чисел в значения множества натуральных чисел.

Как-то абстрактно, это означает, что нам нужен конкретный тип. Система типов дает нам больше возможностей для выражения этих множеств.

Учитывая эти замечания, компилятор теперь может статически (во время компиляции) проверить корректность программы. Это означает, что компиляция будет прервана, если значения (во время выполнения) не будут соответствовать ограничениям накладываемым программой.

Вообще говоря, проверка типов может лишь гарантировать то, что некорректные программы не будут скомпилированы. Она не может гарантировать, что каждый вариант программы будет скомпилирован.

С увеличением выразительности в системе типов, мы можем писать более надежный код, потому что это позволяет нам доказать инварианты в нашей программе еще ​​до того, как она заработает (по ошибкам самих типов в модуле, конечно!). Попытки научного сообщества расширить границы выразительности очень тяжелы, в том числе выразительность типов зависящих от значений!

Обратите внимание, что вся информация о типе удаляется во время компиляции. Она больше не нужна. Это называется стиранием.

Типы в Scala

Мощная система типов Scala позволяет писать очень богатые выражения. Вот главные особенности:

Параметрический полиморфизм

Полиморфизм используется для того, чтобы писать общий код (для значений различных типов) без ущерба для всего богатсва статической типизации.

Например, без параметрического полиморфизма, общий список структур данных всегда будет выглядеть следующим образом (и это действительно выглядело так в Java до введения обобщенных типов):

scala> 2 :: 1 :: "bar" :: "foo" :: Nil
res5: List[Any] = List(2, 1, bar, foo)

Сейчас мы не можем восстановить информацию о типе каждого элемента списка

scala> res5.head
res6: Any = 2

и наше приложение будет опираться на ряд вызовов (“asInstanceOf[]”), и у нас будет отсутствовать типобезопасность (потому что все вызовы динамические).

Полиморфизм достигается через определение типовых переменных.

scala> def drop1[A](l: List[A]) = l.tail
drop1: [A](l: List[A])List[A]

scala> drop1(List(1,2,3))
res1: List[Int] = List(2, 3)

Scala обладает полиморфизмом 1-го ранга

Проще говоря, это означает, что существуют некоторые концепции типов, которые вы захотите использовать в Scala, и которые являются “слишком общими” для компилятора, чтобы он их понял. Предположим, у вас есть некоторые функции

def toList[A](a: A) = List(a)

которые вы хотите использовать:

def foo[A, B](f: A => List[A], b: B) = f(b)

Это определение не скомпилируется, потому что все типовые переменные должны быть определены при вызове. Даже если вы “точно объявили” тип B,

def foo[A](f: A => List[A], i: Int) = f(i)

… вы получите ошибку типа.

Вывод типа

Традиционным возражением против статической типизации является то, что она имеет много синтаксических накладных расходов. Scala снимает это ограничение путем предоставления вывода типа.

Классический метод для вывода типов в функциональных языках программирования – это метод Хиндли-Милнер, который был впервые применен в ML.

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

В Scala, например, вы не можете выполнить следующие действия:

scala> { x => x }
<console>:7: error: missing parameter type
       { x => x }

Хотя в OCaml, вы можете:

# fun x -> x;;
- : 'a -> 'a = <fun>

В Scala все типы выводятся на месте. Скала считывает одно выражение за раз. Например:

scala> def id[T](x: T) = x
id: [T](x: T)T

scala> val x = id(322)
x: Int = 322

scala> val x = id("hey")
x: java.lang.String = hey

scala> val x = id(Array(1,2,3,4))
x: Array[Int] = Array(1, 2, 3, 4)

Сейчас типы определены, Scala компилятор вывел типовый параметр для нас. Отметим также, что нам не нужно указывать тип возвращаемого значения явно.

Изменчивость

Система типов Scala составляет иерархию классов вместе с полиморфизмом. Иерархия классов позволяет определять отношения для подтипов. Главный вопрос, который появляется при смешивании ООП с полиморфизмом: если T’ является подклассом T, является ли Container[T’] подклассом Container[T]? Разница в замечаниях позволяет выразить следующие отношения между иерархией классов и полиморфными типами:

Означает Нотация языка Scala
ковариант C[T’] это подкласс класса C[T] [+T]
контрвариант C[T] это подкласс класса C[T’] [-T]
инвариант C[T] и C[T’] не взаимосвязаны [T]

Связь подтипа на самом деле означает: если T’ является подтипом, для данного типа T, можете ли вы заменить его?

scala> class Covariant[+A]
defined class Covariant

scala> val cv: Covariant[AnyRef] = new Covariant[String]
cv: Covariant[AnyRef] = Covariant@4035acf6

scala> val cv: Covariant[String] = new Covariant[AnyRef]
<console>:6: error: type mismatch;
 found   : Covariant[AnyRef]
 required: Covariant[String]
       val cv: Covariant[String] = new Covariant[AnyRef]
                                   ^
scala> class Contravariant[-A]
defined class Contravariant

scala> val cv: Contravariant[String] = new Contravariant[AnyRef]
cv: Contravariant[AnyRef] = Contravariant@49fa7ba

scala> val fail: Contravariant[AnyRef] = new Contravariant[String]
<console>:6: error: type mismatch;
 found   : Contravariant[String]
 required: Contravariant[AnyRef]
       val fail: Contravariant[AnyRef] = new Contravariant[String]
                                     ^

контрвариант выглядит странным. Когда он используется? Небольшой сюрприз!

trait Function1 [-T1, +R] extends AnyRef

Если вы думаете о замещении, в этом есть смысл. Давайте сначала определим простую иерархию классов:

scala> class Animal { val sound = "rustle" }
defined class Animal

scala> class Bird extends Animal { override val sound = "call" }
defined class Bird

scala> class Chicken extends Bird { override val sound = "cluck" }
defined class Chicken

Допустим вам нужна функция, которая принимает параметр Bird:

scala> val getTweet: (Bird => String) = // TODO

Простая библиотека животных имеет функцию, которая делает то, что вам нужно, но она принимает параметр Animal. В большинстве ситуаций, если вы говорите “Мне нужно ___, у меня есть подкласс класса ___”, все будет хорошо. Но параметры функции контрвариантны. Если вам нужна функция, которая принимает параметр Bird и у вас есть функция которая принимает параметр Chicken, функция будет “шокирована” параметром Duck. Но с функцией, которая принимает параметр Animal все будет хорошо:

scala> val getTweet: (Bird => String) = ((a: Animal) => a.sound )
getTweet: Bird => String = <function1>

Возвращаемый тип значения функции – ковариантен. Если вам нужна функция, которая возвращает Bird, но имеется функция, которая возвращает Chicken, то все хорошо.

scala> val hatch: (() => Bird) = (() => new Chicken )
hatch: () => Bird = <function0>

Ограничения

Scala позволяет ограничить полиморфные переменные, используя ограничения. Эти ограничения отражают отношения подтипов.

scala> def cacophony[T](things: Seq[T]) = things map (_.sound)
<console>:7: error: value sound is not a member of type parameter T
       def cacophony[T](things: Seq[T]) = things map (_.sound)
                                                        ^

scala> def biophony[T <: Animal](things: Seq[T]) = things map (_.sound)
biophony: [T <: Animal](things: Seq[T])Seq[java.lang.String]

scala> biophony(Seq(new Chicken, new Bird))
res5: Seq[java.lang.String] = List(cluck, call)

Нижние границы типа, также поддерживаются. Они связаны с контрвариантом. Скажем, у нас есть некоторый класс Node:

scala> class Node[T](x: T) { def sub(v: T): Node[T] = new Node(v) }

Но мы хотим сделать ковариантным тип T:

scala> class Node[+T](x: T) { def sub(v: T): Node[T] = new Node(v) }
<console>:6: error: covariant type T occurs in contravariant position in type T of value v
       class Node[+T](x: T) { def sub(v: T): Node[T] = new Node(v) }
                                      ^

Напомним, что аргументы метода являются контрвариантными, и поэтому, если мы выполняем нашу замену, используя те же классы, как и раньше:

class Node[Bird](x: Bird) { def sub(v: Bird): Node[Bird] = new Node(v) }

этот класс не является подклассом класса

class Node[Animal](x: Animal) { def sub(v: Animal): Node[Animal] = new Node(v) }

потому что Animal не может быть заменен на Bird внутри “sub”. Тем не менее, мы можем использовать нижнюю границу для обеспечения корректности.

scala> class Node[+T](x: T) { def sub[U >: T](v: U): Node[U] = new Node(v) }
defined class Node

scala> (new Node(new Bird)).sub(new Bird)
res5: Node[Bird] = Node@4efade06

scala> ((new Node(new Bird)).sub(new Bird)).sub(new Animal)
res6: Node[Animal] = Node@1b2b2f7f

scala> ((new Node(new Bird)).sub(new Bird)).asInstanceOf[Node[Chicken]]
res7: Node[Chicken] = Node@6924181b

scala> (((new Node(new Bird)).sub(new Bird)).sub(new Animal)).sub(new Chicken)
res8: Node[Animal] = Node@3088890d

Заметьте также как тип изменяется в последующих вызовах “sub”.

Квантификация

Иногда вы не заботитесь о том, чтобы дать имя типовой переменной, например:

scala> def count[A](l: List[A]) = l.size
count: [A](List[A])Int

Вместо этого вы можете использовать “заменитель”:

scala> def count(l: List[_]) = l.size
count: (List[_])Int

Это короткая запись для:

scala> def count(l: List[T forSome { type T }]) = l.size
count: (List[T forSome { type T }])Int

Заметьте, что квантификация может получиться сложной:

scala> def drop1(l: List[_]) = l.tail
drop1: (List[_])List[Any]

А вдруг мы потеряли информацию о типе! Чтобы понять, что происходит, вернемся к “зубодробительному” синтаксису:

scala> def drop1(l: List[T forSome { type T }]) = l.tail
drop1: (List[T forSome { type T }])List[T forSome { type T }]

Мы не можем ничего сказать о T, потому что тип не позволяет это сделать.

Вы можете также применять ограничения для замены типовых переменных:

scala> def hashcodes(l: Seq[_ <: AnyRef]) = l map (_.hashCode)
hashcodes: (Seq[_ <: AnyRef])Seq[Int]

scala> hashcodes(Seq(1,2,3))
<console>:7: error: type mismatch;
 found   : Int(1)
 required: AnyRef
Note: primitive types are not implicitly converted to AnyRef.
You can safely force boxing by casting x.asInstanceOf[AnyRef].
       hashcodes(Seq(1,2,3))
                     ^

scala> hashcodes(Seq("one", "two", "three"))
res1: Seq[Int] = List(110182, 115276, 110339486)

Смотрите также: Реальные типы в Scala от D. R. MacIver