[scala] 스칼라가 종속 유형을 명시 적으로 지원하지 않는 이유는 무엇입니까?

이 경로 의존적 유형은 나는 거의 모든 스칼라에 경구 또는 AGDA와 같은 언어의 특징을 표현하는 것이 가능하다 생각하지만, 스칼라가 지원하지 않는 이유를 궁금 해요 는 다른 지역에서 매우 잘하는 것처럼 더 명시 적으로 (예를 들어 , DSL)? “필요하지 않습니다”와 같이 내가 놓친 것이 있습니까?



답변

구문 적 편의를 제외하고, 싱글 톤 유형, 경로 종속 유형 및 암시 적 값의 조합은 내가 shapeless 에서 보여 주려고 시도한 것처럼 Scala가 종속 유형에 대해 놀랍도록 좋은 지원을 제공한다는 것을 의미합니다 .

종속 유형에 대한 Scala의 내장 지원은 경로 종속 유형을 통해 이루어 집니다 . 이를 통해 유형이 객체 (즉, 값) 그래프를 통해 선택자 경로에 의존 할 수 있습니다.

scala> class Foo { class Bar }
defined class Foo

scala> val foo1 = new Foo
foo1: Foo = Foo@24bc0658

scala> val foo2 = new Foo
foo2: Foo = Foo@6f7f757

scala> implicitly[foo1.Bar =:= foo1.Bar] // OK: equal types
res0: =:=[foo1.Bar,foo1.Bar] = <function1>

scala> implicitly[foo1.Bar =:= foo2.Bar] // Not OK: unequal types
<console>:11: error: Cannot prove that foo1.Bar =:= foo2.Bar.
              implicitly[foo1.Bar =:= foo2.Bar]

제 생각에 위의 내용은 “Scala가 종속적으로 입력되는 언어입니까?”라는 질문에 답하기에 충분해야합니다. 긍정적으로 : 여기에 접두사 인 값으로 구별되는 유형이 있음이 분명합니다.

그러나, Scala는 Agda, Coq 또는 Idris에서 내장 함수로 발견되는 종속 합계 및 제품 유형 이 없기 때문에 “완전히”종속 된 유형 언어가 아니라는 점을 종종 반대합니다 . 나는 이것이 기본에 대한 형식에 대한 고정을 어느 정도 반영한다고 생각하지만, 그럼에도 불구하고 Scala가 일반적으로 인정되는 것보다 다른 언어에 훨씬 더 가깝다는 것을 보여줄 것입니다.

용어에도 불구하고 종속 합계 유형 (시그마 유형이라고도 함)은 두 번째 값의 유형이 첫 번째 값에 종속되는 값 쌍입니다. 이것은 Scala에서 직접 표현할 수 있습니다.

scala> trait Sigma {
     |   val foo: Foo
     |   val bar: foo.Bar
     | }
defined trait Sigma

scala> val sigma = new Sigma {
     |   val foo = foo1
     |   val bar = new foo.Bar
     | }
sigma: java.lang.Object with Sigma{val bar: this.foo.Bar} = $anon$1@e3fabd8

사실, 이것은 2.10 이전 Scala 의 ‘Bakery of Doom’ 에서 탈출하는 데 필요한 종속 메소드 유형 인코딩의 중요한 부분입니다 (또는 실험적인 -Ydependent-method 유형 Scala 컴파일러 옵션을 통해 이전).

종속 제품 유형 (일명 Pi 유형)은 기본적으로 값에서 유형에 이르는 기능입니다. 이들은 정적으로 크기가 지정된 벡터 및 종속 유형 프로그래밍 언어에 대한 다른 포스터 자식을 표현하는 데 중요합니다 . 경로 종속 유형, 싱글 톤 유형 및 암시 적 매개 변수의 조합을 사용하여 Scala에서 Pi 유형을 인코딩 할 수 있습니다. 먼저 T 유형의 값에서 유형 U 로의 함수를 나타낼 특성을 정의합니다.

scala> trait Pi[T] { type U }
defined trait Pi

이 유형을 사용하는 다형성 방법을 정의 할 수 있습니다.

scala> def depList[T](t: T)(implicit pi: Pi[T]): List[pi.U] = Nil
depList: [T](t: T)(implicit pi: Pi[T])List[pi.U]

( pi.U결과 유형에서 경로 종속 유형의 사용에 유의하십시오 List[pi.U]). T 유형의 값이 주어지면이 함수는 특정 T 값에 해당하는 유형의 값 목록을 반환합니다.

이제 유지하려는 기능적 관계에 대한 적절한 값과 암시 적 증인을 정의 해 보겠습니다.

scala> object Foo
defined module Foo

scala> object Bar
defined module Bar

scala> implicit val fooInt = new Pi[Foo.type] { type U = Int }
fooInt: java.lang.Object with Pi[Foo.type]{type U = Int} = $anon$1@60681a11

scala> implicit val barString = new Pi[Bar.type] { type U = String }
barString: java.lang.Object with Pi[Bar.type]{type U = String} = $anon$1@187602ae

