이 질문 에 따르면 Scala의 유형 시스템은 Turing complete 입니다. 초보자가 유형 수준 프로그래밍의 장점을 활용할 수 있도록 어떤 리소스를 사용할 수 있습니까?
지금까지 찾은 리소스는 다음과 같습니다.
이러한 리소스는 훌륭하지만 기본이 누락 된 것 같아서 구축 할 탄탄한 기반이 없습니다. 예를 들어, 유형 정의에 대한 소개는 어디에 있습니까? 유형에 대해 어떤 작업을 수행 할 수 있습니까?
좋은 입문 자료가 있습니까?
이 질문 에 따르면 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
(매개 변수화되지 않음 .#
(점 연산자 : .
값 과 매우 유사 함 ). 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]
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]
Equal[T1 >: T2 <: T2, T2]
( apocolisp에서 가져옴 )implicitly[A =:= B]
또는 유형을 값으로 변환하고 (위에 표시된대로) 값의 런타임 검사를 수행 할 수 있습니다. 예 : assert(toInt(a) == toInt(b))
where a
is of type A
및 b
is of type B
.
추가 자료
사용 가능한 구성의 전체 세트 는 scala 참조 매뉴얼 (pdf) 의 유형 섹션에서 찾을 수 있습니다 .
Adriaan Moors 는 스칼라의 예제와 함께 유형 생성자 및 관련 주제에 대한 여러 학술 논문을 보유하고 있습니다.
Apocalisp 는 스칼라의 유형 수준 프로그래밍에 대한 많은 예제가있는 블로그입니다.
ScalaZ 는 다양한 유형 수준 프로그래밍 기능을 사용하여 Scala API를 확장하는 기능을 제공하는 매우 활동적인 프로젝트입니다. 팔로어가 많은 매우 흥미로운 프로젝트입니다.
MetaScala 는 자연수, 부울, 단위, HList 등에 대한 메타 유형을 포함하는 Scala 용 유형 수준 라이브러리입니다. Jesper Nordenberg (그의 블로그) 입니다.
Michid (블로그) (다른 답변에서) 스칼라 입력 레벨의 프로그래밍의 굉장한 예제가 있습니다 :
Debasish Ghosh (블로그) 에는 관련 게시물도 있습니다.
(나는이 주제에 대해 조사를 해왔고 여기에 내가 배운 것이 있습니다. 나는 아직 그것에 익숙하지 않으므로이 답변에서 부정확 한 부분을 지적하십시오.)
여기에있는 다른 링크 외에도 Scala의 유형 수준 메타 프로그래밍에 대한 블로그 게시물도 있습니다.
Twitter에서 제안한대로 : Shapeless : Miles Sabin의 Scala에서 일반 / 다형 프로그래밍 탐색 .