Hasell'de “boyutu bir türe dönüştürmek” mümkün mü?


20

Vektörler ve matrislerle ilgili bir kütüphane yazmak istediğimi varsayalım. Boyutları türlere dönüştürmek mümkün mü, böylece uyumsuz boyutların işlemleri derleme zamanında hata üretir mi?

Örneğin, nokta ürününün imzasının aşağıdaki gibi olmasını istiyorum

dotprod :: Num a, VecDim d => Vector a d -> Vector a d -> a

burada dtür tek bir tamsayı değeri içerir (bu Vektörlerin boyutunu temsil eder).

Bu her tamsayı için ayrı bir tür tanımlayarak (el ile) yapılabilir ve onları denilen bir tür sınıfta gruplandırılması olabilir varsayalım VecDim. Bu tür "üretme" mekanizması var mı?

Ya da belki de aynı şeyi başarmanın daha iyi / daha basit bir yolu?


3
Evet, doğru hatırlıyorsam, Haskell'de bu temel bağımlı yazma düzeyini sağlayacak kütüphaneler var. İyi bir cevap verecek kadar tanıdık değilim.
telastin

Etrafa bakıldığında, tensorkütüphane bunu yinelemeli bir datatanım kullanarak oldukça zarif bir şekilde gerçekleştiriyor gibi görünüyor : noaxiom.org/tensor-documentation#ordinals
mitchus

Bu, hasla değil, scala'dır, ancak eşleşmeyen boyutları ve vektörlerin eşleşmeyen "türleri" önlemek için bağımlı türlerin kullanılmasıyla ilgili bazı kavramları vardır. chrisstucchio.com/blog/2014/…
Daenyth

Yanıtlar:


32

@ 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 LANGUAGEyö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 databir deklarasyon tanıtır tür denir Natve iki tip kurucular olarak adlandırılan Sve Z- diğer bir deyişle elimizdeki tip düzeyinde doğal sayılar. Türlerin Sve Zherhangi 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 Vecbir tür gerektiren türdenNat (yani, bir Zya da Suzunluğ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 VNilbu durumda uzunluğu Z(ero) vardır ya da VConsbaş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. VecNası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 foldrderleyici 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! dotUzunlukları 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 familybildirim , 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 Zero'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 . *DataKindsSZNat

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 Nattybize onun hakkında öğretir nve 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.

* singletonsKü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

AVecBir olan varoluşsal tipi : tip değişken ndönüş türü görünmüyor AVecveri yapıcısı. Bağımlı bir çifti simüle etmek için kullanıyoruz : fromListsize 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 ndemetin 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 aalt 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 filtervektö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 dotiki AVecs, onların uzunlukları eşit olduğunu ghc kanıtlamak gerekir. Data.Type.Equalityyalnı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 ReflGHC bunu bilir a ~ b. Bu türle çalışmanıza yardımcı olacak birkaç işlev de vardır: gcastWitheşdeğer türler arasında dönüştürme ve TestEqualityiki Nattys'nin eşit olup olmadığını belirlemek için kullanacağız .

İki eşitliğini test etmek için Nattys, 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

ReflSol 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 Reflhemen yeni bir tane iade etmemizi sağlar .

Şimdi TestEqualitybasit 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 dotbir çift AVecs 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, AVecvektörlerin uzunluklarının çalışma zamanı temsilini çıkarmak için yapıcı üzerinde desen eşleşmesi . Şimdi testEqualitybu 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ı gcastWithyerine getirerek dot u viyi 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 amuhtemelen 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 Nattipi, Natayni, Natty ntekil - 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


@Benjamin FYI, sonunda Idris bağlantısı kopmuş gibi görünüyor.
Erik Eidt

@ErikEidt Hata! İşaret ettiğiniz için teşekkürler! Ben güncelleyeceğim.
Benjamin Hodgson

14

Buna bağımlı yazma denir . Adı öğrendikten sonra, istediğinizi umduğunuzdan daha fazla bilgi bulabilirsiniz. Ayrıca , yerel olarak kullanan Idris adında ilginç bir haskell benzeri dil var . Yazarı, youtube'da bulabileceğiniz konuyla ilgili birkaç iyi sunum yaptı.


Bu hiç bağımlı değildir. Bağımlı yazma, çalışma süresinde türler hakkında konuşur, ancak türün pişirme boyutlandırması derleme zamanında kolayca yapılabilir.
DeadMG

4
Aksine, ilgili bağımlı yazarak görüşmelere @DeadMG değerlere de derleme zamanında . Türleri de çalışma zamanında yansıma değil, bağımlı yazarak olduğunu. Cevabımdan da görebileceğiniz gibi, tipe pişirme boyutu genel bir boyut için çok kolay değil. (Bunu tanımlayabilirsiniz newtype Vec2 a = V2 (a,a), newtype Vec3 a = V3 (a,a,a)vb. Fakat sorunun sorusu bu değildir.)
Benjamin Hodgson

Değerler yalnızca çalışma zamanında görünür, bu nedenle Durma Sorunu'nu çözmek istemiyorsanız, derleme zamanında değerler hakkında konuşamazsınız. Söylediğim tek şey, C ++ 'da bile boyutsallık üzerine şablon yapabileceğiniz ve iyi çalıştığıdır. Haskell'de bunun karşılığı yok mu?
DeadMG

4
@DeadMG "Tam spektrumlu" bağımlı olarak yazılan diller (Agda gibi) aslında yazı dilinde rastgele terim düzeyinde hesaplamalara izin verir. İşaret ettiğiniz gibi, bu sizi Durdurma Sorunu'nu çözme riskine sokar. En bağımlı tip sistemler, afaik, Turing tamamlanmadan bu sorunla karşılaşırlar . Ben bir C ++ adam değilim ama şablonları kullanarak bağımlı türleri simüle edebilirsiniz beni şaşırtmıyor; şablonlar her türlü yaratıcı şekilde kötüye kullanılabilir.
Benjamin Hodgson

4
@BenjaminHodgson Bir pi türünü taklit edemediğiniz için şablonlarla bağımlı türler yapamazsınız. "Kanonik" bağımlı tip sensin ihtiyaç iddia edebilir gerekir Pi (x : A). Bbir fonksiyondur Aiçin B xnerede xişlevin argümandır. Burada işlevin dönüş türü, bağımsız değişken olarak sağlanan ifadeye bağlıdır . Ancak, tüm bunlar silinebilir, sadece derleme zamanı
Daniel Gratzer
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.