이제 여기에 Pi 유형 사용 기능이 작동합니다.

scala> depList(Foo)
res2: List[fooInt.U] = List()

scala> depList(Bar)
res3: List[barString.U] = List()

scala> implicitly[res2.type <:< List[Int]]
res4: <:<[res2.type,List[Int]] = <function1>

scala> implicitly[res2.type <:< List[String]]
<console>:19: error: Cannot prove that res2.type <:< List[String].
              implicitly[res2.type <:< List[String]]
                    ^

scala> implicitly[res3.type <:< List[String]]
res6: <:<[res3.type,List[String]] = <function1>

scala> implicitly[res3.type <:< List[Int]]
<console>:19: error: Cannot prove that res3.type <:< List[Int].
              implicitly[res3.type <:< List[Int]]

(여기서 우리는 Scala의 <:<subtype-witnessing 연산자를 사용하기 =:=때문에 res2.typeres3.type단일 유형 이기 때문에 RHS에서 확인하는 유형보다 더 정확합니다.)

그러나 실제로 Scala에서는 Sigma 및 Pi 유형을 인코딩하는 것으로 시작하지 않고 Agda 또는 Idris 에서처럼 거기에서 진행합니다. 대신 경로 종속 유형, 단일 유형 및 암시를 직접 사용합니다. 크기가 지정된 유형 , 확장 가능한 레코드 , 포괄적 인 HLists , 상용구 스크랩 , 일반 지퍼 등 형태가없는 방식에 대한 수많은 예를 찾을 수 있습니다 .

내가 볼 수있는 유일한 반대는 위의 Pi 유형 인코딩에서 우리는 의존 값의 싱글 톤 유형이 표현 가능해야한다는 것입니다. 불행히도 Scala에서는 이것은 참조 유형의 값에 대해서만 가능하며 비 참조 유형의 값 (특히 Int)에 대해서는 불가능합니다. 이것은 부끄러운 일이지만 본질적인 어려움은 아닙니다. Scala의 유형 검사기는 내부적으로 비 참조 값의 싱글 톤 유형을 나타내며 이를 직접적으로 표현할 수 있도록 가지 실험 이있었습니다 . 실제로 우리는 자연수에 대한 상당히 표준적인 유형 수준 인코딩으로 문제를 해결할 있습니다.

어쨌든이 작은 도메인 제한이 종속적으로 입력 된 언어로서의 스칼라의 상태에 대한 이의로 사용될 수 있다고 생각하지 않습니다. 그렇다면 종속 ML (자연수 값에 대한 종속성 만 허용)에 대해서도 마찬가지라고 할 수 있으며 이는 기괴한 결론입니다.


답변

(경험을 통해 알 수 있듯이 Coq 증명 도우미에서 종속 유형을 사용하여 완전히 지원하지만 여전히 매우 편리한 방식은 아닙니다) 종속 유형은 매우 어려운 프로그래밍 언어 기능이기 때문이라고 생각합니다. 옳다-실제로 복잡성에 기하 급수적 인 폭발을 일으킬 수 있습니다. 그들은 여전히 ​​컴퓨터 과학 연구의 주제입니다.


답변

저는 Scala의 경로 종속 유형이 Σ 유형 만 나타낼 수 있지만 Π 유형은 나타낼 수 없다고 생각합니다. 이:

trait Pi[T] { type U }

정확히 Π 유형이 아닙니다. 정의에 따라 Π- 유형 또는 종속 곱은 결과 유형이 인수 값에 따라 달라지는 함수로, 범용 수량자를 나타내는 ∀x : A, B (x)입니다. 그러나 위의 경우에는 유형 T에만 의존하고이 유형의 일부 값에는 의존하지 않습니다. Pi 특성 자체는 Σ 유형, 실존 적 수량 자입니다. 즉 ∃x : A, B (x). 이 경우 개체의 자체 참조는 수량화 된 변수로 작동합니다. 그러나 암시 적 매개 변수로 전달되면 유형별로 확인되므로 일반 유형 함수로 축소됩니다. Scala의 종속 제품에 대한 인코딩은 다음과 같습니다.

trait Sigma[T] {
  val x: T
  type U //can depend on x
}

// (t: T) => (∃ mapping(x, U), x == t) => (u: U); sadly, refinement won't compile
def pi[T](t: T)(implicit mapping: Sigma[T] { val x = t }): mapping.U 

여기서 누락 된 부분은 필드 x를 예상 값 t로 정적으로 제한하는 기능으로, 유형 T에 거주하는 모든 값의 속성을 나타내는 방정식을 효과적으로 형성합니다. 우리의 방정식이 증명되어야 할 정리 인 논리가 형성됩니다.

참고로, 실제 경우 정리는 코드에서 자동으로 파생되거나 상당한 노력없이 해결 될 수없는 지점까지 매우 사소하지 않을 수 있습니다. 이러한 방식으로 리만 가설을 공식화 할 수도 있습니다. 실제로 증명하거나, 영원히 반복하거나, 예외를 던지지 않고는 구현할 수없는 서명을 찾을 수 있습니다.


