パスに依存するタイプがあり、EpigramやAgdaなどの言語のほぼすべての機能をScalaで表現することは可能だと思いますが、Scalaが他の領域で非常にうまく機能するように、Scalaがこれをより明示的にサポートしない理由を知りたいと思います(たとえば、DSL)?「それは必要ない」のように私が欠けているものはありますか?
パスに依存するタイプがあり、EpigramやAgdaなどの言語のほぼすべての機能をScalaで表現することは可能だと思いますが、Scalaが他の領域で非常にうまく機能するように、Scalaがこれをより明示的にサポートしない理由を知りたいと思います(たとえば、DSL)?「それは必要ない」のように私が欠けているものはありますか?
回答:
構文上の便宜はさておき、シングルトン型、パス依存型、および暗黙値の組み合わせは、私が無形で実証しようとしたように、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に組み込まれている依存型のsumおよびproduct型を持たないため、「完全に」依存型の言語ではないことがよくあります。これはある程度、ファンダメンタルズよりも形式上の固執を反映していると思いますが、それでも、Scalaが一般的に認められているよりも他の言語に非常に近いことを確認します。
用語に関係なく、依存合計タイプ(シグマタイプとも呼ばれます)は、2番目の値のタイプが最初の値に依存する値のペアです。これは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より前(または実験的な-Ydependent-methodsタイプのScalaコンパイラーオプションを介して以前のバージョン)のScalaの'Bakery of Doom'から脱出するために必要な依存メソッドタイプのエンコーディングの重要な部分です。
依存製品タイプ(別名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の使用していること<:<
ではなく、サブタイプ目撃オペレータを=:=
理由res2.type
とres3.type
シングルトンの種類と我々はRHSに検証されているタイプよりもので、より正確です)。
ただし実際には、ScalaではAgdaやIdrisの場合のように、Sigma型とPi型をエンコードし、そこから処理を開始することはしません。代わりに、パス依存型、シングルトン型、および暗黙を直接使用します。サイズなしの型、拡張可能なレコード、包括的なHList、定型句のスクラップ、汎用のZipperなど、これが無形でどのように機能するかを示す多数の例を見つけることができます。
私が見ることができる唯一の異論は、上記のPi型のエンコーディングでは、依存する値のシングルトン型を表現可能にする必要があるということです。残念ながらScalaでは、これは参照型の値に対してのみ可能であり、非参照型(たとえば、Int)の値に対しては不可能です。これは残念ですが、本質的な問題ではありません。Scalaの型チェッカーは、非参照値のシングルトン型を内部的に表し、それらを直接表現できるようにするためにいくつかの実験が行われています。実際には、自然数のかなり標準的なタイプレベルのエンコーディングで問題を回避できます。
いずれにせよ、このわずかなドメイン制限が、依存型付け言語としてのScalaのステータスに対する異議として使用できるとは思いません。そうである場合、同じことがDependent ML(自然数の値への依存のみを許可する)についても言え、これは奇妙な結論になります。
(私が経験から知っているように、Coqプルーフアシスタントで依存型を使用していて、それらを完全にサポートしていますが、それでも非常に便利な方法ではないため)依存型は非常に高度なプログラミング言語機能であり、正しく理解してください-そして実際には複雑さの指数関数的な爆発を引き起こす可能性があります。彼らはまだコンピュータサイエンスの研究のトピックです。
Scalaのパス依存型はΣ型のみを表し、Π型は表すことができないと思います。この:
trait Pi[T] { type U }
正確にはtypeタイプではありません。定義により、Π型、つまり依存積は、結果の型が引数値に依存する関数であり、普遍的な量指定子、つまりiex: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に生息するすべての値のプロパティを表す方程式を効果的に形成する機能です。特定のプロパティを持つオブジェクトの存在を表すために使用されるΣタイプ論理が形成され、ここで私たちの方程式は証明される定理です。
余談ですが、実際の場合、定理は、コードから自動的に導出したり、多大な労力なしに解決することができないところまで、非常に重要です。この方法でリーマン仮説を立てることもできます。実際に証明したり、永久にループしたり、例外をスローしたりしないと、実装が不可能な署名を見つけるためだけです。
Pi
、値に応じてタイプを作成するためにを使用する例を上に示しました。
depList
タイプU
を抽出します。このタイプはたまたまシングルトンタイプであり、現在Scalaシングルトンオブジェクトで使用可能で、それらの正確な値を表します。例では、シングルトンオブジェクトタイプごとに1つの実装を作成するため、Σタイプのようにタイプと値をペアにします。一方、Πタイプは、その入力パラメーターの構造を照合する式です。ΠタイプではすべてのパラメータータイプがGADTである必要があり、ScalaはGADTを他のタイプと区別しないため、Scalaにはそれらがない可能性があります。Pi[T]
t
Pi
pi.U
Milesの例では、依存型としてカウントされませんか?値にありますpi
。
pi.U
はの値に依存しますpi
。予防という問題trait Pi[T]
Π型になるのは、我々が(例えば、任意の引数の値にも依存することができないことであるt
でdepList
タイプレベルでその引数を持ち上げず)。
問題は、依存型指定機能をより直接的に使用することでした。私の意見では、Scalaが提供するものよりも直接型依存型のアプローチを採用することには利点があります。
現在の答えは、タイプ理論レベルでの質問を議論しようとします。もっと実用的なものにしたい。これが、Scala言語の依存型のサポートのレベルで人々が分かれている理由を説明しているのかもしれません。多少異なる定義が考えられるかもしれません。(1つが正しいことと1つが間違っていることは言うまでもありません)。
これは、ScalaをIdrisのようなものに変換するのがどれほど簡単か(非常に難しいと思います)、またはIdrisのような機能をより直接的にサポートするライブラリ( singletons
Haskellでの試行の。
代わりに、Scalaとイドリスのような言語の実用的な違いを強調したいと思います。
値および型レベルの式のコードビットとは何ですか?Idrisは同じコードを使用し、Scalaは非常に異なるコードを使用します。
Scala(Haskellと同様)は、多くの型レベルの計算をエンコードできる場合があります。これは、などのライブラリで表示されますshapeless
。これらのライブラリは、いくつかの本当に印象的で巧妙なトリックを使用してそれを行います。ただし、それらの型レベルのコードは、(現在)値レベルの式とはかなり異なります(Haskellでは、ギャップがいくぶん狭くなることがわかります)。Idrisでは、型レベルでISの値レベルの式を使用できます。
明らかな利点はコードの再利用です(型レベルの式を値レベルとは別にコーディングする必要はありません)。値レベルのコードを書く方が簡単なはずです。シングルトンのようなハックに対処する必要がないほうが簡単です(パフォーマンスコストは言うまでもありません)。1つのことを学ぶ2つのことを学ぶ必要はありません。実際的なレベルでは、必要な概念は少なくなります。型の同義語、型ファミリー、関数など...関数だけはどうですか?私の意見では、この統合の利点ははるかに深く行き、構文上の便宜以上のものです。
検証済みのコードを検討してください。参照:https :
//github.com/idris-lang/Idris-dev/blob/v1.3.0/libs/contrib/Interfaces/Verified.idr
タイプチェッカーは、monadic / 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)プルーフアシスタンスについて、イドリスとスカラのもう1つの大きな違いがあります。Scala(またはHaskell)には、分岐した証明を書くことを防ぐことができるものは何もありません。
case class Void(underlying: Nothing) extends AnyVal //should be uninhabited
def impossible() : Void = impossible()
Idrisにはtotal
、このようなコードのコンパイルを妨げるキーワードがあります。
値と型レベルのコード(Haskellのようなsingletons
)を統一しようとするScalaライブラリーは、Scalaの依存型のサポートをテストする興味深いものでしょう。そのようなライブラリーは、パスに依存する型のために、Scalaではるかによくできますか?
私はScalaに不慣れなので、その質問に自分で答えることはできません。