@ KarlBielefeldt के उत्तर पर विस्तार करने के लिए, यहाँ एक पूरा उदाहरण दिया गया है कि वैक्टर को कैसे लागू किया जाए - हास्केल में एक सांख्यिकीय रूप से ज्ञात संख्या के साथ सूची। अपनी टोपी पर पकड़ ...
{-# 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
जैसा कि आप LANGUAGE
निर्देशों की लंबी सूची से देख सकते हैं , यह केवल GHC के हाल के संस्करण के साथ काम करेगा।
हमें टाइप सिस्टम के भीतर लंबाई का प्रतिनिधित्व करने का एक तरीका चाहिए। परिभाषा के अनुसार, एक प्राकृतिक संख्या या तो शून्य है ( Z
) या यह किसी अन्य प्राकृतिक संख्या ( S n
) का उत्तराधिकारी है । इसलिए, उदाहरण के लिए, संख्या 3 लिखी जाएगी S (S (S Z))
।
data Nat = Z | S Nat
साथ DataKinds विस्तार , इस data
घोषणा प्रस्तुत किया एक तरह कहा जाता है Nat
और दो प्रकार के निर्माताओं कहा जाता है S
और Z
- दूसरे शब्दों में हम प्रकार स्तरीय प्राकृतिक संख्या। ध्यान दें कि प्रकार S
और Z
किसी भी सदस्य मान नहीं हैं - केवल प्रकार के प्रकार *
मूल्यों द्वारा बसे हुए हैं।
अब हम एक ज्ञात लंबाई के साथ वैक्टर का प्रतिनिधित्व करने वाले जीएडीटी का परिचय देते हैं । इस तरह के हस्ताक्षर पर ध्यान दें: इसकी लंबाई का प्रतिनिधित्व करने के Vec
लिए एक प्रकार काNat
(यानी Z
या एक S
प्रकार) की आवश्यकता होती है ।
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)
वैक्टर की परिभाषा लिंक्ड सूची के समान है, इसकी लंबाई के बारे में कुछ अतिरिक्त प्रकार-स्तर की जानकारी के साथ। एक वेक्टर या तो VNil
उस स्थिति में होता है, जिसकी लंबाई Z
(ero) होती है, या यह VCons
किसी अन्य वेक्टर में एक आइटम जोड़ने वाली कोशिका होती है, इस स्थिति में इसकी लंबाई अन्य वेक्टर ( S n
) से एक अधिक होती है । ध्यान दें कि कोई भी प्रकार का कोई तर्क नहीं है n
। यह लंबाई को ट्रैक करने के लिए संकलन समय पर उपयोग किया जाता है, और कंपाइलर मशीन कोड उत्पन्न करने से पहले मिटा दिया जाएगा।
हमने एक वेक्टर प्रकार को परिभाषित किया है, जो इसकी लंबाई के स्थिर ज्ञान के आसपास है। आइए क्वेरी के कुछ प्रकारों को Vec
महसूस करें कि वे कैसे काम करते हैं:
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
डॉट उत्पाद केवल एक सूची के लिए ही आगे बढ़ता है:
-- 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' कार्यों के वेक्टर को तर्कों के वेक्टर में लागू करता है, वह Vec
अनुप्रयोगात्मक है <*>
; मैंने इसे एक Applicative
उदाहरण में नहीं रखा क्योंकि यह गड़बड़ है । ध्यान दें कि मैं foldr
कंपाइलर-जनरेट किए गए इंस्टेंस से उपयोग कर रहा हूं Foldable
।
आइये इसे आजमाते हैं:
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
महान! जब आप dot
वैक्टर की कोशिश करते हैं, जिसकी लंबाई मैच नहीं करती है, तो आपको एक संकलन-समय त्रुटि मिलती है ।
यहाँ एक समारोह में वैक्टर को एक साथ जोड़ने का प्रयास किया गया है:
-- 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)
आउटपुट वेक्टर की लंबाई दो इनपुट वैक्टर की लंबाई का योग होगी । हमें टाइप चेकर को सिखाना है कि कैसे Nat
एक साथ जोड़ना है । इसके लिए हम एक टाइप-स्तरीय फ़ंक्शन का उपयोग करते हैं :
type family (n :: Nat) :+: (m :: Nat) :: Nat where
Z :+: m = m
(S n) :+: m = S (n :+: m)
यह type family
घोषणा एक परिचय प्रकार पर समारोह कहा जाता है :+:
- दूसरे शब्दों में, यह प्रकार चेकर दो प्राकृतिक संख्याओं का योग की गणना करने के लिए एक नुस्खा है। इसे पुनरावर्ती रूप से परिभाषित किया जाता है - जब भी बायाँ ऑपरेंड Z
ero से अधिक होता है तो हम आउटपुट में जोड़ते हैं और इसे पुनरावर्ती कॉल में एक के बाद एक घटा देते हैं। (यह एक प्रकार का कार्य लिखने के लिए एक अच्छा अभ्यास है जो दो Nat
एस को गुणा करता है ।) अब हम +++
संकलन कर सकते हैं :
infixr 5 +++
(+++) :: Vec n a -> Vec m a -> Vec (n :+: m) a
VNil +++ ys = ys
(VCons x xs) +++ ys = VCons x (concat xs ys)
यहां बताया गया है कि आप इसका उपयोग कैसे करते हैं:
ghci> VCons 1 (VCons 2 VNil) +++ VCons 3 (VCons 4 VNil)
VCons 1 (VCons 2 (VCons 3 (VCons 4 VNil)))
अब तक बहुत सरल है। क्या होगा जब हम संघनन के विपरीत करना चाहते हैं और दो में एक सदिश विभाजन करते हैं? आउटपुट वैक्टर की लंबाई तर्क के रनटाइम मूल्य पर निर्भर करती है। हम कुछ इस तरह लिखना चाहेंगे:
-- 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)
लेकिन दुर्भाग्य से हास्केल हमें ऐसा नहीं करने देंगे। रिटर्न प्रकार में प्रदर्शित होने के तर्क के मूल्य को रखने (इसे आमतौर पर एक आश्रित फ़ंक्शन या पीआई प्रकार कहा जाता है ) के लिए "पूर्ण-स्पेक्ट्रम" निर्भर प्रकारों की आवश्यकता होती है, जबकि केवल हमें पदोन्नत प्रकार के निर्माता प्रदान करते हैं। इसे दूसरे तरीके से रखने के लिए, टाइप करें कन्स्ट्रक्टर्स और वैल्यू लेवल पर न दिखें। हमें एक निश्चित समय के रन-टाइम प्रतिनिधित्व के लिए सिंगलटन मानों के लिए समझौता करना होगा । *n
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)
किसी दिए गए प्रकार n
( प्रकार के साथ Nat
) के लिए, ठीक एक प्रकार का शब्द है Natty n
। हम एक रन-टाइम गवाह के रूप में सिंगलटन मूल्य का उपयोग कर सकते हैं n
: Natty
इसके बारे में हमें सिखाता है n
और इसके विपरीत।
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)
आइए इसे एक स्पिन के लिए लें:
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)
पहले उदाहरण में, हमने स्थिति 2 पर तीन-तत्व वेक्टर को सफलतापूर्वक विभाजित किया; तब हमें एक प्रकार की त्रुटि हुई जब हमने एक वेक्टर को अंत में एक स्थिति में विभाजित करने का प्रयास किया। सिंगलेट्स एक प्रकार हैस्केल में एक मूल्य पर निर्भर प्रकार बनाने के लिए मानक तकनीक है।
* singletons
लाइब्रेरी में Natty
आपके लिए सिंगलटन मान उत्पन्न करने के लिए कुछ टेम्प्लेट हास्केल हेल्पर्स शामिल हैं।
अंतिम उदाहरण। जब आप अपने वेक्टर की आयामीता को सांख्यिकीय रूप से नहीं जानते हैं तो क्या होगा? उदाहरण के लिए, यदि हम सूची के रूप में रन-टाइम डेटा से वेक्टर बनाने की कोशिश कर रहे हैं तो क्या होगा? आपको इनपुट सूची की लंबाई पर निर्भर करने के लिए वेक्टर के प्रकार की आवश्यकता होती है । इसे दूसरे तरीके से रखने के लिए, हम एक वेक्टर बनाने के लिए उपयोग नहीं कर सकते हैं क्योंकि आउटपुट वेक्टर का प्रकार गुना के प्रत्येक पुनरावृत्ति के साथ बदलता है। हमें वेक्टर की लंबाई को संकलक से गुप्त रखने की आवश्यकता है।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
एक अस्तित्वगत प्रकार है : टाइप वैरिएबल डेटा कंस्ट्रक्टर n
के रिटर्न प्रकार में प्रकट नहीं होता है AVec
। हम एक आश्रित जोड़ी का अनुकरण करने के लिए इसका उपयोग कर रहे हैं : fromList
आप वेक्टर की लंबाई को सांख्यिकीय रूप से नहीं बता सकते हैं, लेकिन यह कुछ ऐसा कर सकता है जिससे आप वेक्टर की लंबाई जानने के लिए पैटर्न-मिलान कर सकते हैं - Natty n
टुपल के पहले तत्व में । जैसा कि कॉनर मैकब्राइड ने इसे संबंधित उत्तर में लिखा है, "आप एक चीज को देखते हैं, और ऐसा करने में, दूसरे के बारे में जानें"।
यह अस्तित्व में मात्रात्मक प्रकार के लिए एक सामान्य तकनीक है। क्योंकि आप वास्तव में डेटा के साथ कुछ भी नहीं कर सकते हैं जिसके लिए आप प्रकार नहीं जानते हैं - एक फ़ंक्शन लिखने का प्रयास करें data Something = forall a. Sth a
- अस्तित्व अक्सर GADT साक्ष्य के साथ बंडल हो जाते हैं जो आपको पैटर्न-मिलान परीक्षण करके मूल प्रकार को पुनर्प्राप्त करने की अनुमति देता है। अस्तित्व के लिए अन्य सामान्य पैटर्न में आपके प्रकार ( data AWayToGetTo b = forall a. HeresHow a (a -> b)
) को संसाधित करने के लिए पैकेजिंग फ़ंक्शंस शामिल हैं जो प्रथम श्रेणी के मॉड्यूल करने का एक अच्छा तरीका है, या बिल्डिंग-इन एक प्रकार का क्लास शब्दकोश ( data AnOrd = forall a. Ord a => AnOrd a
) है जो उप-प्रकार के बहुरूपता का अनुकरण करने में मदद कर सकता है।
ghci> fromList [1,2,3]
AVec (Sy (Sy (Sy Zy))) (VCons 1 (VCons 2 (VCons 3 Nil)))
जब भी डेटा के स्थिर गुण संकलन समय पर उपलब्ध नहीं हैं, तो निर्भर जोड़े उपयोगी होते हैं। यहाँ filter
वैक्टर के लिए है:
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)
करने के लिए dot
दो AVec
रों, हम GHC को साबित करने के लिए कि उनके लंबाई बराबर हैं की जरूरत है। Data.Type.Equality
एक जीएडीटी को परिभाषित करता है जिसे केवल तब बनाया जा सकता है जब उसके प्रकार के तर्क समान हों:
data (a :: k) :~: (b :: k) where
Refl :: a :~: a -- short for 'reflexivity'
जब आप पैटर्न-मैच करते हैं Refl
, तो जीएचसी जानता है a ~ b
। इस प्रकार के साथ काम करने में आपकी सहायता के लिए कुछ कार्य भी हैं: हम gcastWith
समतुल्य प्रकारों के बीच परिवर्तित करने के लिए उपयोग कर रहे हैं, और TestEquality
यह निर्धारित करने के लिए कि क्या दो Natty
एस समान हैं।
दो Natty
एस की समानता का परीक्षण करने के लिए , हमें इस तथ्य का उपयोग करने की आवश्यकता है कि यदि दो संख्याएं समान हैं, तो उनके उत्तराधिकारी भी समान हैं ( :~:
यह अधिक बधाई है S
):
congSuc :: (n :~: m) -> (S n :~: S m)
congSuc Refl = Refl
Refl
बाएं हाथ की तरफ पैटर्न मिलान GHC को बताता है n ~ m
। उस ज्ञान के साथ, यह तुच्छ है S n ~ S m
, इसलिए जीएचसी हमें एक नया Refl
अधिकार वापस लौटा देता है।
अब हम TestEquality
सीधी पुनरावृत्ति द्वारा एक उदाहरण लिख सकते हैं । यदि दोनों संख्याएँ शून्य हैं, तो वे समान हैं। यदि दोनों संख्याओं में पूर्ववर्ती हैं, तो वे समान हैं यदि पूर्ववर्ती समान हैं। (यदि वे समान नहीं हैं, तो वापस लौटें 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
अब हम टुकड़ों को अज्ञात लंबाई के dot
जोड़े के साथ जोड़ सकते हैं AVec
।
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)
सबसे पहले, AVec
वैक्टर की लंबाई के रनटाइम प्रतिनिधित्व को खींचने के लिए कंस्ट्रक्टर पर पैटर्न मैच । अब testEquality
यह निर्धारित करने के लिए उपयोग करें कि क्या वे लंबाई समान हैं। अगर वे हैं, हम करेंगे Just Refl
; gcastWith
उस समानता प्रमाण का उपयोग करेगा ताकि यह सुनिश्चित किया जा सके कि dot u v
इसकी निहित 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
ध्यान दें कि, इसकी लंबाई का स्थैतिक ज्ञान के बिना एक वेक्टर मूल रूप से एक सूची है, हमने प्रभावी रूप से सूची संस्करण को फिर से लागू किया है dot :: Num a => [a] -> [a] -> Maybe a
। अंतर यह है कि यह संस्करण वैक्टर के संदर्भ में लागू किया गया है dot
। यहां बिंदु है: टाइप करने से पहले परीक्षक आपको कॉल करने की अनुमति देगा dot
, आपने परीक्षण किया होगा कि इनपुट सूचियों की लंबाई समान है या नहीं testEquality
। मैं if
गलत तरीके से दौर पाने के लिए प्रवण हूँ , लेकिन एक भरोसेमंद टाइपिंग सेटिंग में नहीं!
जब आप रनटाइम डेटा के साथ काम कर रहे होते हैं, तो आप अपने सिस्टम के किनारों पर मौजूद संभावित रैपर का उपयोग करने से बच नहीं सकते हैं, लेकिन जब आप इनपुट सत्यापन करते हैं, तो आप अपने सिस्टम के अंदर हर जगह निर्भर प्रकारों का उपयोग कर सकते हैं और अस्तित्व के रैपरों को रख सकते हैं।
चूंकि Nothing
बहुत जानकारीपूर्ण नहीं है, आप आगे एक सबूतdot'
वापस करने के प्रकार को परिष्कृत कर सकते हैं कि विफलता के मामले में लंबाई समान नहीं हैं (सबूत के रूप में कि उनका अंतर 0 नहीं है)। यह Either String a
संभवतः एक त्रुटि संदेश वापस करने के लिए उपयोग करने के मानक हास्केल तकनीक के समान है , हालांकि एक प्रमाण शब्द एक स्ट्रिंग की तुलना में कहीं अधिक कम्प्यूटेशनल रूप से उपयोगी है!
इस प्रकार कुछ तकनीकों का यह सीटी-स्टॉप दौरा समाप्त होता है जो निर्भरता से टाइप किए गए हस्केल प्रोग्रामिंग में सामान्य हैं। हास्केल में इस तरह के प्रकार के साथ प्रोग्रामिंग वास्तव में अच्छा है, लेकिन एक ही समय में वास्तव में अजीब है। अपने सभी आश्रित डेटा को बहुत सारे अभ्यावेदन में तोड़ना, जिसका मतलब एक ही है - Nat
प्रकार, Nat
प्रकार, Natty n
सिंगलटन - बॉयलरप्लेट के साथ मदद करने के लिए कोड-जनरेटर के अस्तित्व के बावजूद, वास्तव में काफी बोझिल है। वर्तमान में सीमाएं भी हैं जो टाइप स्तर तक प्रचारित की जा सकती हैं। यह हालांकि tantalizing है! संभावनाओं पर दिमाग चकराता है - साहित्य में दृढ़ता से टाइप किए गए printf
, डेटाबेस इंटरफेस, यूआई लेआउट आदि के हास्केल में उदाहरण हैं ...
यदि आप कुछ और पढ़ना चाहते हैं, तो निर्भरता से टाइप किए गए हास्केल, दोनों प्रकाशित और स्टैक ओवरफ्लो जैसी साइटों पर साहित्य की बढ़ती हुई बॉडी है। एक अच्छा प्रारंभिक बिंदु है Hasochism कागज - कागज (दूसरों के बीच) यह बहुत ही उदाहरण के माध्यम से चला जाता है, कुछ विस्तार से दर्दनाक भागों पर चर्चा। Singletons कागज (जैसे सिंगलटन मूल्यों की तकनीक को दर्शाता है )। सामान्य रूप से निर्भर टाइपिंग के बारे में अधिक जानकारी के लिए, एजडा ट्यूटोरियल शुरू करने के लिए एक अच्छी जगह है; इसके अलावा, इदरिस विकास में एक भाषा है जो (मोटे तौर पर) "आश्रित प्रकार के साथ हास्केल" के रूप में तैयार की गई है।Natty