Scala'nın bağımlı türleri açıkça desteklememesinin herhangi bir nedeni var mı?


109

Yol bağımlı tipler var ve Scala'da Epigram veya Agda gibi dillerin hemen hemen tüm özelliklerini ifade etmenin mümkün olduğunu düşünüyorum, ancak Scala'nın diğer alanlarda çok hoş olduğu gibi neden bunu daha açık bir şekilde desteklemediğini merak ediyorum (örneğin , DSL'ler)? "Gerekli değil" gibi eksik bir şey var mı?


3
Scala tasarımcıları, Barendregt Lambda Cube'un Tip Teorisinin her şeyin sonu olmadığına inanıyor. Nedeni bu olabilir veya olmayabilir.
Jörg W Mittag

8
@ JörgWMittag Lamda Küpü nedir? Bir çeşit sihirli cihaz mı?
Ashkan Kh. Nazary

@ ashy_32bit Barendregt'in "Genelleştirilmiş Tip Sistemlerine Giriş" makalesine
iainmcgin

Yanıtlar:


152

Sözdizimsel kolaylık bir yana, tekli türlerin, yola bağlı türlerin ve örtük değerlerin kombinasyonu, şekilsiz olarak göstermeye çalıştığım gibi, Scala'nın bağımlı yazım için şaşırtıcı derecede iyi bir desteğe sahip olduğu anlamına gelir .

Scala'nın bağımlı türler için içsel desteği, yola bağlı türler aracılığıyladır . Bunlar, bir türün bir nesne- (yani değer-) grafiği boyunca bir seçici yoluna bağlı olmasına izin verir.

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]

Benim görüşüme göre, yukarıdakiler "Scala bağımlı yazılan bir dil mi?" Sorusuna yanıt vermek için yeterli olmalıdır. olumlu olarak: burada, ön ekleri olan değerlerle ayırt edilen türlerimiz olduğu açıktır.

Ancak, Agda veya Coq veya Idris'te içsel olarak bulunan bağımlı toplam ve ürün türlerine sahip olmadığı için Scala'nın "tamamen" bağımlı bir tür dili olmadığı sıklıkla itiraz edilir . Sanırım bu, bir dereceye kadar temeller üzerinde biçim üzerine bir saplantıyı yansıtıyor, yine de, Scala'nın bu diğer dillere tipik olarak kabul edilenden çok daha yakın olduğunu göstermeye çalışacağım.

Terminolojiye rağmen, bağımlı toplam türleri (Sigma türleri olarak da bilinir), yalnızca ikinci değerin türünün birinci değere bağlı olduğu bir değer çiftidir. Bu doğrudan Scala'da temsil edilebilir,

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

ve aslında bu, 2.10'dan önce (veya daha önce deneysel -Ydependent-yöntem türleri Scala derleyici seçeneği aracılığıyla) Scala'daki 'Bakery of Doom'dan kaçmak için gereken bağımlı yöntem türlerinin kodlanmasının çok önemli bir parçasıdır .

Bağımlı ürün türleri (diğer adıyla Pi türleri), temelde değerlerden türlere işlevlerdir. Statik olarak boyutlandırılmış vektörlerin ve bağımlı olarak yazılmış programlama dilleri için diğer poster çocuklarının temsilinin anahtarıdırlar . Scala'daki Pi türlerini yola bağlı türler, tekli türler ve örtük parametrelerin bir kombinasyonunu kullanarak kodlayabiliriz. İlk önce, T tipi bir değerden U tipine kadar bir işlevi temsil edecek bir özellik tanımlarız,

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

Daha sonra bu türü kullanan polimorfik bir yöntem tanımlayabiliriz,

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.Usonuç türünde yola bağımlı türün kullanımına dikkat edin List[pi.U]). T türünde bir değer verildiğinde, bu işlev söz konusu T değerine karşılık gelen türden bir (n boş) değer listesi döndürür.

Şimdi, tutmak istediğimiz işlevsel ilişkiler için bazı uygun değerler ve örtük tanıklar tanımlayalım,

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

Ve şimdi burada Pi tipi kullanma işlevimiz iş başında,

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]]

