@ KarlBielefeldt'in cevabını genişletmek için, Haskell'de Vektörlerin - statik olarak bilinen sayıda öğeye sahip listelerin - nasıl uygulanacağına dair tam bir örnek . Şapkana tutun ...
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE ExistentialQuantification #-}
{-# LANGUAGE DeriveFoldable #-}
{-# LANGUAGE DeriveFunctor #-}
{-# LANGUAGE DeriveTraversable #-}
{-# LANGUAGE GADTs #-}
{-# LANGUAGE KindSignatures #-}
{-# LANGUAGE StandaloneDeriving #-}
{-# LANGUAGE TypeOperators #-}
{-# LANGUAGE TypeFamilies #-}
import Prelude hiding (foldr, zipWith)
import qualified Prelude
import Data.Type.Equality
import Data.Foldable
import Data.Traversable
Uzun LANGUAGE
yönergeler listesinden görebileceğiniz gibi , bu yalnızca GHC'nin son sürümüyle çalışır.
Tür sistemi içinde uzunlukları temsil etmenin bir yoluna ihtiyacımız var. Tanım olarak, doğal bir sayı ya sıfır ( Z
) ya da başka bir doğal sayının ( S n
) ardılıdır . Örneğin, 3 sayısı yazılacaktı S (S (S Z))
.
data Nat = Z | S Nat
İle DataKinds uzantısı , bu data
bir deklarasyon tanıtır tür denir Nat
ve iki tip kurucular olarak adlandırılan S
ve Z
- diğer bir deyişle elimizdeki tip düzeyinde doğal sayılar. Türlerin S
ve Z
herhangi bir üye değerinin bulunmadığına dikkat edin - yalnızca tür türlerinde *
değerler bulunur.
Şimdi bilinen uzunluğu olan vektörleri temsil eden bir GADT'yi sunuyoruz . : Tür imza Not Vec
bir tür gerektiren türdenNat
(yani, bir Z
ya da S
uzunluğunu temsil etmek tipi).
data Vec :: Nat -> * -> * where
VNil :: Vec Z a
VCons :: a -> Vec n a -> Vec (S n) a
deriving instance (Show a) => Show (Vec n a)
deriving instance Functor (Vec n)
deriving instance Foldable (Vec n)
deriving instance Traversable (Vec n)
Vektörlerin tanımı, uzunlukları hakkında bazı ekstra tip seviyesi bilgileriyle bağlantılı listelerinkine benzer. Bir vektör ya da VNil
bu durumda uzunluğu Z
(ero) vardır ya da VCons
başka bir vektöre bir öğe ekleyen bir hücredir, bu durumda uzunluğu diğer vektörden ( S n
) daha fazladır . Herhangi bir tür yapıcı argümanı olmadığını unutmayın n
. Sadece uzunlukları izlemek için derleme zamanında kullanılır ve derleyici makine kodu üretmeden önce silinir.
Uzunluğunun statik bilgisini taşıyan bir vektör türü tanımladık. Vec
Nasıl çalıştıklarına dair fikir edinmek için birkaç s türünü sorgulayalım :
ghci> :t (VCons 'a' (VCons 'b' VNil))
(VCons 'a' (VCons 'b' VNil)) :: Vec ('S ('S 'Z)) Char -- (S (S Z)) means 2
ghci> :t (VCons 13 (VCons 11 (VCons 3 VNil)))
(VCons 13 (VCons 11 (VCons 3 VNil))) :: Num a => Vec ('S ('S ('S 'Z))) a -- (S (S (S Z))) means 3
Nokta ürün, bir liste için olduğu gibi ilerler:
-- note that the two Vec arguments are declared to have the same length
vap :: Vec n (a -> b) -> Vec n a -> Vec n b
vap VNil VNil = VNil
vap (VCons f fs) (VCons x xs) = VCons (f x) (vap fs xs)
zipWith :: (a -> b -> c) -> Vec n a -> Vec n b -> Vec n c
zipWith f xs ys = fmap f xs `vap` ys
dot :: Num a => Vec n a -> Vec n a -> a
dot xs ys = foldr (+) 0 $ zipWith (*) xs ys
vap
'zippily' bir argüman vektörüne bir fonksiyon vektörü uygulayan, 'uygulanabilir Vec
' <*>
; Bir Applicative
örneğe koymadım çünkü dağınık oluyor . Ayrıca foldr
derleyici tarafından oluşturulan örneğini kullandığımı unutmayın Foldable
.
Hadi deneyelim:
ghci> let v1 = VCons 2 (VCons 1 VNil)
ghci> let v2 = VCons 4 (VCons 5 VNil)
ghci> v1 `dot` v2
13
ghci> let v3 = VCons 8 (VCons 6 (VCons 1 VNil))
ghci> v1 `dot` v3
<interactive>:20:10:
Couldn't match type ‘'S 'Z’ with ‘'Z’
Expected type: Vec ('S ('S 'Z)) a
Actual type: Vec ('S ('S ('S 'Z))) a
In the second argument of ‘dot’, namely ‘v3’
In the expression: v1 `dot` v3
Harika! dot
Uzunlukları eşleşmeyen vektörleri oluşturmaya çalıştığınızda derleme zamanı hatası alıyorsunuz .
Vektörleri bir araya getirmek için bir işlev denemesi:
-- This won't compile because the type checker can't deduce the length of the returned vector
-- VNil +++ ys = ys
-- (VCons x xs) +++ ys = VCons x (concat xs ys)
Çıkış vektörünün uzunluğu, iki giriş vektörünün uzunluklarının toplamı olacaktır . Tip denetleyicisine nasıl ekleneceğini öğretmeliyiz Nat
. Bunun için bir tür düzeyinde işlev kullanıyoruz :
type family (n :: Nat) :+: (m :: Nat) :: Nat where
Z :+: m = m
(S n) :+: m = S (n :+: m)
Bu type family
bildirim , denilen türler üzerinde bir işlev sunar:+:
- diğer bir deyişle, tür denetleyicisinin iki doğal sayının toplamını hesaplaması için bir reçetedir. Yinelemeli olarak tanımlanır - sol işlenen Z
ero'dan büyük olduğunda , çıktıya bir tane ekler ve yinelemeli çağrıda bir tane azaltırız. (İki saniyeyi çarpan bir tip fonksiyonu yazmak iyi bir alıştırmadır Nat
.) Şimdi +++
derleme yapabiliriz :
infixr 5 +++
(+++) :: Vec n a -> Vec m a -> Vec (n :+: m) a
VNil +++ ys = ys
(VCons x xs) +++ ys = VCons x (concat xs ys)
İşte nasıl kullanacağınız:
ghci> VCons 1 (VCons 2 VNil) +++ VCons 3 (VCons 4 VNil)
VCons 1 (VCons 2 (VCons 3 (VCons 4 VNil)))
Şimdiye kadar çok basit. Birleştirme işleminin tersini yapmak ve bir vektörü ikiye bölmek istediğimizde ne olur? Çıktı vektörlerinin uzunlukları, bağımsız değişkenlerin çalışma zamanı değerine bağlıdır. Bunun gibi bir şey yazmak istiyoruz:
-- this won't work because there aren't any values of type `S` and `Z`
-- split :: (n :: Nat) -> Vec (n :+: m) a -> (Vec n a, Vec m a)
ama ne yazık ki Haskell bunu yapmamıza izin vermiyor. Bağımsız değişkenin değerinin dönüş türünden
görünmesine izin vermek (buna genellikle bağımlı işlev veya pi türü denir ) "tam spektrumlu" bağımlı türler gerektirir, oysa bize yalnızca yükseltilmiş tür yapıcıları verir. Başka bir deyişle, tür yapıcılar ve değer düzeyinde görünmüyor. Belli bir çalışma zamanı temsili için tekil değerleri belirlememiz gerekir . *DataKinds
S
Z
Nat
data Natty (n :: Nat) where
Zy :: Natty Z -- pronounced 'zed-y'
Sy :: Natty n -> Natty (S n) -- pronounced 'ess-y'
deriving instance Show (Natty n)
Belirli bir tür için n
(tür ile Nat
), tam olarak bir tür terim vardır Natty n
. Singleton değerini bir çalışma zamanı tanığı olarak kullanabiliriz n
: bir öğretmeyi öğrenmek Natty
bize onun hakkında öğretir n
ve bunun tersi de geçerlidir.
split :: Natty n ->
Vec (n :+: m) a -> -- the input Vec has to be at least as long as the input Natty
(Vec n a, Vec m a)
split Zy xs = (Nil, xs)
split (Sy n) (Cons x xs) = let (ys, zs) = split n xs
in (Cons x ys, zs)
Bir dönüş için alalım:
ghci> split (Sy (Sy Zy)) (VCons 1 (VCons 2 (VCons 3 VNil)))
(VCons 1 (VCons 2 VNil), VCons 3 VNil)
ghci> split (Sy (Sy Zy)) (VCons 3 VNil)
<interactive>:116:21:
Couldn't match type ‘'S ('Z :+: m)’ with ‘'Z’
Expected type: Vec ('S ('S 'Z) :+: m) a
Actual type: Vec ('S 'Z) a
Relevant bindings include
it :: (Vec ('S ('S 'Z)) a, Vec m a) (bound at <interactive>:116:1)
In the second argument of ‘split’, namely ‘(VCons 3 VNil)’
In the expression: split (Sy (Sy Zy)) (VCons 3 VNil)
İlk örnekte, üç elementli bir vektörü 2. pozisyonda başarıyla böldük; bir vektörün sonunu geçecek bir konuma bölmeye çalıştığımızda bir tür hatası aldık. Tektonlar, Haskell'deki bir değere bağlı bir tür yapmak için standart tekniktir.
* singletons
KütüphaneNatty
sizin gibi singleton değerleri üretmek için bazı Şablon Haskell yardımcıları içerir .
Son örnek. Vektörünüzün boyutsallığını statik olarak bilmediğinizde ne olur? Örneğin, çalışma zamanı verilerinden bir liste şeklinde bir vektör oluşturmaya çalışıyorsak ne olur? Giriş listesinin uzunluğuna bağlı olarak vektörün tipine ihtiyacınız vardır . Başka bir deyişle, bir vektör oluşturmak için kullanamayız çünkü çıktı vektörünün tipi katlamanın her yinelemesiyle değişir. Vektörün uzunluğunu derleyiciden bir sır olarak tutmamız gerekiyor.foldr VCons VNil
data AVec a = forall n. AVec (Natty n) (Vec n a)
deriving instance (Show a) => Show (AVec a)
fromList :: [a] -> AVec a
fromList = Prelude.foldr cons nil
where cons x (AVec n xs) = AVec (Sy n) (VCons x xs)
nil = AVec Zy VNil
AVec
Bir olan varoluşsal tipi : tip değişken n
dönüş türü görünmüyor AVec
veri yapıcısı. Bağımlı bir çifti simüle etmek için kullanıyoruz : fromList
size vektörün uzunluğunu statik olarak söyleyemiyoruz, ancak vektörün uzunluğunu öğrenmek için desen eşleştirebileceğiniz bir şey döndürebilir - Natty n
demetin ilk öğesinde . Conor McBride bunu ilgili bir cevaba koyarken , "Bir şeye bakıyorsunuz ve bunu yaparken başka bir şeyi öğreniyorsunuz".
Bu, varoluşsal olarak nicelenen tipler için yaygın bir tekniktir. Aslında türünü bilmediğiniz verilerle hiçbir şey yapamazsınız - bir işlev yazmayı deneyin data Something = forall a. Sth a
- varoluşlar genellikle desen eşleştirme testleri gerçekleştirerek orijinal türü kurtarmanızı sağlayan GADT kanıtlarıyla birlikte gelir. Varoluşlar için diğer yaygın modeller, data AWayToGetTo b = forall a. HeresHow a (a -> b)
birinci sınıf modülleri yapmanın düzgün bir yolu olan tipinizi ( ) işlemek için paketleme işlevlerini veya data AnOrd = forall a. Ord a => AnOrd a
alt tip polimorfizmini taklit etmeye yardımcı olabilecek bir tip sınıfı sözlüğü ( ) oluşturmaktır.
ghci> fromList [1,2,3]
AVec (Sy (Sy (Sy Zy))) (VCons 1 (VCons 2 (VCons 3 Nil)))
Bağımlı çiftler, verilerin statik özellikleri derleme zamanında bulunmayan dinamik bilgilere bağlı olduğunda yararlıdır. İşte filter
vektörler için:
filter :: (a -> Bool) -> Vec n a -> AVec a
filter f = foldr (\x (AVec n xs) -> if f x
then AVec (Sy n) (VCons x xs)
else AVec n xs) (AVec Zy VNil)
To dot
iki AVec
s, onların uzunlukları eşit olduğunu ghc kanıtlamak gerekir. Data.Type.Equality
yalnızca tür bağımsız değişkenleri aynı olduğunda oluşturulabilen bir GADT tanımlar:
data (a :: k) :~: (b :: k) where
Refl :: a :~: a -- short for 'reflexivity'
Desen eşleştirdiğinizde Refl
GHC bunu bilir a ~ b
. Bu türle çalışmanıza yardımcı olacak birkaç işlev de vardır: gcastWith
eşdeğer türler arasında dönüştürme ve TestEquality
iki Natty
s'nin eşit olup olmadığını belirlemek için kullanacağız .
İki eşitliğini test etmek için Natty
s, biz iki sayı eşitse, o zaman onların ardılları da eşit olduğu gerçeğinin yapmak kullanımına ihtiyaç gidiyoruz ( :~:
olan uyumlu üzerinde S
):
congSuc :: (n :~: m) -> (S n :~: S m)
congSuc Refl = Refl
Refl
Sol taraftaki desen eşleşmesi GHC'nin bunu bilmesini sağlar n ~ m
. Bu bilgi ile, önemsizdir S n ~ S m
, bu yüzden GHC Refl
hemen yeni bir tane iade etmemizi sağlar .
Şimdi TestEquality
basit bir özyineleme ile bir örnek yazabiliriz . Her iki sayı da sıfırsa, eşittir. Her iki sayının da öncülleri varsa, öncüllerin eşit olması durumunda eşittirler. (Eşit değillerse, geri dönün Nothing
.)
instance TestEquality Natty where
-- testEquality :: Natty n -> Natty m -> Maybe (n :~: m)
testEquality Zy Zy = Just Refl
testEquality (Sy n) (Sy m) = fmap congSuc (testEquality n m) -- check whether the predecessors are equal, then make use of congruence
testEquality Zy _ = Nothing
testEquality _ Zy = Nothing
Şimdi parçaları , uzunluğu bilinmeyen dot
bir çift AVec
s ile bir araya getirebiliriz .
dot' :: Num a => AVec a -> AVec a -> Maybe a
dot' (AVec n u) (AVec m v) = fmap (\proof -> gcastWith proof (dot u v)) (testEquality n m)
İlk olarak, AVec
vektörlerin uzunluklarının çalışma zamanı temsilini çıkarmak için yapıcı üzerinde desen eşleşmesi . Şimdi testEquality
bu uzunlukların eşit olup olmadığını belirlemek için kullanın . Eğer öyleyse, sahip olacağız Just Refl
; örtük varsayımını gcastWith
yerine getirerek dot u v
iyi yazıldığından emin olmak için bu eşitlik kanıtını kullanacaktır n ~ m
.
ghci> let v1 = fromList [1,2,3]
ghci> let v2 = fromList [4,5,6]
ghci> let v3 = fromList [7,8]
ghci> dot' v1 v2
Just 32
ghci> dot' v1 v3
Nothing -- they weren't the same length
Uzunluğunu statik olarak bilmeyen bir vektör temel olarak bir liste olduğundan, listesinin etkin bir şekilde yeniden uygulandığını unutmayın dot :: Num a => [a] -> [a] -> Maybe a
. Fark, bu versiyonun vektörler açısından uygulanmasıdır dot
. İşte nokta: tip denetleyicisi aramak sağlayacak önce dot
, sen test olmalı giriş listeleri aynı uzunlukta kullanarak olup olmadığını testEquality
. if
-Durumları yanlış bir şekilde elde etmeye eğilimliyim , ancak bağımlı tipte bir ortamda değil!
Çalışma zamanı verileriyle uğraşırken sisteminizin kenarlarında varolan sarmalayıcıları kullanmaktan kaçınamazsınız, ancak giriş doğrulaması yaparken sisteminizin her yerinde bağımlı türleri kullanabilir ve varolan sarmalayıcıları kenarlarda tutabilirsiniz.
Yana Nothing
çok bilgilendirici değil, daha fazla türünü rafine olabilir dot'
dönmek için uzunlukları eşit olmadığı bir kanıt (kendi fark 0 olmadığını kanıtlar formunda) arızası durumunda. Bu, Either String a
muhtemelen bir hata mesajı döndürmek için kullanılan standart Haskell tekniğine oldukça benzer , ancak bir kanıt terimi bir dizeden çok daha hesaplı olarak yararlıdır!
Böylece, bağımlı tip Haskell programlamasında yaygın olan bazı tekniklerin bu ıslık-durdurma turu sona erer. Haskell'de bunun gibi türlerle programlamak gerçekten harika, ama aynı zamanda gerçekten garip. - Aynı şeyi ifade temsiller sürü halinde tüm bağımlı verileri Breaking Nat
tipi, Nat
ayni, Natty n
tekil - Demirbaş ile yardıma kod jeneratörler varlığına rağmen, oldukça zahmetlidir gerçekten. Halen, tip seviyesine neyin yükseltilebileceği konusunda da sınırlamalar bulunmaktadır. Yine de cezbedici! Zihin olasılıklarla boğuşuyor - literatürde Haskell'de güçlü tip printf
, veritabanı arayüzleri, UI düzen motorları örnekleri var ...
Biraz daha okumak isterseniz, hem yayınlanmış hem de Stack Overflow gibi sitelerde bağımlı olarak yazılan Haskell hakkında büyüyen bir literatür var. İyi bir başlangıç noktası Hasochism kağıt - kağıt biraz detaylı olarak ağrılı parça tartışırken, (diğerleri arasında) bu çok örnekte geçer. Singletons kağıt (örneğin, tekil değerlerin teknik gösterilmiştir ). Genel olarak bağımlı yazma hakkında daha fazla bilgi için Agda öğreticisi iyi bir başlangıç noktasıdır ; ayrıca, İdris (kabaca) "bağımlı türlere sahip Haskell" olarak tasarlanmış bir dildir.Natty