Bağımlıca Yazılan Haskell, Şimdi?
Haskell, bir dereceye kadar, bağımlı olarak yazılmış bir dildir. Tip seviyesi veri kavramı var, şimdi sayesinde daha mantıklı bir şekilde yazılıyorDataKinds
GADTs
çalışma zamanı temsili vermek için bazı araçlar ( ) vardır. Bu nedenle, çalışma zamanı öğelerinin değerleri türlerde etkili bir şekilde ortaya çıkar, bu da bir dilin bağımlı olarak yazılmasının anlamıdır.
Basit veri türleri , tür düzeyine yükseltilir , böylece içerdikleri değerler türlerde kullanılabilir. Bu nedenle arketip örneği
data Nat = Z | S Nat
data Vec :: Nat -> * -> * where
VNil :: Vec Z x
VCons :: x -> Vec n x -> Vec (S n) x
mümkün olur ve bununla birlikte,
vApply :: Vec n (s -> t) -> Vec n s -> Vec n t
vApply VNil VNil = VNil
vApply (VCons f fs) (VCons s ss) = VCons (f s) (vApply fs ss)
ki bu güzel. Uzunluğun n
bu işlevde tamamen statik bir şey olduğunu ve giriş ve çıkış vektörlerinin aynı uzunlukta olmasını sağladığına dikkat edin, bu uzunluk, yürütülmesinde hiçbir rol oynamaz
vApply
. Buna karşılık, o yapar işlevi uygulamak için (yani imkansız) çok daha yanıltıcıdır n
verilen bir kopyalarını x
(olacağını pure
hiç vApply
s' <*>
)
vReplicate :: x -> Vec n x
çünkü çalışma zamanında kaç kopya çekileceğini bilmek çok önemlidir. Tek tonları girin.
data Natty :: Nat -> * where
Zy :: Natty Z
Sy :: Natty n -> Natty (S n)
Herhangi bir promotable türü için, değerlerinin çalışma zamanı kopyaları tarafından ikamet edilen, yükseltilen tür üzerinde endekslenen singleton ailesini oluşturabiliriz. Natty n
tür seviyesinin çalışma zamanı kopyalarının türüdür n
:: Nat
. Şimdi yazabiliriz
vReplicate :: Natty n -> x -> Vec n x
vReplicate Zy x = VNil
vReplicate (Sy n) x = VCons x (vReplicate n x)
Böylece, çalışma zamanı değerine sabitlenmiş bir tür düzeyi değeriniz vardır: çalışma zamanı kopyasının incelenmesi, tür düzeyi değerinin statik bilgisini geliştirir. Terimler ve türler ayrılsa da, singleton yapısını bir tür epoksi reçinesi olarak kullanarak fazlar arasında bağlar oluşturarak bağımlı tipte bir şekilde çalışabiliriz. Bu, türlerde rasgele çalışma zamanı ifadelerine izin vermenin çok uzun bir yoludur, ancak hiçbir şey değildir.
Nasty nedir? Ne kayıp?
Bu teknolojiye biraz baskı uygulayalım ve neyin sallanmaya başladığını görelim. Tekiltonların biraz daha dolaylı olarak yönetilebilir olması gerektiği fikrini alabiliriz
class Nattily (n :: Nat) where
natty :: Natty n
instance Nattily Z where
natty = Zy
instance Nattily n => Nattily (S n) where
natty = Sy natty
yazmamıza izin verin,
instance Nattily n => Applicative (Vec n) where
pure = vReplicate natty
(<*>) = vApply
Bu işe yarıyor, ama şimdi orijinal Nat
tipimizin üç kopya oluşturduğu anlamına geliyor : bir tür, bir tek aile ve bir tek sınıf. Müstehcen Natty n
değerler ve Nattily n
sözlükler alışverişinde bulunmak için oldukça karmaşık bir sürecimiz var . Dahası, Natty
değil Nat
: çalışma zamanı değerlerine bir çeşit bağımlılığımız var, ama ilk düşündüğümüz tipte değil. Tamamen bağımlı olarak yazılan hiçbir dil bağımlı türleri bu kadar karmaşık hale getirmez
Bu arada, Nat
terfi edilebilir , ancak olamaz Vec
. Dizine alınmış bir türe göre dizine ekleyemezsiniz. Bağımlı olarak yazılan diller üzerinde tam bir kısıtlama yoktur ve kariyerime bağımlı olarak yazılan bir gösteriş olarak, sadece tek katmanlı dizinleme yapan kişilere öğretmek için görüşmelerime iki katmanlı dizinleme örnekleri eklemeyi öğrendim. bir kart evi gibi katlanmamı beklememek zor ama mümkün. Sorun ne? Eşitlik. GADT'ler, bir kurucuya belirli bir dönüş türü verirken dolaylı olarak elde ettiğiniz kısıtlamaları açık denklem taleplerine çevirerek çalışır. Bunun gibi.
data Vec (n :: Nat) (x :: *)
= n ~ Z => VNil
| forall m. n ~ S m => VCons x (Vec m x)
İki denklemimizin her birinde, her iki tarafın da nazik Nat
.
Şimdi aynı çeviriyi vektörler üzerinden dizine eklenmiş bir şey için deneyin.
data InVec :: x -> Vec n x -> * where
Here :: InVec z (VCons z zs)
After :: InVec z ys -> InVec z (VCons y ys)
olur
data InVec (a :: x) (as :: Vec n x)
= forall m z (zs :: Vec x m). (n ~ S m, as ~ VCons z zs) => Here
| forall m y z (ys :: Vec x m). (n ~ S m, as ~ VCons y ys) => After (InVec z ys)
ve şimdi as :: Vec n x
ve
arasında eşitlik kısıtlamaları oluşturuyoruzVCons z zs :: Vec (S m) x
iki tarafın sözdizimsel olarak farklı (ama muhtemelen eşit) türler olduğu yerde . GHC çekirdeği şu anda böyle bir konsept için donatılmamıştır!
Başka ne eksik? Eh, Haskell çoğu tür düzeyinde eksik. Terfi edebileceğiniz terimlerin dili gerçekten sadece değişkenlere ve GADT dışı kuruculara sahiptir. Bunlara sahip olduğunuzda, type family
makine tip seviyesi programlar yazmanıza izin verir: bunlardan bazıları, terim düzeyinde yazmayı düşündüğünüz fonksiyonlara benzeyebilir (örneğin, Nat
ekleme ile donatmak , böylece eklemek için iyi bir tür verebilirsiniz Vec
) , ama bu sadece bir tesadüf!
Uygulamada eksik olan bir diğer şey de türleri değerlere göre endekslemek için yeni yeteneklerimizden yararlanan bir kütüphanedir . Bu cesur yeni dünyada ne olur Functor
ve ne Monad
olur? Bunu düşünüyorum, ama daha yapacak çok şey var.
Tür Düzeyinde Programları Çalıştırma
Haskell, çoğu bağımlı tipteki programlama dili gibi, iki
işlevsel semantiğe sahiptir. Çalışma zamanı sisteminin programları çalıştırma şekli vardır (yalnızca kapalı ifadeler, tür silindikten sonra, son derece optimize edilmiştir) ve sonra daktilo makinesinin programları çalıştırma şekli vardır (tip aileleriniz, "tip sınıf Prolog", açık ifadeler ile). Haskell için normalde ikisini karıştırmazsınız, çünkü yürütülen programlar farklı dillerdedir. Bağımlı olarak yazılan diller aynı program dili için ayrı çalışma zamanı ve statik yürütme modellerine sahiptir , ancak endişelenmeyin, çalışma zamanı modeli yine de yazım silmenizi ve gerçekten de kanıt silmenizi sağlar: Coq'un çıkarımı budurmekanizma size verir; en azından Edwin Brady'nin derleyicisinin yaptığı şey (Edwin gereksiz yere yinelenen değerleri, türleri ve kanıtları siler). Faz ayrımı
artık sözdizimsel kategorinin bir ayrımı olmayabilir , ama canlı ve iyidir.
Bağımlı olarak yazılan diller, toplam olarak, daktilo makinesinin programları uzun bir bekleyişten daha kötü bir şey korkusundan uzak çalıştırmasına izin verir. Haskell daha bağımlı bir şekilde yazıldıkça, statik yürütme modelinin ne olması gerektiği sorusuyla karşı karşıyayız? Bir yaklaşım, statik yürütmeyi toplam işlevlerle sınırlamak olabilir, bu da bize aynı özgürlüğü çalıştırmamıza izin verir, ancak bizi veri ve kodata arasında ayrım yapmaya (en azından tip seviyesi kodu) ayırmaya zorlayabilir, böylece fesih veya verimliliği zorunlu kılmak. Ama bu tek yaklaşım değil. Programları çalıştırmak istemeyen çok daha zayıf bir yürütme modeli seçmekte özgürüz, daha az denklemin sadece hesaplama ile ortaya çıkması pahasına. Ve aslında, GHC aslında bunu yapar. GHC çekirdeği için yazma kuralları çalıştırmadan
programları, ancak sadece denklemler için kanıtları kontrol etmek için. Çekirdeğe çevirirken, GHC'nin kısıtlayıcı çözücüsü, belirli bir ifadenin normal biçimine eşit olduğuna dair küçük bir kanıt izi oluşturarak, tür düzeyindeki programlarınızı çalıştırmaya çalışır. Bu kanıt oluşturma yöntemi biraz tahmin edilemez ve kaçınılmaz olarak eksiktir: örneğin, korkutucu görünümlü özyineleme ile savaşır ve muhtemelen akıllıca olur. Endişelenmemiz gerekmeyen bir şey IO
, daktiloda hesaplamaların yapılmasıdır: daktilo makinesinin launchMissiles
çalışma zamanı sisteminin yaptığıyla aynı anlamı vermek zorunda olmadığını unutmayın
!
Hindley-Milner Kültürü
Hindley-Milner tipi sistem, birçok insanın ayrımlar arasındaki ayrımı göremediği ve tesadüfün kaçınılmaz olduğunu varsaydığı talihsiz kültürel yan etki ile dört farklı ayrımın gerçekten müthiş tesadüfünü gerçekleştirir! Ne hakkında konuşuyorum?
- şartlar ve türler
- açıkça yazılmış şeyler vs üstü kapalı yazılmış şeyler
- çalışma süresinde mevcudiyet vs silme
- bağımlı olmayan soyutlama vs bağımlı niceleme
Terimler yazmaya ve türlerin çıkarılmaya bırakılmasına alışkınız ... ve sonra siliniriz. Sessiz ve statik olarak ilgili tip soyutlaması ve uygulaması ile tip değişkenlerini ölçmeye alışkınız.
Bu ayrımlar hizalanmadan önce vanilya Hindley-Milner'dan çok uzak durmak zorunda değilsiniz ve bu kötü bir şey değil . Başlangıç olarak, bunları birkaç yere yazmak istiyorsak daha ilginç türlere sahip olabiliriz. Bu arada, aşırı yüklenmiş işlevleri kullandığımızda tip sınıfı sözlükler yazmak zorunda değiliz, ancak bu sözlükler çalışma zamanında kesinlikle mevcut (veya satır içi). Bağımlı olarak yazılan dillerde, çalışma zamanında yalnızca türlerden daha fazlasını silmeyi bekleriz, ancak (tür sınıflarında olduğu gibi) bazı örtük olarak çıkartılmış değerlerin silinmeyeceğini sileriz. Örneğin, vReplicate
sayısal argüman genellikle istenen vektörün tipinden etkilenmez, ancak yine de çalışma zamanında bilmemiz gerekir.
Bu tesadüfler artık geçerli olmadığından hangi dil tasarım seçeneklerini gözden geçirmeliyiz? Örneğin, Haskell'in forall x. t
açıkça bir nicelleştiriciyi somutlaştırmanın bir yolu olmadığı doğru mu? Daktilo unifiying ile tahmin x
edemezse t
, ne x
olması gerektiğini söylemenin başka bir yolu yoktur .
Daha geniş anlamda, "tür çıkarımını" ya hepimizin ya da hiçbirimizin sahip olmadığı monolitik bir kavram olarak ele alamayız. Başlangıç olarak, aptal bir makinenin birini tahmin edebilmesini sağlamak için hangi türlerin mevcut olduğunu kısıtlamaya dayanan "genelleme" yönünü (Milner'ın "izin" kuralı) ayırmamız gerekir (Milner'ın "var "kural) kısıtlama çözücünüz kadar etkili. Üst düzey türlerin çıkarımını zorlaştırmasını bekleyebiliriz, ancak iç tür bilgilerinin yayılması oldukça kolay kalacaktır.
Haskell İçin Sonraki Adımlar
Tür ve tür seviyelerinin çok benzer büyüdüğünü görüyoruz (ve zaten GHC'de dahili bir temsili paylaşıyorlar). Onları birleştirebiliriz. * :: *
Yapabilirsek alması eğlenceli olurdu : uzun zaman önce, dibe izin verdiğimizde mantıksal sağlamlığı kaybettik
, ancak tip
sağlamlığı genellikle daha zayıf bir gereksinimdir. Kontrol etmeliyiz. Farklı tür, tür vb. Seviyelere sahip olmamız gerekiyorsa, en azından tür düzeyinde ve üstündeki her şeyin her zaman tanıtılabileceğinden emin olabiliriz. Tür seviyesindeki polimorfizmi yeniden icat etmek yerine, halihazırda sahip olduğumuz polimorfizmi yeniden kullanmak harika olurdu.
Biz basitleştirmek ve izin vererek kısıtlamalar mevcut sistemi genelleme gereken heterojen denklemleri a ~ b
çeşitleri a
ve
b
sözdizimsel aynı olmayan (ama eşit kanıtlanabilir). Bağımlılıkla başa çıkmayı kolaylaştıran eski bir teknik (tezimde geçen yüzyıl). GADT'lerde ifadeler üzerindeki kısıtlamaları ifade edebilir ve böylece neyin tanıtılabileceğine dair kısıtlamaları gevşetebiliriz.
Bağımlı bir fonksiyon tipi getirerek singleton yapımına olan ihtiyacı ortadan kaldırmalıyız pi x :: s -> t
. Bu tipte bir işlev, tür ve terim dillerinin kesişiminde yaşayan herhangi bir tür ifadesine açıkça uygulanabilir (yani, değişkenler, yapıcılar, daha sonra gelecek). İlgili lambda ve uygulama çalışma zamanında silinmez, bu yüzden yazabilirizs
vReplicate :: pi n :: Nat -> x -> Vec n x
vReplicate Z x = VNil
vReplicate (S n) x = VCons x (vReplicate n x)
değiştirmeden Nat
tarafından Natty
. Etki alanı pi
herhangi bir promotable tip olabilir, bu nedenle GADT'ler yükseltilebiliyorsa, bağımlı niceleyici diziler (veya de Briuijn'in dediği gibi "teleskoplar") yazabiliriz
pi n :: Nat -> pi xs :: Vec n x -> ...
ihtiyacımız olan uzunlukta.
Bu adımların amacı , zayıf araçlar ve karmaşık kodlamalar yapmak yerine doğrudan daha genel araçlarla çalışarak karmaşıklığı ortadan kaldırmaktır . Mevcut kısmi satın alma, Haskell'in bağımlı türlerinin faydalarını olması gerekenden daha pahalı hale getiriyor.
Çok zor?
Bağımlı tipler birçok insanı gerginleştirir. Beni sinirlendiriyorlar, ama gergin olmayı seviyorum, ya da en azından gergin olmamayı zor buluyorum. Ancak konunun etrafında böyle bir cehalet sisi olması yardımcı olmuyor. Bazıları hepimizin hala öğrenecek çok şeyi olması nedeniyle. Ancak daha az radikal yaklaşımların savunucularının, gerçeklerin tamamen onlarla olduğundan emin olmadan bağımlı türlerin korkusunu engellediği bilinmektedir. İsimleri adlandırmayacağım. Bu "kararsız daktilo denetimi", "Tamamlanmayan tur", "faz ayrımı yok", "tip silme yok", "her yerde kanıt", vb.
Bağımlı olarak yazılan programların her zaman doğru olduğu kesinlikle doğru değildir. Kişi, programlarının temel hijyenini geliştirebilir, tam bir spesifikasyona gitmeden türlerde ek değişmezleri zorlayabilir. Bu yöndeki küçük adımlar, çoğu zaman ek kanıt yükümlülüğü olmaksızın veya daha azıyla çok daha güçlü garantiler sağlar. Bağımlı olarak yazılan programların kaçınılmaz olarak kanıtlarla dolu olduğu doğru değildir , aslında genellikle tanımlarımı sorgulamak için kodumda herhangi bir kanıtın varlığını alırım .
Çünkü artikülasyondaki herhangi bir artışta olduğu gibi, faul yeni şeyler de adil olarak söyleyebiliriz. Örneğin, ikili arama ağaçlarını tanımlamak için birçok crummy yol vardır, ancak bu iyi bir yol olmadığı anlamına gelmez . Egoyu kabul etmeye zorlasa bile, kötü deneyimlerin iyileştirilemeyeceğini varsaymamak önemlidir. Bağımlı tanımların tasarımı öğrenmeyi gerektiren yeni bir beceridir ve Haskell programcısı olmak sizi otomatik olarak uzman yapmaz! Ve bazı programlar kötü olsa bile, neden başkalarına adil olma özgürlüğünü inkar edesiniz ki?
Neden Haskell ile Rahatsız Etmeliyim?
Bağımlı tiplerden gerçekten hoşlanıyorum, ancak bilgisayar korsanlığı projelerimin çoğu hala Haskell'de. Neden? Haskell'in tip sınıfları vardır. Haskell'in faydalı kütüphaneleri vardır. Haskell, efektlerle programlamanın uygulanabilir (ideal olmaktan uzak olsa da) tedavisine sahiptir. Haskell endüstriyel bir güç derleyicisine sahiptir. Bağımlı olarak yazılan diller, büyüyen topluluk ve altyapı konusunda çok daha erken bir aşamadadır, ancak oraya ulaşacağız, örneğin metaprogramlama ve veri türü jenerikleri gibi mümkün olan şeylerde gerçek bir nesil değişimi. Ancak, sadece mevcut dil neslini ileriye doğru iterek kazanılacak çok fayda olduğunu görmek için insanların Haskell'in bağımlı türlere yönelik adımlarının bir sonucu olarak neler yaptığını incelemelisiniz.