Класи типів у Scala3: посібник для початківців | Леджер

Класи типів у Scala3: посібник для початківців | Леджер

Вихідний вузол: 3028113

Цей документ призначений для початківців розробників Scala3, які вже знайомі з прозою Scala, але спантеличені всіма `implicits` і параметризовані характеристики в коді.

Цей документ пояснює, чому, як, де і коли Класи типів (TC).

Прочитавши цей документ, розробник-початківець Scala3 отримає міцні знання для використання та зануриться у вихідний код багато бібліотек Scala та почніть писати ідіоматичний код Scala.

Почнемо з того, чому…

Проблема виразу

У 1998, – заявив Філіп Уодлер що «проблема виразу — це нова назва для старої проблеми». Це проблема розширюваності програмного забезпечення. Згідно з писанням містера Вадлера, розв'язок задачі виразу повинен відповідати наступним правилам:

  • Правило 1: дозволити реалізацію існуюча поведінка (подумайте про ознаку Scala), до якої потрібно застосувати нові уявлення (подумайте про клас case )
  • Правило 2:  Дозволити впровадження нова поведінка бути застосованим до існуючі представництва
  • Правило 3: Це не повинно загрожувати безпеки типу
  • Правило 4: не потрібно перекомпілювати існуючий код

Вирішення цієї проблеми буде срібною ниткою цієї статті.

Правило 1: реалізація існуючої поведінки на новому представленні

Будь-яка об’єктно-орієнтована мова має вбудоване рішення для правила 1 підтиповий поліморфізм. Ви можете сміливо реалізувати будь-який `trait` визначений у залежності від `class` у вашому власному коді, без перекомпіляції залежності. Давайте подивимося на це в дії:

масштаб

def todo = 42
type Height = Int
type Block = Int

object Lib1:
  trait Blockchain:
    def getBlock(height: Height): Block

  case class Ethereum() extends Blockchain:
    override def getBlock(height: Height) = todo

  case class Bitcoin() extends Blockchain:
    override def getBlock(height: Height) = todo


object Lib2:
  import Lib1.*

  case class Polkadot() extends Blockchain:
    override def getBlock(height: Height): Block = todo

val eth = Lib1.Ethereum()
val btc = Lib1.Bitcoin()
val dot = Lib2.Polkadot()

У цьому фіктивному прикладі бібліотека `Lib1` (рядок 5) визначає рису `Blockchain` (рядок 6) із двома його реалізаціями (рядки 2 і 9). `Lib1` залишиться незмінним у ВСІМ цьому документі (застосування правила 4).

`Lib2` (рядок 15) реалізує існуючу поведінку `Blockchain`на новому класі`Polkadot` (правило 1) безпечним (правило 3) способом, без перекомпіляції `Lib1` (правило 4). 

Правило 2: реалізація нової поведінки, яка буде застосована до існуючих представлень

Давайте уявимо в `Lib2«ми хочемо нової поведінки».lastBlock` буде реалізовано спеціально для кожного `Blockchain`.

Перше, що спадає на думку, це створити великий перемикач на основі типу параметра.

масштаб

def todo = 42
type Height = Int
type Block = Int

object Lib1:
  trait Blockchain:
    def getBlock(height: Height): Block

  case class Ethereum() extends Blockchain:
    override def getBlock(height: Height) = todo

  case class Bitcoin() extends Blockchain:
    override def getBlock(height: Height) = todo

object Lib2:
  import Lib1.*

  case class Polkadot() extends Blockchain:
    override def getBlock(height: Height): Block = todo

  def lastBlock(blockchain: Blockchain): Block = blockchain match
      case _:Ethereum => todo
      case _:Bitcoin  => todo
      case _:Polkadot => todo
  

object Lib3:
  import Lib1.*

  case class Polygon() extends Blockchain:
    override def getBlock(height: Height): Block = todo

import Lib1.*, Lib2.*, Lib3.*
println(lastBlock(Bitcoin()))
println(lastBlock(Ethereum()))
println(lastBlock(Polkadot()))
println(lastBlock(Polygon()))

Це рішення є слабкою повторною реалізацією поліморфізму на основі типів, який уже вбудований у мову!

`Lib1` залишається недоторканим (пам’ятайте, правило 4 застосоване у всьому цьому документі). 

