Статьи

Ускоренный курс по типам Scala

После многих лет разработки Java обнаружение системы типов Scala и связанных с ней функций стало для меня чем-то вроде ухода. Достаточно сказать, что GADT не был моим первым четырехбуквенным высказыванием при изучении сопоставления с образцом для типов, не говоря уже о том, что, когда и как использовать дисперсионные аннотации и обобщенные ограничения типов. Для начала вот несколько небольших, но мощных бинго модных слов в системе типов:
… Scala — это статически, строго типизированный язык с неявным выводом типов и поддержкой структурных и экзистенциальных типов. Он также имеет параметризованные типы, абстрактные и фантомные типы и способен неявно конвертировать типы данных. Эти основные возможности используются границами контекста и представления и дополнены обобщенными ограничениями типов, чтобы обеспечить мощные контракты времени компиляции. Кроме того, Scala поддерживает аннотации типов сайтов объявлений для упрощения инвариантной, ковариантной и контравариантной дисперсии типов…

Одним словом, Ой!

В оставшейся части этого поста я постараюсь демистифицировать эти концепции и засеять семена интриги для дальнейшего изучения.

Для того, чтобы держать пост на управляемой длине, некоторые детали будут предоставлены, а ссылки предоставлены (как в конце, так и в строке), чтобы читатель мог проводить их самостоятельно. Если позволят время и пространство, я постараюсь кратко рассказать о том, как и почему некоторые из этих функций представлены в контексте их практической значимости и последствий. Заранее извиняюсь за длину поста, но есть много места для покрытия и много возможностей для читателя, чтобы просмотреть. Имейте это в виду, поскольку в них есть золото, что неудивительно, если учесть всю глубину возможностей, которые язык пытается добыть.

Итак, сначала, что такое система типов?

Одно из определений авторов (которое я нашел тщательным, если не слегка загадочным):

«..Система типов — это гибкий синтаксический метод для доказательства отсутствия определенных поведений программы путем классификации фраз в соответствии с типами значений, которые они вычисляют» из « Типов и языков программирования » Бена Пирса — библии для всех, кто интересуется теорией системы типов ..

И снова Ой!

Я предпочитаю непрофессиональную интерпретацию этого, при которой тип предоставляет какую-то метку для системы типов. Это, в свою очередь, позволяет системе типов доказать (или ограничить) некоторые свойства поведения программ.
Практически система типов позволяет либо компилятору [обычно], либо среде выполнения добавлять некоторый смысл к данным и значениям / переменным (что я и впредь буду называть в общих чертах как поля / элементы из-за перегрузки терминов val и var в Scala), чтобы реагировать или терпеть неудачу соответственно.

2 толстые дамы …

Таким образом, для деконструкции лозунга выше, основным атрибутом в списке является статический тип . Так что это значит? Статическая типизация обеспечивает ограничения, проверки и противовесы COMPILE TIME и, как таковая, обеспечивает первую линию защиты, контроль качества и обратную связь от программных ошибок. Обратной стороной этого является динамическая типизация , при которой тип присваивается элементам во время выполнения. Помимо раннего цикла обратной связи, другие часто упоминаемые преимущества статической типизации:
  • лучшая производительность и возможность оптимизации кода — так как компилятор может выполнять больше оптимизаций и избавляет от необходимости делать проверки типов во время выполнения.
  • лучшая неявная и явная поддержка документации — поскольку сигнатуры методов делают это неявным в коде и явным в любом коде, сгенерированном из источника, также можно использовать информацию о типе для передачи намерений авторов.
  • лучшая поддержка инструментов и возможность анализа кода — так как инструменты могут проверять типы, передаваемые между методами и в конструкторы и т. д.
  • лучшая поддержка корректности (см . теорему Райса , эту слайд-деку о структурной индукции и контролере типов и изоморфизме Карри-Ховарда ) — как мы увидим далее в этой части, корректность может быть дополнительно поддержана разумным использованием констант типов.
  • лучшая абстракция / модульность — поскольку поддержка абстрактных типов позволяет автору формулировать проблему по-другому и (потенциально) более модульно.
