Ресурси програмування типу Scala


102

Відповідно до цього питання , система типу Scala - Тьюрінг завершена . Які ресурси доступні, що дають можливість новачкові скористатися потужністю програмування на рівні типу?

Ось ресурси, які я знайшов поки що:

Ці ресурси чудові, але я відчуваю, що мені не вистачає основ, і тому я не маю міцної основи, на якій можна будувати. Наприклад, де є вступ до визначень типів? Які операції я можу виконувати на типах?

Чи є якісь вступні ресурси?


Особисто я вважаю припущення, що той, хто хоче займатися програмуванням на рівні в Scala, вже знає, як робити програмування в Scala цілком розумним. Навіть якщо це означає, що я не розумію жодного слова з тих статей, з якими ви пов’язані :-)
Jörg W Mittag

Відповіді:


140

Огляд

Програмування на рівні типу має багато подібності з традиційним програмуванням на рівні цінності. Однак, на відміну від програмування на рівні значень, де обчислення відбуваються під час виконання, у програмуванні на рівні типу, обчислення відбуваються під час компіляції. Я спробую провести паралелі між програмуванням на рівні значення та програмуванням на рівні типу.

Парадигми

У програмуванні на рівні типу є дві основні парадигми: "об'єктно-орієнтована" та "функціональна". Більшість прикладів, пов'язаних звідси, випливають з об'єктно-орієнтованої парадигми.

Хороший, досить простий приклад програмування на рівні типу в об'єктно-орієнтованій парадигмі можна знайти в реалізації апокаліпсом зчитування лямбда , що повторюється тут:

// 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-level: 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)
    • type-level: трапляється в компіляторі за допомогою перевірки типу (тобто немає помилок компілятора):
      • по суті є типовим порівнянням: напр 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, і тому кондиціонування типу об'єкта (наприклад, через заяву відповідності) малоефективне.

Хитрість полягає у використанні неявних функцій та значень. Базовий випадок, як правило, є неявним значенням, а рекурсивний випадок зазвичай є неявною функцією. Дійсно, програмування на рівні типу широко використовує імпліцити.

Розглянемо цей приклад ( взято з метаскали та апокаліпсу ):

sealed trait Nat
sealed trait _0 extends Nat
sealed trait Succ[N <: Nat] extends Nat

Тут у вас є піано-кодування натуральних чисел. Тобто у вас є тип для кожного невід’ємного цілого числа: спеціальний тип для 0, а саме _0; і кожне ціле число, що перевищує нуль, має тип форми Succ[A], де Aтип, що представляє менше ціле число. Наприклад, тип, що представляє 2, буде: Succ[Succ[_0]](наступник застосовується двічі до типу, що представляє нуль).

Ми можемо псевдоніми різних натуральних чисел для більш зручного ознайомлення. Приклад:

type _3 = Succ[Succ[Succ[_0]]]

(Це дуже схоже на визначення значення a valдля результату функції.)

Тепер, припустимо, ми хочемо визначити функцію рівня значення, def toInt[T <: Nat](v : T)яка приймає значення аргументу v, яка відповідає Natі повертає ціле число, що представляє натуральне число, закодоване у vтип. Наприклад, якщо у нас є значення val x:_3 = null( nulltype 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щоб ви хочете перевірити, вони рівні. Потім перевірте, чи складається наступне:

Крім того, ви можете перетворити тип у значення (як показано вище) і зробити перевірку значень часу виконання. Наприклад assert(toInt(a) == toInt(b)), де aє тип Aі bє тип B.

Додаткові ресурси

Повний набір доступних конструкцій можна знайти в розділі про типи довідкового посібника зі шкалою (pdf) .

Adriaan Moors має декілька наукових праць про конструктори типів та пов'язані з ними теми із прикладами шкали:

Apocalisp - це блог з багатьма прикладами програмування на рівні в масштабі.

  • Програмування на рівні в Scala - це фантастична екскурсія по деяких програмах на рівні типу, що включає булеви, натуральні числа (як вище), двійкові числа, гетерогенні списки тощо.
  • Більше Scala Typehackery - це реалізація обчислення лямбда вище.

ScalaZ - це дуже активний проект, який забезпечує функціональність, яка розширює API Scala, використовуючи різні функції програмування на рівні типу. Це дуже цікавий проект, який має великі результати.

MetaScala - це бібліотека на рівні типу Scala, включаючи метатипи для натуральних чисел, булевих одиниць, одиниць, HList тощо. Це проект Jesper Nordenberg (його блог) .

У Michid (блозі) є кілька приголомшливих прикладів програмування на рівні в Scala (з іншого відповіді):

У Debasish Ghosh (блозі) також є відповідні публікації:

(Я робив деякі дослідження з цього приводу, і ось що я дізнався. Я все ще новачок у ньому, тому, будь ласка, вкажіть будь-які неточності у цій відповіді.)


12

Просто хотів сказати подяку за цікавий блог; Я спостерігав за цим деякий час, і особливо останній пост, згаданий вище, загострив моє розуміння важливих властивостей, якими повинна володіти система типу для об'єктно-орієнтованої мови. Отже, дякую!
Зак Сніг



Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.