Рішення, реалізоване в `Lib2` є гаразд поки ще один блокчейн не буде представлено в `Lib3`. Це порушує правило безпеки типу (правило 3), оскільки цей код не працює під час виконання в рядку 37. І зміна `Lib2` порушує правило 4.

Іншим рішенням є використання `extension`.

масштаб

def todo = 42
type Height = Int
type Block = Int

object Lib1:
  trait Blockchain:
    def getBlock(height: Height): Block

  case class Ethereum() extends Blockchain:
    override def getBlock(height: Height) = todo

  case class Bitcoin() extends Blockchain:
    override def getBlock(height: Height) = todo

object Lib2:
  import Lib1.*

  case class Polkadot() extends Blockchain:
    override def getBlock(height: Height): Block = todo

    def lastBlock(): Block = todo

  extension (eth: Ethereum) def lastBlock(): Block = todo

  extension (btc: Bitcoin) def lastBlock(): Block = todo

import Lib1.*, Lib2.*
println(Bitcoin().lastBlock())
println(Ethereum().lastBlock())
println(Polkadot().lastBlock())

def polymorphic(blockchain: Blockchain) =
  // blockchain.lastBlock()
  ???

`Lib1` залишається без змін (застосування правила 4 у всьому документі). 

`Lib2` визначає поведінку для свого типу (рядок 21) і `розширення` для існуючих типів (рядки 23 і 25).

Рядки 28-30, нову поведінку можна використовувати в кожному класі. 

Але неможливо назвати цю нову поведінку поліморфною (рядок 32). Будь-яка спроба зробити це призводить до помилок компіляції (рядок 33) або до перемикачів на основі типу. 

Це Правило № 2 є складним. Ми спробували реалізувати це за допомогою нашого власного визначення поліморфізму та трюку «розширення». І це було дивно.

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

Шаблон класу типу

Рецепт шаблону типу класу (скорочено TC) складається з 3 кроків. 

  1. Визначте нову поведінку
  2. Реалізуйте поведінку
  3. Використовуйте поведінку

У наступному розділі я реалізую шаблон TC найпростішим способом. Це багатослівно, незграбно і непрактично. Але зачекайте, ці застереження будуть виправлені крок за кроком далі в документі.

1. Визначте нову поведінку
масштаб

object Lib2:
  import Lib1.*

  trait LastBlock[A]:
    def lastBlock(instance: A): Block

`Lib1` знову залишається недоторканим.

Нова поведінка is ТК матеріалізується ознакою. Функції, визначені в ознакі, є способом застосування деяких аспектів цієї поведінки.

Параметр `A` представляє тип, до якого ми хочемо застосувати поведінку, які є підтипами `Blockchain` в нашому випадку.

Деякі зауваження:

  • Якщо потрібно, параметризований тип `A` може додатково обмежуватися системою типу Scala. Наприклад, ми могли б застосувати `A` бути `Blockchain`. 
  • Крім того, у ТК може бути задекларовано набагато більше функцій.
  • Нарешті, кожна функція може мати набагато більше довільних параметрів.

Але давайте зробимо все просто для зручності читання.

2. Реалізація поведінки
масштаб

object Lib2:
  import Lib1.*

  trait LastBlock[A]:
    def lastBlock(instance: A): Block

  val ethereumLastBlock = new LastBlock[Ethereum]:
    def lastBlock(eth: Ethereum) = eth.lastBlock

  val bitcoinLastBlock = new LastBlock[Bitcoin]:
    def lastBlock(btc: Bitcoin) = http("http://bitcoin/last")

Для кожного типу новий `LastBlock` поведінка очікувана, є конкретний випадок такої поведінки. 

Файл `Ethereum` рядок реалізації 22 обчислюється з `eth` екземпляр, переданий як параметр. 

Реалізація `LastBlock` для `Bitcoin` рядок 25 реалізовано з некерованим IO і не використовує його параметр.

Отже, `Lib2` реалізує нову поведінку `LastBlock` для `Lib1` класи.

3. Використовуйте поведінку
масштаб

object Lib2:
  import Lib1.*

  trait LastBlock[A]:
    def lastBlock(instance: A): Block

  val ethereumLastBlock = new LastBlock[Ethereum]:
    def lastBlock(eth: Ethereum) = eth.lastBlock

  val bitcoinLastBlock = new LastBlock[Bitcoin]:
    def lastBlock(btc: Bitcoin) = http("http://bitcoin/last")