Сказав, что на практике немногие языки являются исключительно динамическими или статически типизированными. Учитывая этот список возможностей системы статических типов, зачем кому-то использовать динамическую типизацию? Обычно считается, что динамическая типизация обеспечивает большую гибкость для построения слабосвязанных систем и для быстрого прототипирования. Это довольно естественные преимущества динамических языков, поскольку они не должны придерживаться ограничений, налагаемых компилятором, но это достигается ценой вышеупомянутых преимуществ статической типизации.

Учимся отпускать… отпускаем учиться…

Опять же, исходя из фона Java, мои естественные инстинкты предполагали: а) я достаточно хорошо понимал статическую типизацию, потратив много лет на разработку Java; б) что статически типизированные языки по своей природе раздуты и требуют большого количества стандартного кода. С точки зрения Scala интересно посмотреть, как быстро вы достигнете пределов поддержки Javas для типов «из коробки» (т.е. без добавления внешних библиотек или значительного взлома базовой системы), и как Scala разрушается. предположение, что статическая типизация == раздувание кода.
Одна из таких особенностей вырубки в Scala очевидна благодаря поддержке вывода типа . При этом элементы должны предоставлять информацию о своем типе «справа» только тогда, когда они объявлены. Таким образом, синтаксис для объявления элемента Scala имеет тенденцию упорядочиваться по значению [IMHO] со следующим приоритетом: статус изменяемости элементов; тогда название элемента; информация о типе (которая хранится исключительно, (и не повторяется .. верно, не повторяется ;-) ) «справа» .
1
2
3
4
5
6
7
8
9
val a = 1 // this type gets inferred to be of type Int
val b = "b" // this type in inferred to be a String
val c = 2.0 // this type is inferred to be a Double
 
case class SomeThing
class SomeOtherThing
 
val d = SomeThing // this type is instantiated as SomeThing. Not no need for the 'new' keyword as this is a case class
val e = new SomeOtherThing // This type requires the new keyword as no factory method is created for non case classes
Еще одно замечание относительно стратегии вывода типов, используемой в Scala, заключается в том, что «локальный вывод типов», то есть «вывод потоковых типов» , используется вместо Дамаса-Милнера (он же Хиндли-Милнер). aka HM ) стратегия, используемая в других статически типизированных, неявно выведенных языках (см. также Систему F и ее варианты).

Типы в кубах

Действительно, многое сделано из-за сложности и богатства поддержки типов в Scala, причем абстрактные типы Scala являются причиной путаницы и большого беспокойства (например, см. Этот пост для примера). Как следует из названия, абстрактные типы в Scala позволяют ссылаться на типы в реферате и, следовательно, использовать его в качестве членов классов на уровне поля. Это означает, что поля типов могут выступать в качестве заполнителей для типов, которые будут реализованы позднее (что позволяет лениво проектировать конкретные типы в качестве решения обнаруженной проблемы… обеспечивая только достаточное ( lagom ™ ) входное значение для проектирования). В некотором смысле они похожи на ссылки на типы, используемые в параметризованных типах (то есть типах, для которых требуется объявление параметра типа, таких как универсальные шаблоны Java), хотя они и разрываются из своих контейнеров Collection. (Обратите внимание, что идиоматическое различие между параметризованными типами и абстрактными типами имеет тенденцию различать индикатор типа, используемый для коллекции, по сравнению с другими сценариями. См. « Статически безопасная альтернатива виртуальным типам »).
1
2
3
4
5
6
import scala.collection.GenSeq
 
