Scala3의 유형 클래스: 초보자 가이드 | 원장

Scala3의 유형 클래스: 초보자 가이드 | 원장

소스 노드 : 3028113

이 문서는 이미 Scala 산문에 정통하지만 'implicits` 및 코드의 매개변수화된 특성.

이 문서는 왜, 어떻게, 어디서, 언제를 설명합니다. 유형 클래스(TC).

이 문서를 읽은 후 초보 Scala3 개발자는 ScalaXNUMX의 소스 코드를 사용하고 자세히 알아볼 수 있는 탄탄한 지식을 얻게 됩니다. 많이 Scala 라이브러리를 살펴보고 관용적인 Scala 코드 작성을 시작하세요.

이유부터 시작해보자…

표현의 문제

1998년에 필립 와들러(Philip Wadler)는 다음과 같이 말했습니다. “표현 문제는 오래된 문제의 새로운 이름이다”. 소프트웨어 확장성의 문제입니다. Wadler 씨의 글에 따르면 표현 문제에 대한 해결책은 다음 규칙을 준수해야 합니다.

  • 규칙 1: 구현을 허용합니다. 기존 행동 (스칼라 특성을 생각해보세요) 적용 대상 새로운 표현 (케이스 클래스를 생각해 보세요)
  • 규칙 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행 및 12행). `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행)가 발생하거나 유형 기반 스위치가 발생합니다. 

이 규칙 n°2는 까다롭습니다. 우리는 다형성에 대한 우리 자신의 정의와 '확장' 트릭을 사용하여 이를 구현하려고 했습니다. 그리고 그것은 이상했습니다.

라는 누락된 부분이 있습니다. 임시 다형성: 동작과 유형이 정의된 위치에 관계없이 유형에 따라 동작 구현을 안전하게 전달하는 기능입니다. 들어가다 유형 클래스 패턴입니다.

유형 클래스 패턴

Type Class(짧게 TC) 패턴 레시피에는 3단계가 있습니다. 

  1. 새로운 행동 정의
  2. 동작 구현
  3. 동작을 사용하세요

다음 섹션에서는 가장 간단한 방법으로 TC 패턴을 구현합니다. 장황하고 투박하며 비실용적입니다. 하지만 잠깐만요, 이러한 주의 사항은 문서에서 단계별로 수정될 것입니다.

1. 새로운 행동 정의
스칼라

object Lib2:
  import Lib1.*

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

`Lib1'는 다시 한 번 손대지 않은 채로 남아 있습니다.

새로운 행동 is TC는 특성에 의해 구체화됩니다. 특성에 정의된 함수는 해당 동작의 일부 측면을 적용하는 방법입니다.

매개변수 `A`는 동작을 적용하려는 유형을 나타내며 `의 하위 유형입니다.Blockchain` 우리의 경우.

일부 비고 :

  • 필요한 경우 매개변수화된 유형 `A`는 Scala 유형 시스템에 의해 추가로 제한될 수 있습니다. 예를 들어 `A`가 되다Blockchain`. 
  • 또한 TC에는 더 많은 기능이 선언되어 있을 수 있습니다.
  • 마지막으로, 각 함수에는 더 많은 임의 매개변수가 있을 수 있습니다.

하지만 가독성을 위해 간단하게 설명하겠습니다.

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()`` 성명.

실용적인 TC 사용을 위한 몇 가지 Scala 기능을 강조해 보겠습니다. 

향상된 개발자 경험

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 구현은 자주 발생합니다. 데이터 지향 매퍼. 비즈니스 로직이 필요하지 않고 작성하기 지루하며 사례 클래스와 동기화를 유지해야 하는 부담이 있습니다.

그러한 상황에서 해당 라이브러리는 다음을 제공합니다. 자동적으로 파생 또는 반자동 유도. 예를 들어 Circe를 참조하십시오. 자동적으로 반자동 유도. 반자동 파생을 사용하면 프로그래머는 몇 가지 사소한 구문을 사용하여 유형 클래스의 인스턴스를 선언할 수 있는 반면, 자동 파생은 가져오기를 제외하고 코드 수정이 필요하지 않습니다.

내부적으로는 컴파일 타임에 일반 매크로가 검사됩니다. 유형 순수 데이터 구조로 사용하고 도서관 사용자를 위한 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번째 줄 새로운 TC `Named'를 소개합니다. 이 TC는 엄밀히 말하면 블록체인 사업과 관련이 없습니다. 그 목적은 케이스 클래스의 이름을 기반으로 블록체인의 이름을 지정하는 것입니다.