import Lib1.*, Lib2.*

def useLastBlock[A](instance: A, behavior: LastBlock[A]) =
  behavior.lastBlock(instance)

println(useLastBlock(Ethereum(lastBlock = 2), ethereumLastBlock))
println(useLastBlock(Bitcoin(), bitcoinLastBlock))

Рядок 30`useLastBlock` використовує екземпляр `A` і `LastBlock` поведінка, визначена для цього екземпляра.

Рядок 33`useLastBlock` викликається з екземпляром `Ethereum` і реалізація `LastBlock` визначено в `Lib2`. Зверніть увагу, що можна передати будь-яку альтернативну реалізацію `LastBlock[A]` (подумай ін'єкційна залежність).

`useLastBlock` є сполучною ланкою між представленням (фактичним A) і його поведінкою. Дані та поведінка розділені, за що виступає функціональне програмування.

Обговорення

Давайте повторимо правила задачі виразу:

  • Правило 1: дозволити реалізацію існуюча поведінка  бути застосованим до нові класи
  • Правило 2:  Дозволити впровадження нова поведінка бути застосованим до існуючі класи
  • Правило 3: Це не повинно загрожувати безпеки типу
  • Правило 4: не потрібно перекомпілювати існуючий код

Правило 1 може бути вирішено з коробки за допомогою поліморфізму підтипу.

Щойно представлений шаблон TC (див. попередній знімок екрана) вирішує правило 2. Він безпечний для типу (правило 3), і ми ніколи не торкалися `Lib1` (правило 4). 

Однак використовувати його недоцільно з кількох причин:

  • У рядках 33-34 ми повинні явно передати поведінку в його екземпляр. Це додаткові накладні витрати. Ми повинні просто написати `useLastBlock(Bitcoin())`.
  • Рядок 31 синтаксис незвичайний. Ми б віддали перевагу написати стислий і більш об’єктно-орієнтований  `instance.lastBlock()` заява.

Давайте виділимо деякі функції Scala для практичного використання TC. 

Покращений досвід розробника

Scala має унікальний набір функцій і синтаксичних цукрів, які роблять TC справді приємним досвідом для розробників.

Імпліцитні

Неявна область — це спеціальна область, яка вирішується під час компіляції, де може існувати лише один екземпляр даного типу. 

Програма поміщає екземпляр у неявну область за допомогою `given` ключове слово. Крім того, програма може отримати екземпляр із неявної області за допомогою ключового слова `using`.

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

Він відрізняється від глобального масштабу, оскільки: 

  1. Це вирішується контекстно. Два розташування програми можуть використовувати екземпляр того самого заданого типу в неявній області видимості, але ці два екземпляри можуть бути різними.
  2. За сценою код передає функцію неявних аргументів для функціонування, доки не буде досягнуто неявне використання. Він не використовує глобальний простір пам’яті.

Повертаючись до класу типу! Візьмемо точно такий же приклад.

масштаб

def todo = 42
type Height = Int
type Block = Int
def http(uri: String): Block = todo

object Lib1:
  trait Blockchain:
    def getBlock(height: Height): Block

  case class Ethereum() extends Blockchain:
    override def getBlock(height: Height) = todo

  case class Bitcoin() extends Blockchain:
    override def getBlock(height: Height) = todo

`Lib1` це той самий немодифікований код, який ми визначили раніше. 

масштаб

object Lib2:
  import Lib1.*

  trait LastBlock[A]:
    def lastBlock(instance: A): Block

  given ethereumLastBlock:LastBlock[Ethereum] = new LastBlock[Ethereum]:
    def lastBlock(eth: Ethereum) = eth.lastBlock

  given bitcoinLastBlock:LastBlock[Bitcoin] = new LastBlock[Bitcoin]:
    def lastBlock(btc: Bitcoin) = http("http://bitcoin/last")

import Lib1.*, Lib2.*

def useLastBlock[A](instance: A)(using behavior: LastBlock[A]) =
  behavior.lastBlock(instance)

println(useLastBlock(Ethereum(lastBlock = 2)))
println(useLastBlock(Bitcoin()))

Рядок 19 нова поведінка `LastBlock` визначається так само, як ми це робили раніше.

Рядок 22 і рядок 25, `val` замінюється на `given`. Обидві реалізації `LastBlock` поміщаються в неявну область видимості.

Рядок 31`useLastBlock` декларує поведінку `LastBlock` як неявний параметр. Компілятор розпізнає відповідний екземпляр `LastBlock` з неявної області, контекстуалізованої з місць виклику (рядки 33 і 34). Рядок 28 імпортує все з `Lib2`, включаючи неявну область. Отже, компілятор передає екземпляри, визначені рядки 22 і 25, як останній параметр `useLastBlock`. 

Як користувач бібліотеки, використовувати клас типу легше, ніж раніше. Рядки 34 і 35 розробник має лише переконатися, що екземпляр поведінки введено в неявну область (і це може бути просто `import`). Якщо неявний не є `given` де знаходиться код `using` це, каже йому компілятор.

Неявне використання Scala полегшує завдання передачі екземплярів класів разом із екземплярами їхньої поведінки.

Неявні цукру

Рядки 22 і 25 попереднього коду можна ще покращити! Давайте повторимо реалізацію TC.

масштаб

given LastBlock[Ethereum] = new LastBlock[Ethereum]:
    def lastBlock(eth: Ethereum) = eth.lastBlock

  given LastBlock[Bitcoin] = new LastBlock[Bitcoin]:
    def lastBlock(btc: Bitcoin) = http("http://bitcoin/last")

Рядки 22 і 25, якщо ім’я екземпляра не використовується, його можна опустити.

масштаб


  given LastBlock[Ethereum] with
    def lastBlock(eth: Ethereum) = eth.lastBlock

  given LastBlock[Bitcoin] with
    def lastBlock(btc: Bitcoin) = http("http://bitcoin/last")

У рядках 22 і 25 повторення типу можна замінити на `with` ключове слово.

масштаб

given LastBlock[Ethereum] = _.lastBlock

  given LastBlock[Bitcoin] = _ => http("http://bitcoin/last")

Оскільки ми використовуємо вироджену ознаку з однією функцією, IDE може запропонувати спростити код за допомогою виразу SAM. Хоча це правильно, я не думаю, що це належне використання SAM, якщо ви випадково не кодуєте гольф.

Scala пропонує синтаксичні цукри для оптимізації синтаксису, видалення непотрібних імен, декларацій і надмірності типів.

Розширення

Розумно використане `extension` механізм може спростити синтаксис для використання класу типу.

масштаб

object Lib2:
  import Lib1.*

  trait LastBlock[A]:
    def lastBlock(instance: A): Block

  given LastBlock[Ethereum] with
    def lastBlock(eth: Ethereum) = eth.lastBlock

  given LastBlock[Bitcoin] with
    def lastBlock(btc: Bitcoin) = http("http://bitcoin/last")

  extension[A](instance: A)
    def lastBlock(using tc: LastBlock[A]) = tc.lastBlock(instance)

import Lib1.*, Lib2.*

println(Ethereum(lastBlock = 2).lastBlock)
println(Bitcoin().lastBlock)

Рядки 28-29 загальний метод розширення `lastBlock` визначається для будь-якого `A` з `LastBlock` Параметр TC у неявній області.

У рядках 33-34 розширення використовує об’єктно-орієнтований синтаксис для використання TC.

масштаб

object Lib2:
  import Lib1.*

  trait LastBlock[A]:
    def lastBlock(instance: A): Block

  given LastBlock[Ethereum] with
    def lastBlock(eth: Ethereum) = eth.lastBlock

  given LastBlock[Bitcoin] with
    def lastBlock(btc: Bitcoin) = http("http://bitcoin/last")

  extension[A](instance: A)(using tc: LastBlock[A])
    def lastBlock = tc.lastBlock(instance)
    def penultimateBlock = tc.lastBlock(instance) - 1

import Lib1.*, Lib2.*

val eth = Ethereum(lastBlock = 2)
println(eth.lastBlock)
println(eth.penultimateBlock)

val btc = Bitcoin()
println(btc.lastBlock)
println(btc.penultimateBlock)

Рядок 28, параметр TC можна також визначити для всього розширення, щоб уникнути повторення. У рядку 30 ми повторно використовуємо TC у розширенні для визначення `penultimateBlock` (хоча це може бути реалізовано на `LastBlock` ознака безпосередньо)

Магія відбувається, коли використовується TC. Вираз виглядає набагато природнішим, створюючи ілюзію поведінки `lastBlock` поєднується з примірником.

Універсальний тип з TC
масштаб

import Lib1.*, Lib2.*

def useLastBlock1[A](instance: A)(using LastBlock[A]) = instance.lastBlock

def useLastBlock2[A: LastBlock](instance: A) = instance.lastBlock

val eth = Ethereum(lastBlock = 2)
assert(useLastBlock1(eth) == useLastBlock2(eth))

У рядку 34 функція використовує неявний TC. Зауважте, що TC не потрібно називати, якщо це ім’я непотрібне.

Шаблон TC настільки широко використовується, що існує загальний синтаксис типу для вираження «типу з неявною поведінкою». Синтаксис рядка 36 є більш короткою альтернативою попередньому (рядок 34). Він уникає оголошення конкретного безіменного неявного параметра TC.

На цьому розділ досвіду розробника завершено. Ми бачили, як розширення, неявні та деякі синтаксичні цукри можуть забезпечити менш захаращений синтаксис, коли TC використовується та визначається.

Автоматичне виведення

Багато бібліотек Scala використовують TC, залишаючи програмістам реалізувати їх у своїй базі коду.

Наприклад, Circe (бібліотека десеріалізації json) використовує TC `Encoder[T]` і `Decoder[T]` для впровадження програмістами у свою кодову базу. Після впровадження можна використовувати весь обсяг бібліотеки. 

Такі реалізації TC більш ніж часті картографи, орієнтовані на дані. Їм не потрібна жодна бізнес-логіка, їх нудно писати, і їх важко підтримувати в синхронізації з класами case.

У такій ситуації ті бібліотеки пропонують, що називається автоматичний виведення або напівавтоматичний виведення. Дивіться, наприклад, Цирцея автоматичний та напівавтоматичний виведення. За допомогою напівавтоматичного виведення програміст може оголосити екземпляр класу типу з деяким другорядним синтаксисом, тоді як автоматичне похідне не потребує будь-яких модифікацій коду, окрім імпорту.

Під капотом, під час компіляції, аналіз загальних макросів Типи як чисту структуру даних і генерувати TC[T] для користувачів бібліотеки. 

Виведення загального TC є дуже поширеним явищем, тому Scala представила повний інструментарій для цієї мети. Цей метод не завжди рекламується в бібліотечній документації, хоча це спосіб використання деривації у Scala 3.

масштаб

object GenericLib:

  trait Named[A]:
    def blockchainName(instance: A): String

  object Named:
    import scala.deriving.*

    inline final def derived[A](using inline m: Mirror.Of[A]): Named[A] =
      val nameOfType: String = inline m match
        case p: Mirror.ProductOf[A] => compiletime.constValue[p.MirroredLabel]
        case _ => compiletime.error("Not a product")
      new Named[A]:
        override def blockchainName(instance: A):String = nameOfType.toLowerCase

  extension[A] (instance: A)(using tc: Named[A])
    def blockchainName = tc.blockchainName(instance)

import Lib1.*, GenericLib.*

case class Polkadot() derives Named
given Named[Bitcoin] = Named.derived
given Named[Ethereum] = Named.derived

println(Ethereum(lastBlock = 2).blockchainName)
println(Bitcoin().blockchainName)
println(Polkadot().blockchainName)

Рядок 18 новий ТК `Named` вводиться. Цей TC не має відношення до блокчейн-бізнесу, строго кажучи. Його мета — назвати блокчейн на основі назви класу case.

Спочатку зосередьтеся на визначеннях рядків 36-38. Є 2 синтаксиси для отримання TC:

  1. У рядку 36 примірник TC можна визначити безпосередньо в класі case за допомогою `derives` ключове слово. Під капотом компілятор генерує заданий `Named` екземпляр у `Polkadot` супутній об'єкт.
  2. Рядки 37 і 38, екземпляри класів типів наведено на вже існуючих класах за допомогою `TC.derived

Рядок 31 визначає загальне розширення (див. попередні розділи) і `blockchainName` використовується природно.  

Файл `derives` ключове слово очікує метод із формою `inline def derived[T](using Mirror.Of[T]): TC[T] = ???`, яка визначена в рядку 24. Я не буду детально пояснювати, що робить код. У загальних рисах:

  • `inline def` визначає макрос
  • `Mirror` є частиною інструментарію для самоаналізу типів. Існують різні види дзеркал, і рядок 26 коду фокусується на `Product` дзеркала (клас case є продуктом). Рядок 27, якщо програмісти намагаються вивести щось, що не є `Product`, код не компілюється.
  • `Mirror` містить інші типи. Один із них, `MirrorLabel`, це рядок, який містить назву типу. Це значення використовується в реалізації, рядок 29, `Named` TC.

Автори TC можуть використовувати метапрограмування для надання функцій, які генерують екземпляри TC певного типу. Програмісти можуть використовувати API спеціальної бібліотеки або інструменти отримання Scala для створення екземплярів свого коду.

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

Короткий перелік усіх переваг

  • Це вирішує задачу виразу
    • Нові типи можуть реалізувати існуючу поведінку через успадкування традиційних ознак
    • Нова поведінка може бути реалізована на існуючих типах
  • Поділ занепокоєння
    • Код не зіпсований і його легко видалити. TC розділяє дані та поведінку, що є девізом функціонального програмування.
  • Це безпечно
    • Це безпечно для типу, оскільки не покладається на самоаналіз. Це дозволяє уникнути зіставлення великих шаблонів із залученням типів. якщо ви зіткнетеся з написанням такого коду, ви можете виявити випадок, коли шаблон TC підійде ідеально.
    • Неявний механізм є безпечним для компіляції! Якщо екземпляр відсутній під час компіляції, код не компілюється. Нічого несподіваного під час виконання.
  • Це приносить спеціальний поліморфізм
    • Спеціальний поліморфізм зазвичай відсутній у традиційному об'єктно-орієнтованому програмуванні.
    • За допомогою спеціального поліморфізму розробники можуть реалізувати однакову поведінку для різних непов’язаних типів без використання традиційного підтипу (який об’єднує код)
  • Ін’єкція залежностей стала легкою
    • Екземпляр ТК можна змінити відповідно до принципу підстановки Ліскова. 
    • Якщо компонент залежить від TC, імітований TC можна легко впровадити для цілей тестування. 

Протипоказання

Кожен молоток призначений для цілого ряду завдань.

Класи типу призначені для поведінкових проблем і не повинні використовуватися для успадкування даних. Для цього використовуйте склад.

Звичайний підтип більш простий. Якщо ви володієте базою коду і не прагнете розширюваності, класи типів можуть бути надмірними.

Наприклад, у ядрі Scala є `Numeric` тип класу:

масштаб

trait Numeric[T] extends Ordering[T] {
  def plus(x: T, y: T): T
  def minus(x: T, y: T): T
  def times(x: T, y: T): T

Використовувати такий клас типів справді має сенс, оскільки він дозволяє не лише повторно використовувати алгебраїчні алгоритми для типів, вбудованих у Scala (Int, BigInt, …), але й для типів, визначених користувачем (a `ComplexNumber` наприклад).

З іншого боку, реалізація колекцій Scala здебільшого використовує підтипування замість класу типу. Цей дизайн має сенс з кількох причин:

  • Передбачається, що API колекції буде повним і стабільним. Він розкриває загальну поведінку через ознаки, успадковані реалізаціями. Бути високорозширюваним тут не є особливою метою.
  • Він повинен бути простим у використанні. TC додає накладні витрати на програміста кінцевого користувача.
  • TC також може спричинити невеликі накладні витрати на продуктивність. Це може бути критично для API колекції.
  • Однак API колекції все ще можна розширити за допомогою нового TC, визначеного сторонніми бібліотеками.

Висновок

Ми побачили, що TC — це простий шаблон, який вирішує велику проблему. Завдяки розширеному синтаксису Scala шаблон TC можна реалізувати та використовувати різними способами. Шаблон TC відповідає парадигмі функціонального програмування та є чудовим інструментом для чистої архітектури. Немає срібної кулі, і шаблон TC потрібно застосовувати, коли він підходить.

Сподіваюся, ви отримали знання, прочитавши цей документ. 

Код доступний за адресою https://github.com/jprudent/type-class-article. Будь ласка, зв’яжіться зі мною, якщо у вас виникнуть запитання чи зауваження. Ви можете використовувати проблеми або коментарі до коду в репозиторії, якщо хочете.


Джером ПРУДЕНТ

Інженер-програміст

Часова мітка:

Більше від Гросбух