trait SimpleListTypeContainer {
    type Simple                         // declare an abstract type with the label Simple
    type SimpleList <: GenSeq[Simple]   // Constrain the Simple List abstract type based on the previously defined abstract type
}
Дополнительной особенностью абстрактных типов является возможность использования границ типов (т.е. фактические конкретные типы, которые разрешены для объявления, могут быть программно ограничены и применены во время компиляции). Это позволяет сделать намерение в коде явным, например, при попытке предложить определенные типы специализации (например, семейный полиморфизм ).
В сочетании с собственными типами это создает мощный набор типов инструментов. Self-типы позволяют ссылкам «this» быть явно привязанными к другому классу с помощью ключевого слова «self» (поэтому в коде можно сделать так, чтобы ссылка «this» означала тип, отличный от фактического содержащего типа).
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
trait DB { def startDB: Unit }  // defines an abstract start for a DB component
trait MT { def startMT: Unit }  // defines an abstract start for an MT component
 
trait Oracle extends DB { def startDB = println("Starting Oracle") } // Some actual concrete instances.. dummied for example
trait Service extends MT { def startMT = println("Starting Service") }
 
trait App {
    self: DB with MT => // declare that self for App refers to a DB with MT, so that we have access to the startXX ops
 
    startDB
    startMT
}
 
object DummyApp extends App with Oracle with Service // create a concrete instance with an actual DB and MT instance
DummyApp.run // run it and see "Starting Oracle" then "Starting Service"
Self-типы имеют ряд применений, таких как: предоставление признаков или видимости абстрактных классов для полей и / или методов класса, в который они маскируются / смешиваются; как безопасный для типов (т. е. проверенный временем компиляции) способ выполнения декларативного внедрения зависимостей (см. шаблон торт ).

Одна маленькая утка

Scala также облегчает типизированную типизацию утки посредством структурных типов (см. Здесь для сравнения структурной типизации и типизации уток).
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
class Duck {
    def squawk = println("Quack")
    def waddle = println("Duck walk")
}
class Pengiun {
    def squawk = println("Squeek")
    def waddle = println("Penguin walk")
}
class Person { }
 
// everybody's heard about the word...
def birdIsTheWord(bird : { def squawk; def waddle}) = {
    bird.squawk
    bird.waddle
}
 