답변

질문은 종속적으로 입력 된 기능을 더 직접적으로 사용하는 것에 관한 것이었고, 제 생각에는 Scala가 제공하는 것보다 더 직접적인 종속 입력 방식을 사용하는 것이 이점이있을 것입니다.
현재 답변은 유형 이론적 수준에서 질문을 주장합니다. 나는 그것에 더 실용적인 스핀을 넣고 싶다. 이것은 사람들이 Scala 언어에서 종속 유형의 지원 수준에서 왜 나뉘어져 있는지 설명 할 수 있습니다. 우리는 다소 다른 정의를 염두에 둘 수 있습니다. (하나는 옳고 하나는 틀렸다고 말하는 것이 아닙니다).

이것은 Scala를 Idris와 같은 것으로 바꾸거나 (매우 힘들다고 생각합니다) Idris와 같은 기능을보다 직접적으로 지원하는 라이브러리를 작성하는 것이 얼마나 쉬운 지 (예 : singletonsHaskell에 있는 시도)라는 질문에 답하려는 시도가 아닙니다 .

대신 스칼라와 이드리스와 같은 언어의 실용적인 차이점을 강조하고 싶습니다.
값 및 유형 수준 식에 대한 코드 비트는 무엇입니까? Idris는 동일한 코드를 사용하고 Scala는 매우 다른 코드를 사용합니다.

Scala (Haskell과 유사)는 많은 유형 수준 계산을 인코딩 할 수 있습니다. 이것은 shapeless. 이 라이브러리는 정말 인상적이고 영리한 트릭을 사용하여 수행합니다. 그러나 유형 레벨 코드는 (현재) 값 레벨 표현식과는 상당히 다릅니다 (하스켈에서는 그 간격이 다소 가까워지는 것을 알았습니다). Idris는있는 그대로 유형 수준에서 값 수준 식을 사용할 수 있습니다.

명백한 이점은 코드 재사용입니다 (두 위치 모두에서 필요한 경우 유형 수준 식을 값 수준과 별도로 코딩 할 필요가 없음). 가치 수준 코드를 작성하는 것이 훨씬 쉬워야합니다. 싱글 톤과 같은 해킹을 처리 할 필요가없는 것이 더 쉬울 것입니다 (성능 비용은 말할 것도 없습니다). 한 가지를 배우는 두 가지를 배울 필요가 없습니다. 실용적인 수준에서는 더 적은 수의 개념이 필요합니다. 타입 동의어, 타입 패밀리, 함수, … 그냥 함수는 어떻습니까? 제 생각에는 이러한 통합 이점은 훨씬 더 깊고 구문상의 편의 이상입니다.

확인 된 코드를 고려하십시오. 참조 :
https://github.com/idris-lang/Idris-dev/blob/v1.3.0/libs/contrib/Interfaces/Verified.idr
Type checker는 monadic / functor / applicative law의 증명을 확인하고 증명은 실제에 관한 것입니다. monad / functor / applicative의 구현 및 동일하거나 동일하지 않을 수있는 일부 인코딩 된 유형 수준에 해당하지 않습니다. 큰 문제는 우리가 무엇을 증명하고 있는가?

영리한 인코딩 트릭을 사용하여 동일한 작업을 수행 할 수 있습니다 (Haskell 버전의 경우 다음 참조, Scala의 경우 본 적이 없음)
https://blog.jle.im/entry/verified-instances-in-haskell.html
https : // github.com/rpeszek/IdrisTddNotes/wiki/Play_FunctorLaws
는 유형이 너무 복잡해서 법칙을보기가 어렵고, 값 수준 표현은 유형 수준 사물로 변환 (자동이지만 여전히)되며 해당 변환도 신뢰해야합니다. . 이 모든 것에는 오류가 발생할 여지가 있으며, 이는 컴파일러가 증명 도우미 역할을하는 목적을 다소 위반합니다.

(2018.8.10 편집) 증명 지원에 대해 말하면, 여기에 Idris와 Scala의 또 다른 큰 차이점이 있습니다. Scala (또는 Haskell)에는 발산 증명을 작성하는 것을 막을 수있는 것은 없습니다.

case class Void(underlying: Nothing) extends AnyVal //should be uninhabited
def impossible() : Void = impossible()

Idris에는 total이와 같은 코드가 컴파일되는 것을 방지하는 키워드 가 있습니다.

값과 유형 레벨 코드 (예 : Haskell singletons) 를 통합하려는 Scala 라이브러리는 Scala의 종속 유형 지원에 대한 흥미로운 테스트가 될 것입니다. 이러한 라이브러리는 경로 종속 유형으로 인해 Scala에서 훨씬 더 잘 수행 될 수 있습니까?

나는 그 질문에 스스로 답하기에는 Scala에 너무 익숙하다.


답변