В этом уроке вы узнаете:
По словам Пирса: “Система типов – это синтаксический способ для автоматической проверки отсутствия определенного ошибочного поведения, путем классификации фраз программы по видам значений, которые они вычисляют”.
Типы позволяют определить область определения функции и ее множество значений. Например, из математики, мы привыкли видеть:
f: R -> N
это говорит нам, что функция “f” отображает значения из множества действительных чисел в значения множества натуральных чисел.
Как-то абстрактно, это означает, что нам нужен конкретный тип. Система типов дает нам больше возможностей для выражения этих множеств.
Учитывая эти замечания, компилятор теперь может статически (во время компиляции) проверить корректность программы. Это означает, что компиляция будет прервана, если значения (во время выполнения) не будут соответствовать ограничениям накладываемым программой.
Вообще говоря, проверка типов может лишь гарантировать то, что некорректные программы не будут скомпилированы. Она не может гарантировать, что каждый вариант программы будет скомпилирован.
С увеличением выразительности в системе типов, мы можем писать более надежный код, потому что это позволяет нам доказать инварианты в нашей программе еще до того, как она заработает (по ошибкам самих типов в модуле, конечно!). Попытки научного сообщества расширить границы выразительности очень тяжелы, в том числе выразительность типов зависящих от значений!
Обратите внимание, что вся информация о типе удаляется во время компиляции. Она больше не нужна. Это называется стиранием.
Мощная система типов Scala позволяет писать очень богатые выражения. Вот главные особенности:
val i: Int = 12: Int
Полиморфизм используется для того, чтобы писать общий код (для значений различных типов) без ущерба для всего богатсва статической типизации.
Например, без параметрического полиморфизма, общий список структур данных всегда будет выглядеть следующим образом (и это действительно выглядело так в 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, и которые являются “слишком общими” для компилятора, чтобы он их понял. Предположим, у вас есть некоторые функции
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