(not burada Scala'nın kullandıkları <:<yerine alt tip-tanık operatörü =:=çünkü res2.typeve res3.typetekil türleri ve biz rhs doğruluyorsanız türlerinden daha dolayısıyla daha hassas olan).

Ancak pratikte Scala'da Sigma ve Pi türlerini kodlayarak başlayıp ardından Agda veya Idris'te yaptığımız gibi oradan devam etmezdik. Bunun yerine, yola bağlı türleri, tekli türleri ve dolaylı uygulamaları kullanırdık. Bunun şekilsiz olarak nasıl oynandığına dair çok sayıda örnek bulabilirsiniz: boyutlu tipler , genişletilebilir kayıtlar , kapsamlı HList'ler , kazan plakanızı hurdaya ayırın , jenerik Fermuarlar vb.

Görebildiğim tek itiraz, Pi türlerinin yukarıdaki kodlamasında, bağlı değerlerin tekil türlerinin ifade edilebilir olmasını istediğimizdir. Ne yazık ki Scala'da bu sadece referans tiplerinin değerleri için mümkündür ve referans olmayan tiplerin değerleri için mümkün değildir (özellikle örneğin Int). Scala'nın tip denetleyicisi dahili olmayan referans değerlerinin tekil türlerini temsil eder ve bir olmuştur: Bu bir utanç değil, içsel bir zorluktur çiftin ait deneyler onları doğrudan ifade edilebilen yapımında. Pratikte , doğal sayıların oldukça standart bir tür düzeyinde kodlamasıyla sorunu çözebiliriz .

Her durumda, bu küçük alan kısıtlamasının Scala'nın bağımlı olarak yazılmış bir dil statüsüne bir itiraz olarak kullanılabileceğini düşünmüyorum. Eğer öyleyse, aynı şey Bağımlı ML için de söylenebilir (bu sadece doğal sayı değerlerine bağımlılıklara izin verir) ve bu tuhaf bir sonuç olur.


8
Miles, bu çok detaylı cevap için teşekkürler. Yine de bir şeyi biraz merak ediyorum. Örneklerinizden hiçbiri ilk bakışta Haskell'de ifade etmesi özellikle imkansız görünmüyor; o zaman Haskell'in aynı zamanda bağımlı olarak yazılmış bir dil olduğunu mu iddia ediyorsunuz?
Jonathan Sterling

8
Ben McBride en açıklanan tekniklerinden özünde burada tekniklerini ayırt edemez, çünkü "Faking It" downvoted citeseerx.ist.psu.edu/viewdoc/summary?doi=10.1.1.22.2636 - yani bu simüle etmek yollarıdır bağımlı türler, bunları doğrudan sağlamaz.
sclv

2
@sclv Scala'nın herhangi bir kodlama biçimi olmayan bağımlı türlere sahip olduğunu kaçırdığınızı düşünüyorum: yukarıdaki ilk örneğe bakın. Pi türlerini kodlamamın Connor'ın kağıdıyla aynı tekniklerden bazılarını kullandığı konusunda oldukça haklısınız, ancak zaten yola bağlı türleri ve tekli türleri içeren bir alt tabakadan.
Miles Sabin

4
Hayır! Elbette nesnelere bağlı türlere sahip olabilirsiniz (bu, nesnelerin modüller olarak bir sonucudur). Ancak, değer düzeyinde tanıklar kullanmadan bu türler üzerinde hesaplama yapamazsınız. Aslında =: = kendisi değer düzeyinde bir tanıktır! Hâlâ numara yapıyorsun, tıpkı Haskell'deki gibi, ya da belki daha çok.
sclv

9
Scala'nın =: = değer düzeyi değil, bir tür kurucusu - bunun için bir değer burada: github.com/scala/scala/blob/v2.10.3/src/library/scala/… ve görünmüyor Agda ve İdris gibi bağımlı tipte dillerde bir eşitlik önermesine tanık olmaktan özellikle farklı: refl. ( Sırasıyla www2.tcs.ifi.lmu.de/~abel/Equality.pdf bölüm 2 ve eb.host.cs.st-andrews.ac.uk/writings/idris-tutorial.pdf bölüm 8.1'e bakın.)
pdxleif

6

(Deneyimlerimden bildiğim gibi, onları tam olarak destekleyen ancak yine de çok uygun bir şekilde olmayan Coq prova asistanında bağımlı türleri kullandım) bağımlı türlerin çok gelişmiş bir programlama dili özelliği olduğunu varsayıyorum. doğru yapın - ve pratikte karmaşıklıkta üstel bir patlamaya neden olabilir. Hâlâ bilgisayar bilimi araştırmaları konusu.


