Ресурсы программирования типа Scala
Согласно этому вопросу, система типов Скалы является полной по Тьюрингу. Какие ресурсы доступны, что позволяет новичку воспользоваться возможностями программирования на уровне типов?
Вот ресурсы, которые я нашел до сих пор:
- Высшее волшебство Даниэля Спевака в земле Скалы
- Программирование на уровне типов Apocalisp в Scala
- Список Джеспера
Эти ресурсы великолепны, но я чувствую, что мне не хватает основ, и поэтому у меня нет прочной основы, на которой можно строить. Например, где есть введение в определения типов? Какие операции я могу выполнять над типами?
Есть ли хорошие вводные ресурсы?
5 ответов
обзор
Программирование на уровне типов имеет много общего с традиционным программированием на уровне значений. Однако, в отличие от программирования на уровне значений, где вычисления выполняются во время выполнения, в программировании на уровне типов вычисления выполняются во время компиляции. Я попытаюсь провести параллели между программированием на уровне значений и программированием на уровне типов.
парадигмы
В программировании на уровне типов есть две основные парадигмы: "объектно-ориентированная" и "функциональная". Большинство примеров, связанных с этим, следуют объектно-ориентированной парадигме.
Хороший, довольно простой пример программирования на уровне типов в объектно-ориентированной парадигме можно найти в реализации апокалипсиса лямбда-исчисления, воспроизведенной здесь:
// Abstract trait
trait Lambda {
type subst[U <: Lambda] <: Lambda
type apply[U <: Lambda] <: Lambda
type eval <: Lambda
}
// Implementations
trait App[S <: Lambda, T <: Lambda] extends Lambda {
type subst[U <: Lambda] = App[S#subst[U], T#subst[U]]
type apply[U] = Nothing
type eval = S#eval#apply[T]
}
trait Lam[T <: Lambda] extends Lambda {
type subst[U <: Lambda] = Lam[T]
type apply[U <: Lambda] = T#subst[U]#eval
type eval = Lam[T]
}
trait X extends Lambda {
type subst[U <: Lambda] = U
type apply[U] = Lambda
type eval = X
}
Как видно из примера, объектно-ориентированная парадигма для программирования на уровне типов происходит следующим образом:
- Первое: определите абстрактную черту с различными полями абстрактного типа (что такое абстрактное поле см. Ниже). Это шаблон для гарантии того, что поля определенных типов существуют во всех реализациях без принудительной реализации. В примере с лямбда-исчислением это соответствует
trait Lambda
это гарантирует, что существуют следующие типы:subst
,apply
, а такжеeval
, - Далее: определить подвыражения, которые расширяют абстрактную черту и реализуют различные поля абстрактного типа
- Часто эти субтитры будут параметризованы аргументами. В примере лямбда-исчисления подтипы
trait App extends Lambda
который параметризован двумя типами (S
а такжеT
оба должны быть подтипамиLambda
),trait Lam extends Lambda
параметризован с одним типом (T
), а такжеtrait X extends Lambda
(который не параметризован). - поля типа часто реализуются путем обращения к параметрам типа вычитания и иногда ссылки на их поля типа через оператор хеширования:
#
(что очень похоже на оператор точки:.
для ценностей). В чертуApp
примера лямбда-исчисления, типeval
реализован следующим образом:type eval = S#eval#apply[T]
, По сути это называетсяeval
тип параметра признакаS
и звонитapply
с параметромT
на результат. Заметка,S
гарантированно иметьeval
тип, потому что параметр указывает, что это подтипLambda
, Аналогичным образом, результатeval
должен иметьapply
тип, так как он указан как подтипLambda
, как указано в абстрактном признакеLambda
,
- Часто эти субтитры будут параметризованы аргументами. В примере лямбда-исчисления подтипы
Функциональная парадигма состоит из определения множества параметризованных конструкторов типов, которые не сгруппированы по признакам.
Сравнение программирования на уровне значений и программирования на уровне типов
- абстрактный класс
- Значение уровня:
abstract class C { val x }
- уровень типа:
trait C { type X }
- Значение уровня:
- зависимые от пути типы
C.x
(ссылка на значение поля / функцию x в объекте C)C#x
(ссылка на тип поля x в признаке C)
- подпись функции (без реализации)
- Значение уровня:
def f(x:X) : Y
- уровень типа:
type f[x <: X] <: Y
(это называется "конструктор типов" и обычно встречается в абстрактной черте)
- Значение уровня:
- реализация функции
- Значение уровня:
def f(x:X) : Y = x
- уровень типа:
type f[x <: X] = x
- Значение уровня:
- условными
- проверка равенства
- Значение уровня:
a:A == b:B
- уровень типа:
implicitly[A =:= B]
- уровень значения: происходит в JVM через модульное тестирование во время выполнения (т.е. без ошибок времени выполнения):
- в сущности это утверждение
assert(a == b)
- в сущности это утверждение
- Уровень типа: происходит в компиляторе через проверку типов (т.е. без ошибок компилятора):
- по сути это сравнение типов: например
implicitly[A =:= B]
A <:< B
, компилируется только еслиA
это подтипB
A =:= B
, компилируется только еслиA
это подтипB
а такжеB
это подтипA
A <%< B
, ("видимый как") компилируется, только еслиA
просматривается какB
(т.е. есть неявное преобразование изA
к подтипуB
)- пример
- больше операторов сравнения
- по сути это сравнение типов: например
- Значение уровня:
Преобразование между типами и значениями
Во многих примерах типы, определенные с помощью признаков, часто бывают как абстрактными, так и запечатанными, и поэтому не могут быть созданы ни непосредственно, ни через анонимный подкласс. Так что это часто используется
null
в качестве значения заполнителя при выполнении вычисления уровня значения с использованием некоторого типа интереса:- например
val x:A = null
, гдеA
это тип, который вам небезразличен
- например
Из-за стирания типов все параметризованные типы выглядят одинаково. Кроме того, (как упомянуто выше) значения, с которыми вы работаете, имеют тенденцию быть
null
и, следовательно, обусловливание типа объекта (например, с помощью оператора сопоставления) неэффективно.
Хитрость заключается в использовании неявных функций и значений. Базовый регистр обычно является неявным значением, а рекурсивный регистр обычно является неявной функцией. В самом деле, программирование на уровне типов активно использует последствия.
Рассмотрим этот пример ( взят из metascala и apocalisp):
sealed trait Nat
sealed trait _0 extends Nat
sealed trait Succ[N <: Nat] extends Nat
Здесь у вас есть peano кодирование натуральных чисел. То есть у вас есть тип для каждого неотрицательного целого числа: специальный тип для 0, а именно _0
; и каждое целое число больше нуля имеет тип вида Succ[A]
, где A
это тип, представляющий меньшее целое число. Например, тип, представляющий 2 будет: Succ[Succ[_0]]
(наследник применяется дважды к типу, представляющему ноль).
Мы можем псевдонимы различных натуральных чисел для более удобной ссылки. Пример:
type _3 = Succ[Succ[Succ[_0]]]
(Это очень похоже на определение val
быть результатом функции.)
Теперь предположим, что мы хотим определить функцию уровня значения def toInt[T <: Nat](v : T)
который принимает значение аргумента, v
, что соответствует Nat
и возвращает целое число, представляющее натуральное число, закодированное в v
тип. Например, если у нас есть значение val x:_3 = null
(null
типа Succ[Succ[Succ[_0]]]
) мы бы хотели toInt(x)
возвращать 3
,
Реализовать toInt
мы собираемся использовать следующий класс:
class TypeToValue[T, VT](value : VT) { def getValue() = value }
Как мы увидим ниже, будет объект, построенный из класса TypeToValue
для каждого Nat
от _0
до (например) _3
и каждый будет хранить представление значения соответствующего типа (т.е. TypeToValue[_0, Int]
будет хранить значение 0
, TypeToValue[Succ[_0], Int]
будет хранить значение 1
, так далее.). Заметка, TypeToValue
параметризован двумя типами: T
а также VT
, T
соответствует типу, которому мы пытаемся присвоить значения (в нашем примере Nat
) а также VT
соответствует типу значения, которое мы ему присваиваем (в нашем примере Int
).
Теперь мы сделаем следующие два неявных определения:
implicit val _0ToInt = new TypeToValue[_0, Int](0)
implicit def succToInt[P <: Nat](implicit v : TypeToValue[P, Int]) =
new TypeToValue[Succ[P], Int](1 + v.getValue())
И мы реализуем toInt
следующее:
def toInt[T <: Nat](v : T)(implicit ttv : TypeToValue[T, Int]) : Int = ttv.getValue()
Чтобы понять как toInt
работает, давайте рассмотрим, что он делает на пару входов:
val z:_0 = null
val y:Succ[_0] = null
Когда мы звоним toInt(z)
компилятор ищет неявный аргумент ttv
типа TypeToValue[_0, Int]
(поскольку z
имеет тип _0
). Находит объект _0ToInt
называет getValue
метод этого объекта и возвращается 0
, Важно отметить, что мы не указали программе, какой объект использовать, компилятор обнаружил это неявно.
Теперь давайте рассмотрим toInt(y)
, На этот раз компилятор ищет неявный аргумент ttv
типа TypeToValue[Succ[_0], Int]
(поскольку y
имеет тип Succ[_0]
). Находит функцию succToInt
, который может вернуть объект соответствующего типа (TypeToValue[Succ[_0], Int]
) и оценивает это. Эта функция сама принимает неявный аргумент (v
) типа TypeToValue[_0, Int]
(это TypeToValue
где первый параметр типа имеет на одно меньше Succ[_]
). Компилятор поставляет _0ToInt
(как это было сделано при оценке toInt(z)
выше) и succToInt
строит новый TypeToValue
объект со значением 1
, Опять же, важно отметить, что компилятор предоставляет все эти значения неявным образом, поскольку у нас нет доступа к ним явно.
Проверка вашей работы
Есть несколько способов проверить, что ваши вычисления на уровне типов делают то, что вы ожидаете. Вот несколько подходов. Сделать два типа A
а также B
, что вы хотите проверить, равны. Затем проверьте, что следующая компиляция:
Equal[A, B]
- с: черта
Equal[T1 >: T2 <: T2, T2]
( взято из apocolisp)
- с: черта
implicitly[A =:= B]
Кроме того, вы можете преобразовать тип в значение (как показано выше) и выполнить проверку значений во время выполнения. Например assert(toInt(a) == toInt(b))
, где a
имеет тип A
а также b
имеет тип B
,
Дополнительные ресурсы
Полный набор доступных конструкций можно найти в разделе типов справочного руководства по scala (pdf).
У Адриана Мурса есть несколько научных статей о конструкторах типов и связанных темах с примерами из scala:
- Дженерики высшего сорта (pdf)
- Полиморфизм конструкторов типов для Scala: теория и практика (pdf) (кандидатская диссертация, включающая предыдущую статью Моорса)
- Тип Конструктор Полиморфизм Вывод
Apocalisp - это блог со многими примерами программирования на уровне типов в scala.
- Программирование на уровне типов в Scala - это фантастическая экскурсия по программированию на уровне типов, который включает логические, натуральные числа (как указано выше), двоичные числа, гетерогенные списки и многое другое.
- Подробнее Scala Typehackery - это реализация лямбда-исчисления, описанная выше.
ScalaZ - очень активный проект, который предоставляет функциональные возможности, расширяющие Scala API с использованием различных функций программирования на уровне типов. Это очень интересный проект, у которого много поклонников.
MetaScala - это библиотека уровня типов для Scala, включающая мета-типы для натуральных чисел, логические значения, единицы измерения, HList и т. Д. Это проект Джеспера Норденберга (его блог).
Michid (блог) имеет несколько замечательных примеров программирования на уровне типов в Scala (из другого ответа):
- Мета-программирование с Scala Часть I: Дополнение
- Мета-программирование с Scala Часть II: Умножение
- Мета-программирование с помощью Scala Part III: применение частичной функции
- Мета-программирование в Scala: условная компиляция и развертывание цикла
- Scala кодирование уровня типа исчисления SKI
Debasish Ghosh (блог) также имеет несколько соответствующих сообщений:
- Абстракции высших порядков в скале
- Статическая типизация дает вам преимущество
- Скала подразумевает тип классов, вот и я
- Рефакторинг в scala type-классы
- Использование обобщенных ограничений типа
- Как скаляры набирают системные слова для тебя
- Выбор между членами абстрактного типа
(Я провел некоторое исследование по этому вопросу, и вот что я узнал. Я все еще новичок в этом, поэтому, пожалуйста, укажите на любые неточности в этом ответе.)
В дополнение к другим ссылкам здесь, есть также мои сообщения в блоге о метапрограммировании на уровне типа в Scala:
Как предложено в Твиттере: Shapeless: исследование универсального / политипического программирования в Scala Майлза Сабина.
- Sing, библиотека метапрограммирования на уровне типов в Scala.
- Начало программирования на уровне типов в Scala