먼저 정의 라인 36-38에 집중하세요. TC를 파생하는 데는 두 가지 구문이 있습니다.

  1. 36행에서 TC 인스턴스는 `를 사용하여 케이스 클래스에서 직접 정의할 수 있습니다.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` 거울(케이스 클래스는 제품입니다). 27행, 프로그래머가 `가 아닌 것을 파생시키려고 하면Product`, 코드가 컴파일되지 않습니다.
  • `Mirror`에는 다른 유형이 포함되어 있습니다. 그 중 하나는 `MirrorLabel`, 유형 이름이 포함된 문자열입니다. 이 값은 `의 29행 구현에서 사용됩니다.Named`TC.

TC 작성자는 메타 프로그래밍을 사용하여 특정 유형의 TC 인스턴스를 일반적으로 생성하는 함수를 제공할 수 있습니다. 프로그래머는 전용 라이브러리 API 또는 Scala 파생 도구를 사용하여 코드에 대한 인스턴스를 생성할 수 있습니다.

TC를 구현하기 위해 일반 코드가 필요하든 특정 코드가 필요하든 각 상황에 맞는 솔루션이 있습니다. 

모든 혜택 요약

  • 표현 문제를 해결합니다.
    • 새로운 유형은 전통적인 특성 상속을 통해 기존 동작을 구현할 수 있습니다.
    • 기존 유형에 새로운 동작을 구현할 수 있습니다.
  • 관심의 분리
    • 코드는 훼손되지 않으며 쉽게 삭제할 수 있습니다. TC는 데이터와 동작을 분리하는데, 이는 함수형 프로그래밍의 모토입니다.
  • 안전합니다
    • 자체 조사에 의존하지 않기 때문에 유형이 안전합니다. 유형과 관련된 대규모 패턴 일치를 방지합니다. 이러한 코드를 작성하는 경우 TC 패턴이 완벽하게 적합한 경우를 발견할 수 있습니다.
    • 암시적 메커니즘은 컴파일에 안전합니다! 컴파일 타임에 인스턴스가 누락되면 코드가 컴파일되지 않습니다. 런타임에는 놀랄 일이 아닙니다.
  • 임시 다형성을 가져옵니다.
    • 임시 다형성은 일반적으로 전통적인 객체 지향 프로그래밍에서 누락됩니다.
    • 임시 다형성을 통해 개발자는 기존 하위 유형 지정(코드 결합)을 사용하지 않고도 관련되지 않은 다양한 유형에 대해 동일한 동작을 구현할 수 있습니다.
  • 의존성 주입이 쉬워졌습니다
    • TC 인스턴스는 Liskov 대체 원칙에 따라 변경될 수 있습니다. 
    • 구성 요소가 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, …)에 대한 대수 알고리즘의 재사용을 허용할 뿐만 아니라 사용자 정의 유형(`ComplexNumber` 예를 들어).

반면, 스칼라 컬렉션 구현에서는 대부분 유형 클래스 대신 하위 유형 지정을 사용합니다. 이 디자인은 다음과 같은 여러 가지 이유로 의미가 있습니다.

  • 컬렉션 API는 완전하고 안정적이어야 합니다. 구현에 의해 상속된 특성을 통해 일반적인 동작을 노출합니다. 확장성이 높다는 것은 여기서 특별한 목표가 아닙니다.
  • 사용이 간편해야 합니다. TC는 최종 사용자 프로그래머에게 정신적 오버헤드를 추가합니다.
  • TC는 성능 면에서 약간의 오버헤드를 초래할 수도 있습니다. 이는 컬렉션 API에 매우 중요할 수 있습니다.
  • 하지만 컬렉션 API는 타사 라이브러리에서 정의한 새로운 TC를 통해 계속 확장 가능합니다.

결론

우리는 TC가 큰 문제를 해결하는 간단한 패턴이라는 것을 보았습니다. Scala의 풍부한 구문 덕분에 TC 패턴을 다양한 방법으로 구현하고 사용할 수 있습니다. TC 패턴은 함수형 프로그래밍 패러다임과 일치하며 클린 아키텍처를 위한 훌륭한 도구입니다. 만능은 없으며 TC 패턴은 맞을 때 적용해야 합니다.

이 문서를 읽으면서 지식을 얻었기를 바랍니다. 

코드는 다음에서 확인할 수 있습니다. https://github.com/jprudent/type-class-article. 질문이나 의견이 있으면 저에게 연락해 주세요. 원하는 경우 저장소의 이슈나 코드 주석을 사용할 수 있습니다.


제롬 프루덴트

소프트웨어 엔지니어

타임 스탬프 :

더보기 원장