birdIsTheWord(new Duck())      // prints "Quack" then "Duck walk"
birdIsTheWord(new Penguin())   // prints "Squeek" then "Penguin walk"
birdIsTheWord(new Person())    // Will not compile
Способность использовать классы в соответствии с некоторыми особенностями их структуры (а не по имени) имеет ряд применений, таких как: в сочетании с последствиями (которые еще должны появиться) как часть шаблона интеллектуального адаптера ; для создания быстрого специального кода прототипирования; в качестве активатора для повторного использования метода в случаях, когда клиентские классы не связаны, но имеют общую структурную особенность (Примечание: типичный пример, который здесь описывается, это Gun и Camera, это не связанные элементы, но оба имеют метод shoot ()! Например, имеет двойную цель — также подчеркнуть присущие структурной и динамической типизации опасности как таковые. По какой-то причине этот пример всегда напоминает мне о фильмах начала 90-х.
До настоящего времени наблюдалось, что несколько вышеупомянутых структурных блоков системного типа вызывают изменение мышления и подхода к проблемам. На самом деле, понятие «мышление в Scala» — это не синтаксическая сложность (IMHO), а то, что является « лучшим » (по любой субъективной мере лучшего ) идиоматическим использованием предоставленного обширного набора функций. Лично я обнаружил, что деконструирую проблемы в их ожидаемые входы и желаемые результаты, и смотрю на моделирование моей проблемной области с точки зрения типов и операций, которые происходят с этими типами, вместо того, чтобы смотреть на Мир сквозь объектные очки.
К счастью, есть [ссылка на нижнюю часть страницы] некоторые [/ link] ресурсы, которые помогают при попытках дальнейшего исследования, а некоторые идиомы (например, теоремы ) предоставляются бесплатно! Одной из таких конструкций является конструкция зависимых типов , для которой создание экземпляров Tuple представляет собой самый простой пример. Расширяя понятие зависимых типов и опираясь на врожденные возможности вложения, Scala также поддерживает пути-зависимые типы . Как следует из названия, любые созданные типы вращаются вокруг пространства имен, в котором они созданы. Идиоматически, зависимые от пути типы использовались в создании компонентно-ориентированного программного обеспечения и в методе Cake Pattern для обработки внедрения зависимостей. Интересно, что зависимые от пути и значения типы также могут чередоваться, как показано в этом примере .
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
// First a simple example of Tuple creation
 
val myTuple2 = ("A", 1)
val myTuple3 = ("B", 2, true) // creates a Tuple3 of String, Int, Boolean
 
// Not on to the path dep types stuff
 
trait BaseTrait {
 type T
 val baseA : T
}
 
class ChildA[String](param : String) extends BaseTrait {
 type T = String
 override val baseA = param
    println(baseA)
}
 
val childA = new ChildA("A")
 
type B = String
 
class ChildB[String](param : String) extends BaseTrait {
 // type T = B  // Won't compile - baseA (below) has an incompatible type !
 // type T = childA.T // Won't compile - baseA (below) has an incompatible type !
 type T = String
 override val baseA = param
    println(baseA)
}
 
val childB = new ChildB("B")

Отклонения аннотации .. ооо … простите?

Универсальные типы в Scala инвариантны по умолчанию (т.е. они могут принимать только точный тип в качестве параметра, с которым они были объявлены). Scala предоставляет другие аннотации отклонений, позволяющие использовать ковариационные типы (т. Е. Разрешать любые дочерние элементы объявленных типов), а также противоречивость объявленных типов (т. Е. Родители объявленного типа также не допускаются никакими дочерними типами). Эти объявления отклонений (или аннотации отклонений) указываются на сайте объявлений (т. Е. Там, где объявляется исходный параметризованный тип), а не на сайте использования (как в случае с Java, например, каждое создание свободно определяет, что ожидается в Параметризованный тип для конкретного использования). Так что же все это значит и как это проявляется в коде? И какая польза от наличия таких ограничений на параметризованные типы? Вероятно, самое ясное объяснение этого я прочитал из превосходного программирования O’Reillys Scala . В дополнение к описанию, приведенному в программе scala, давайте рассмотрим значение их scala из func2-script:
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
// Taken from the excellent tutorial here:
// Note this sample leverages the Function1 trait: Function1[-T, +R]
 
// WON'T COMPILE
 
class CSuper                { def msuper = println("CSuper") }
class C      extends CSuper { def m      = println("C") }
class CSub   extends C      { def msub   = println("CSub") }
 
def useF(f: C => C) = {
  val c1 = new C     // #1
  val c2: C = f(c1)  // #2
  c2.msuper          // #3
  c2.m               // #4
}
 
useF((c: C)      => new C)        // #5
useF((c: CSuper) => new CSub)     // #6
useF((c: CSub)   => {println(c.msub); new CSuper})   // #7: ERROR!

Взято из: O’Reilly Программирование Scala

Учитывая, что функция Function1 имеет следующее объявление Function1 [-T, + R], аннотация дисперсии для типа для параметра с одним аргументом [бит -T объявления] является контравариантным и, следовательно, принимает явный объявленный тип и любого родителя тип T, тогда как тип возвращаемого значения [бит + R] является ковариантным и обеспечивает принудительное выполнение любого возвращаемого параметра либо типа R, либо дочернего типа R, т. е. тип возврата «is-an» R. что мы по контракту ожидаем, что любая функция сможет взять (как минимум) экземпляр T и вернуть R. Следовательно, любой клиентский код, использующий эту функцию, может быть уверен, что он сможет вызывать любые методы, объявленные на введите R в значение, возвращаемое из функции. Контравариантный входной параметр типа T здесь также допускает более широкое применение функции, то есть более широкая / универсальная функция может быть заменена экземпляром, который просто принимает тип T. Например, учитывая следующее (псевдо) описание Function1: Функция Function1 (Ninja, Pirate), замещающая (строго говоря, дочерний тип функции) функция, которая может принимать более универсальный тип (например, Person, т.е. Function1 (Person, Pirate)) и возвращать Pirate, будет допустимой функцией подстановки. в соответствии с объявленным договором. Точно так же любая функция, которая может принимать ниндзя и возвращать Длинного Джона Сильвера, была бы действительным преобразованием ниндзя в пирата. В этом случае функция фактически возвращает специализацию типа возврата Pirate. Рассматривая параметризованные типы в Scala, полезно упомянуть границы контекста в этом… контексте! Ограничения контекста расширяют функциональность данного типа, используя начальный тип, который будет использоваться в качестве типа для параметризованного типа. Например, для заданного класса A и признака B [T] типы контекста предоставляют синтаксический сахар, позволяющий создать экземпляр B [A] (следовательно, можно вызывать вызовы методов из типа B для экземпляра типа A). Синтаксический сахар здесь будет определен def someMethod [A: B] (a: A) = неявно [B [A]]. SomeMethodOnB (a) (Примечание: здесь есть отличная запись контекстных границ, предоставленных Дебасишем Гошем).

Еще раз … 79!

Обсудив ранее абстрактные типы, стоит посмотреть, как они соотносятся с только что упомянутыми аннотациями. Функционально (хотя и с определенной степенью чистоты) абстрактные типы и аннотации отклонений могут использоваться взаимозаменяемо. На практике две конструкции имеют разные истории и разные намерения. Аннотации отклонений в основном применяются для создания декалирований и обычно используются для параметризованных типов (например, при объявлении списков вещей или опций определенных типов). В мире Java параметризованные типы проявляются как Java Generics, и их использование гораздо более широко распространено в области OO, где наследование является ключевой особенностью.
Следствием поддержки абсолютных типов и границ типов в Scala является наличие фантомных типов . По сути, фантомные типы — это переменные типа, которые не создаются во время выполнения, но используются компилятором для обеспечения ограничений в исходном коде. Таким образом, фантомные типы можно использовать для программирования на уровне типов в Scala и добавления еще одного уровня поддержки правильности программы. (например, пример использования фантомных типов в безопасном отражении типов и шаблон безопасных типов с фантомными типами в Scala ). Границы представления связаны с фантомными типами, поскольку они дополнительно ограничивают использование типов на основе их способности быть преобразованными в другие типы. Пример такого использования границ вида был представлен в примере структурной типизации [link] [/ link] ранее. Границы представления также свойственны шаблону «pimp my library» , где функциональность класса может быть расширена за счет добавления функциональности из других классов, но [важно] без изменения исходного класса. Это означает, что исходный класс может быть возвращен из вызова, даже если он может быть использован под видом другого класса. Кроме того, хотя Scala поддерживает неявное преобразование типов (наиболее явно видимое через границы представления), он все еще является строго типизированным языком (т. Е. Возможны только явно определенные неявные преобразования, компилятор не пытается автоматически выводить преобразования и использовать их). , что может привести к непредсказуемым последствиям во время выполнения).

