이 질문 에 따르면 Scala의 유형 시스템은 Turing complete 입니다. 초보자가 유형 수준 프로그래밍의 장점을 활용할 수 있도록 어떤 리소스를 사용할 수 있습니까?
지금까지 찾은 리소스는 다음과 같습니다.
이러한 리소스는 훌륭하지만 기본이 누락 된 것 같아서 구축 할 탄탄한 기반이 없습니다. 예를 들어, 유형 정의에 대한 소개는 어디에 있습니까? 유형에 대해 어떤 작업을 수행 할 수 있습니까?
좋은 입문 자료가 있습니까?
답변
개요
유형 수준 프로그래밍은 기존의 가치 수준 프로그래밍과 많은 유사점이 있습니다. 그러나 런타임에 계산이 발생하는 값 수준 프로그래밍과 달리 유형 수준 프로그래밍에서는 컴파일 타임에 계산이 수행됩니다. 나는 가치 수준에서의 프로그래밍과 유형 수준에서의 프로그래밍 사이의 유사점을 그리려고 노력할 것이다.
패러다임
유형 수준 프로그래밍에는 “객체 지향”과 “기능적”의 두 가지 주요 패러다임이 있습니다. 여기에서 링크 된 대부분의 예제는 객체 지향 패러다임을 따릅니다.
객체 지향 패러다임에서 유형 수준 프로그래밍의 훌륭하고 매우 간단한 예는 apocalisp의 lambda calculus 구현 에서 찾을 수 있습니다 .
// 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
(매개 변수화되지 않음 . - 유형 필드는 종종 subtrait의 유형 매개 변수를 참조하고 때로는 해시 연산자를 통해 유형 필드를 참조하여 구현됩니다
#
(점 연산자 :.
값 과 매우 유사 함 ).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
(객체 C의 필드 값 / 함수 x 참조)C#x
(특성 C에서 필드 유형 x 참조)
- 함수 서명 (구현 없음)
- 가치 수준 :
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
, ( “viewable as”)A
는 다음과 같이 볼 수있는 경우에만 컴파일됩니다B
(예 :A
에서 하위 유형으로의 암시 적 변환 이 있음B
).- 예
- 더 많은 비교 연산자
- 본질적으로 유형 비교입니다. 예 :
- 가치 수준 :
유형과 값 간 변환
-
많은 예제에서 트레이 트를 통해 정의 된 유형은 추상적이고 봉인 된 경우가 많으므로 직접 인스턴스화하거나 익명 하위 클래스를 통해 인스턴스화 할 수 없습니다. 따라서
null
특정 유형의 관심을 사용하여 값 수준 계산을 수행 할 때 자리 표시 자 값 으로 사용하는 것이 일반적 입니다.- 예 : 관심있는 유형은
val x:A = null
어디에 있습니까?A
- 예 : 관심있는 유형은
-
유형 삭제로 인해 매개 변수화 된 유형은 모두 동일하게 보입니다. 또한 (위에서 언급했듯이) 작업하는 값은 모두 인 경향이
null
있으므로 개체 유형에 대한 조건 지정 (예 : match 문을 통해)은 효과가 없습니다.
비결은 암시 적 함수와 값을 사용하는 것입니다. 기본 사례는 일반적으로 암시 적 값이고 재귀 사례는 일반적으로 암시 적 함수입니다. 실제로 유형 수준 프로그래밍은 암시 적 요소를 많이 사용합니다.
다음 예제를 고려하십시오 ( metascala 및 apocalisp에서 가져옴 ).
sealed trait Nat
sealed trait _0 extends Nat
sealed trait Succ[N <: Nat] extends Nat
여기에는 자연수의 피노 인코딩이 있습니다. 즉, 음수가 아닌 각 정수에 대한 유형이 있습니다. 0에 대한 특수 유형, 즉 _0
; 및 각각 0보다 큰 정수의 형태의 유형 가지고 Succ[A]
, A
작은 정수를 나타내는 형식이다. 예를 들어 2를 나타내는 유형은 다음과 같습니다 Succ[Succ[_0]]
(0을 나타내는 유형에 두 번 적용).
보다 편리한 참조를 위해 다양한 자연수에 별칭을 지정할 수 있습니다. 예:
type _3 = Succ[Succ[Succ[_0]]]
(이것은 val
함수의 결과로 a를 정의하는 것과 매우 유사 합니다.)
이제 우리가 의 타입에 인코딩 된 자연수를 나타내는 정수 를 따르고 반환하는 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)
위 의 평가에서 수행 된 것처럼) 공급 하고 value를 사용 succToInt
하여 새 TypeToValue
객체를 생성합니다 1
. 다시 말하지만, 컴파일러는 이러한 모든 값을 명시 적으로 액세스 할 수 없기 때문에 암시 적으로 모든 값을 제공한다는 점에 유의해야합니다.
작업 확인
유형 수준 계산이 예상 한대로 작동하는지 확인하는 방법에는 여러 가지가 있습니다. 다음은 몇 가지 접근 방식입니다. 확인 하려는 두 가지 유형 A
및 B
을 확인하십시오. 그런 다음 다음 컴파일을 확인하십시오.
Equal[A, B]
- with : 특성
Equal[T1 >: T2 <: T2, T2]
( apocolisp에서 가져옴 )
- with : 특성
implicitly[A =:= B]
또는 유형을 값으로 변환하고 (위에 표시된대로) 값의 런타임 검사를 수행 할 수 있습니다. 예 : assert(toInt(a) == toInt(b))
where a
is of type A
및 b
is of type B
.
추가 자료
사용 가능한 구성의 전체 세트 는 scala 참조 매뉴얼 (pdf) 의 유형 섹션에서 찾을 수 있습니다 .
Adriaan Moors 는 스칼라의 예제와 함께 유형 생성자 및 관련 주제에 대한 여러 학술 논문을 보유하고 있습니다.
- 더 높은 종류의 제네릭 (pdf)
- 스칼라를위한 타입 생성자 다형성 : 이론과 실습 (pdf) (PhD 논문, Moors의 이전 논문 포함)
- 유형 생성자 다형성 추론
Apocalisp 는 스칼라의 유형 수준 프로그래밍에 대한 많은 예제가있는 블로그입니다.
- Scala 의 유형 수준 프로그래밍은 부울, 자연수 (위와 같음), 이진 숫자, 이기종 목록 등을 포함하는 일부 유형 수준 프로그래밍에 대한 환상적인 가이드 투어입니다.
- More Scala Typehackery 는 위의 람다 미적분 구현입니다.
ScalaZ 는 다양한 유형 수준 프로그래밍 기능을 사용하여 Scala API를 확장하는 기능을 제공하는 매우 활동적인 프로젝트입니다. 팔로어가 많은 매우 흥미로운 프로젝트입니다.
MetaScala 는 자연수, 부울, 단위, HList 등에 대한 메타 유형을 포함하는 Scala 용 유형 수준 라이브러리입니다. Jesper Nordenberg (그의 블로그) 입니다.
Michid (블로그) (다른 답변에서) 스칼라 입력 레벨의 프로그래밍의 굉장한 예제가 있습니다 :
- Scala를 사용한 메타 프로그래밍 파트 I : 추가
- Scala를 사용한 메타 프로그래밍 2 부 : 곱셈
- Scala를 사용한 메타 프로그래밍 Part III : 부분 함수 적용
- Scala를 사용한 메타 프로그래밍 : 조건부 컴파일 및 루프 언 롤링
- SKI 미적분의 스칼라 유형 수준 인코딩
Debasish Ghosh (블로그) 에는 관련 게시물도 있습니다.
- 스칼라의 고차 추상화
- 정적 타이핑은 당신에게 유리한 시작을 제공합니다
- Scala implicits 타입 클래스, 여기 왔어요
- 스칼라 유형 클래스로 리팩토링
- 일반화 된 유형 제약 사용
- 스칼라가 시스템 단어를 입력하는 방법
- 추상 유형 멤버 중에서 선택
(나는이 주제에 대해 조사를 해왔고 여기에 내가 배운 것이 있습니다. 나는 아직 그것에 익숙하지 않으므로이 답변에서 부정확 한 부분을 지적하십시오.)
답변
여기에있는 다른 링크 외에도 Scala의 유형 수준 메타 프로그래밍에 대한 블로그 게시물도 있습니다.
답변
Twitter에서 제안한대로 : Shapeless : Miles Sabin의 Scala에서 일반 / 다형 프로그래밍 탐색 .