Bana bağımlı tipler hakkında biraz teorik arka plan verecek kadar nazik olur musun (belki bir bağlantı)?
Ashkan Kh. Nazary

3
@ ashy_32bit Benjamin Pierce'ın "Türler ve Programlama Dillerinde Gelişmiş Konular" a erişebiliyorsanız, bağımlı türlere makul bir giriş sağlayan bir bölüm vardır. Teoride değil, pratikte bağımlı türlere özel ilgi duyan Conor McBride'ın bazı makalelerini de okuyabilirsiniz.
iainmcgin

3

Scala'nın yola bağlı türlerinin yalnızca Σ türlerini temsil edebileceğine, ancak Π türlerini temsil edemeyeceğine inanıyorum. Bu:

trait Pi[T] { type U }

tam olarak bir Π tipi değildir. Tanıma göre, type tipi veya bağımlı ürün, sonuç türünün evrensel niceliklendiriciyi temsil eden bağımsız değişken değerine bağlı olduğu bir işlevdir, yani ∀x: A, B (x). Bununla birlikte, yukarıdaki durumda, yalnızca T türüne bağlıdır, ancak bu türden bazı değerlere bağlı değildir. Pi özelliğinin kendisi bir Σ-tipi, varoluşsal bir niceleyici, yani ∃x: A, B (x). Bu durumda nesnenin öz referansı, niceliklendirilmiş değişken olarak hareket eder. Bununla birlikte, örtük parametre olarak iletildiğinde, tür bazında çözümlendiği için sıradan bir tür işlevine indirgenir. Scala'da bağımlı ürün kodlaması aşağıdaki gibi görünebilir:

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 

Buradaki eksik parça, x alanını statik olarak beklenen değer t ile sınırlama, etkin bir şekilde T tipinde bulunan tüm değerlerin özelliğini temsil eden bir denklem oluşturma yeteneğidir. Denklemimizin kanıtlanması gereken bir teorem olduğu mantık oluşturulur.

Bir yan notta, gerçek durumda teorem, koddan otomatik olarak türetilemeyeceği veya önemli miktarda çaba sarf edilmeden çözülemeyeceği noktaya kadar oldukça önemsiz olabilir. Riemann Hipotezi bile bu şekilde formüle edilebilir, ancak imzayı gerçekten kanıtlamadan, sonsuza dek döngüye girmeden veya bir istisna atmadan uygulanmasının imkansız olduğunu bulmak için.


1
Miles Sabin yukarıda Pi, değerlere bağlı olarak türler oluşturmak için bir kullanım örneği göstermiştir .
missingfaktor

Örnekte, depListekstreler tip Ugelen Pi[T]tipi (değer yok) için seçilmiş, t. Bu tür, şu anda Scala tekli nesnelerde bulunan ve bunların tam değerlerini temsil eden tekli tiptir. Örnek, Pitekil nesne türü başına bir uygulama oluşturur , böylece türü thus türündeki gibi değerle eşleştirir. Π-türü ise, girdi parametresinin yapısıyla eşleşen bir formüldür. Muhtemelen Scala bunlara sahip değildir çünkü Π türleri her parametre türünün GADT olmasını gerektirir ve Scala GADT'leri diğer türlerden ayırt etmez.
P. Frolov

Tamam, biraz kafam karıştı. Olmaz pi.Ubağımlı tip olarak Miles'in örnek sayısında? Değer üzerindedir pi.
missingfaktor

2
Aslında bağımlı tür olarak sayılır, ancak bunların farklı lezzetleri vardır: Σ-tipi ("P (x) olacak şekilde x vardır", mantıksal olarak) ve Π-tipi ("tüm x için P (x)") . Sizin de belirttiğiniz gibi tür pi.U, değerine bağlıdır pi. trait Pi[T]Bir Π-türü haline gelmeyi engellemenin sorunu, bu bağımsız değişkeni tür düzeyinde kaldırmadan keyfi bir bağımsız değişkenin değerine (örneğin, tiçinde depList) bağımlı hale getiremememizdir .
P. Frolov

1

Soru, bağımlı olarak yazılan özelliği daha doğrudan kullanmakla ilgiliydi ve bence, Scala'nın sunduğundan daha doğrudan bağımlı bir yazım yaklaşımına sahip olmanın bir faydası olacaktı.
Mevcut cevaplar, soruyu tip teorik düzeyde tartışmaya çalışır. Daha pragmatik bir dönüş yapmak istiyorum. Bu, Scala dilinde neden insanların bağımlı türlerin destek düzeyine göre bölündüğünü açıklayabilir. Aklımızda biraz farklı tanımlamalar olabilir. (birinin doğru ve birinin yanlış olduğunu söylememek).