Делая это особенным …

В качестве краткого резюме, мы упомянули, что Scala является языком со статической типизацией, и представили некоторые плюсы и минусы его существования. Несмотря на то, что Scala статически и строго типизирован, мощный вывод типов, структурная типизация и неявные преобразования типов обеспечивают большую гибкость и устраняют множество ненужного стандартного кода. Кроме того, поскольку Scala является преимущественно языком и компилятором (т. Е. Обесценивает потрясающее сообщество Scala, библиотеки и фреймворки, которые также доступны), мы увидели, что одним из главных преимуществ с точки зрения пользователей является возросшая возможность программирования для правильность обеспечивается способностью противопоставлять, связывать и расширять типы. До сих пор мы фокусировались на ограничениях типов посредством аннотаций отклонений для параметризованных типов и просмотра границ для методов. Начиная с версии Scala 2.8 параметризованным типам были предоставлены еще более постоянные возможности с помощью обобщенных классов ограничений типов . Эти классы обеспечивают дополнительную специализацию в методах и дополняют границы контекста следующим образом:

A =: = B утверждает, что A и B должны быть равны
A <: <B утверждает, что A должен быть подтипом B

Пример использования этих классов будет состоять в том, чтобы включить специализацию для добавления числовых элементов в коллекцию, или для индивидуального форматирования печати, или для учета индивидуальных расчетов ответственности по конкретным ставкам или типам фондов в портфеле трейдеров. Например:
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
case class PrintFormatter[T](item : T) {
 def formatString(implicit evidence: T =:= String) = { // Will only work for String PrintFormatters
     println("STRING specialised printformatting...")
 }
    def formatPrimitive(implicit evidence: T <:< AnyVal) = { // Will only work for Primitive PrintFormatters
  println("WRAPPED PRIMITIVE specialised printformatting...")
 }
}
 