Bu, Scala'yı İdris gibi bir şeye dönüştürmenin (çok zor olduğunu düşünüyorum) ya da İdris benzeri yeteneklere ( singletonsHaskell'de olmaya çalışmak gibi) daha doğrudan destek sunan bir kütüphane yazmanın ne kadar kolay olacağı sorusuna cevap verme girişimi değil .

Bunun yerine, Scala ile Idris gibi bir dil arasındaki pragmatik farkı vurgulamak istiyorum.
Değer ve tür seviyesi ifadeleri için kod bitleri nelerdir? İdris aynı kodu kullanır, Scala çok farklı bir kod kullanır.

Scala (Haskell'e benzer şekilde) çok sayıda tip seviyesi hesaplamasını kodlayabilir. Bu, gibi kütüphaneler tarafından gösterilir shapeless. Bu kütüphaneler bunu gerçekten etkileyici ve zekice bazı numaralar kullanarak yapıyor. Bununla birlikte, tür seviyesi kodları (şu anda) değer seviyesi ifadelerinden oldukça farklıdır (Haskell'de bu boşluğu biraz daha yakın buluyorum). İdris, değer seviyesi ifadesinin OLDUĞU GİBİ tip seviyesinde kullanılmasına izin verir.

Bariz fayda, kodun yeniden kullanılmasıdır (her iki yerde de ihtiyacınız varsa, tür düzeyi ifadelerini değer düzeyinden ayrı olarak kodlamanıza gerek yoktur). Değer seviyesi kodu yazmak çok daha kolay olmalı. Tekil bilgisayar korsanları gibi bilgisayar korsanları ile uğraşmak zorunda kalmamak daha kolay olmalı (performans maliyetinden bahsetmeye gerek yok). Tek bir şey öğrendiğiniz iki şeyi öğrenmenize gerek yok. Pragmatik düzeyde, daha az konsepte ihtiyacımız olur. Eşanlamlılar yazın, aileleri yazın, işlevler, ... peki ya sadece işlevler? Bana göre, bu birleştirici faydalar çok daha derine iniyor ve sözdizimsel kolaylıktan daha fazlası.

Doğrulanmış kodu düşünün. Bakınız:
https://github.com/idris-lang/Idris-dev/blob/v1.3.0/libs/contrib/Interfaces/Verified.idr
Tür denetleyici, monadic / functor / uygulama yasalarının kanıtlarını doğrular ve kanıtlar gerçek monad / functor / aplikatif uygulamaları ve aynı veya aynı olmayabilen bazı kodlanmış tip seviyesi eşdeğerleri değil. Asıl soru neyi kanıtlıyoruz?

Aynısı akıllı kodlama hileleri kullanarak da yapılabilir (Haskell sürümü için aşağıdakilere bakın, Scala için bir tane görmedim)
https://blog.jle.im/entry/verified-instances-in-haskell.html
https: // github.com/rpeszek/IdrisTddNotes/wiki/Play_FunctorLaws
, türlerin yasaları görmenin zor olması dışında, değer düzeyi ifadeleri (otomatik olarak ancak yine de) düzey şeyleri yazmak için dönüştürülür ve bu dönüşüme de güvenmeniz gerekir. . Derleyicinin ispat asistanı olarak hareket etme amacına meydan okuyan tüm bunlarda hata payı vardır.

(DÜZENLENMİŞ 2018.8.10) İspat yardımından bahsedersek, işte İdris ve Scala arasındaki bir başka büyük fark. Scala'da (veya Haskell'de) farklı ispatlar yazmayı engelleyebilecek hiçbir şey yoktur:

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

İdris bu totalgibi kodların derlenmesini engelleyen bir anahtar kelimeye sahipken .

Değer ve tür seviye kodunu birleştirmeye çalışan bir Scala kitaplığı (Haskell gibi singletons), Scala'nın bağımlı türleri desteklemesi için ilginç bir test olacaktır. Böyle bir kitaplık, yola bağlı türler nedeniyle Scala'da çok daha iyi yapılabilir mi?

Scala'da bu soruyu kendim cevaplayamayacak kadar yeniyim.

Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.