val stringPrintFormatter = PrintFormatter("String to format...")
stringPrintFormatter formatString
// stringPrintFormatter formatPrimitive  // Will not compile due to type mismatch
 
val intPrintFormatter = PrintFormatter(123)
intPrintFormatter formatPrimitive
//  intPrintFormatter formatString   // Will not compile due to type mismatch

Дом !

Наконец, и в качестве примера для несколько надуманного примера, стоит отметить, что Scala поддерживает экзистенциальные типы в первую очередь как средство лучшей интеграции с Java (как для универсальных, так и для поддержки примитивных типов). В попытке компенсировать стирание типов, реализованное в поддержке обобщений Java, Scala включает функцию под названием «Манифесты», чтобы (эффективно) вести учет классов, используемых в используемых параметризованных типах. Итак, давайте закончим с расширенным примером, использующим константы реификации и специализации на основе Manifest (псевдо, то есть производные от компилятора) на нескольких вспомогательных методах, чтобы показать некоторые приемы системы типов Scala и поддержку в действии.
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
class ReifiedManifest[T <: Any : Manifest](value: T) {
 
  val m = manifest[T]   // So at this point we have the manifest for the Parameterized type
                        // At which point we could either do an if() expression on what type is contained in our manifest
 
  if (m equals manifest[String]) {       
    println("The manifest contains a String")
  } else if (m <:< manifest[AnyVal]) {        // A subtype check using the <:< operation on the Manifest trait
    println("The manifest contains a subtype of AnyVal")
  } else if (m <:< manifest[AnyRef]) { 
    println("The manifest contains a subtype of AnyRef")
  } else {
    println("Not sure what type is contained ?")
  }
   
  // or we could grab the erased type from the manifest and do a match on some attribute of the type             
   
  m.erasure.toString match {
    case "class java.lang.String" => println("ERASURE: pattern matches on a String")
    case "double" | "int"  => println("ERASURE: pattern matches on a Numeric value.")
    case x => println("ERASURE: has picked up another type not spec'd in the pattern match: " + x)
    }
}
 
new ReifiedManifest("Test")                     // Contains a String  / matches on a String
new ReifiedManifest(1)                          // Contains an AnyVal / matches on a Numeric
new ReifiedManifest(1.2)                        // Contains an AnyVal / matches on a Numeric
new ReifiedManifest(BigDecimal("3.147"))        // Contains an AnyRef / matches on a an unspecified type

Последнее слово

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

связи

Ссылка: ускоренный курс по типам Scala от нашего партнера по JCG Кингсли Дэвиса в блоге Scalabound .