หากต้องการขยายคำตอบของ @ KarlBielefeldt ต่อไปนี้เป็นตัวอย่างที่สมบูรณ์ของวิธีการใช้งานเวกเตอร์ - รายการที่มีจำนวนองค์ประกอบที่รู้จักกันแบบคงที่ - ใน Haskell ยึดมั่นในหมวกของคุณ ...
{-# 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ไม่มีค่าสมาชิกใด ๆ - ประเภทเท่านั้นที่*เป็นที่อยู่อาศัยของค่า
ตอนนี้เราแนะนำGADT ที่เป็นตัวแทนของเวกเตอร์ที่มีความยาวเป็นที่รู้จัก หมายเหตุลายเซ็นชนิด: Vecต้องการชนิดของชนิดNat (เช่น a 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
ผลิตภัณฑ์ dot ดำเนินการเช่นเดียวกับรายการ:
-- 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's applicative <*>; ฉันไม่ได้ใส่ไว้ในApplicativeอินสแตนซ์เพราะมันได้รับยุ่ง ยังทราบว่าผมใช้จากอินสแตนซ์คอมไพเลอร์ที่สร้างจากfoldrFoldable
ลองดูสิ:
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)
ความยาวของเวกเตอร์เอาต์พุตจะเป็นผลรวมของความยาวของเวกเตอร์อินพุตสองตัว เราต้องสอนตัวตรวจสอบชนิดวิธีการเพิ่มNats เข้าด้วยกัน สำหรับสิ่งนี้เราใช้ฟังก์ชั่นระดับประเภท :
type family (n :: Nat) :+: (m :: Nat) :: Nat where
Z :+: m = m
(S n) :+: m = S (n :+: m)
type familyการประกาศนี้จะแนะนำฟังก์ชั่นเกี่ยวกับประเภทที่เรียกว่า:+:- ในคำอื่น ๆ มันเป็นสูตรสำหรับตัวตรวจสอบชนิดในการคำนวณผลรวมของจำนวนธรรมชาติสอง มันถูกกำหนดแบบเรียกซ้ำ - เมื่อใดก็ตามที่ตัวถูกดำเนินการด้านซ้ายมีค่ามากกว่าZero เราจะเพิ่มหนึ่งตัวในเอาต์พุตและลดลงทีละหนึ่งในการเรียกซ้ำ (มันเป็นการออกกำลังกายที่ดีในการเขียนฟังก์ชั่นชนิดซึ่งคูณสองNats) ตอนนี้เราสามารถ+++คอมไพล์ได้:
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)
แต่น่าเสียดายที่ Haskell จะไม่ยอมให้เราทำเช่นนั้น การอนุญาตให้ค่าของnอาร์กิวเมนต์ปรากฏในชนิดส่งคืน (ซึ่งโดยทั่วไปเรียกว่าฟังก์ชันที่ขึ้นต่อกันหรือชนิด pi ) จะต้องใช้ชนิดที่ต้องพึ่งพา "full-spectrum" ในขณะที่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 เป็นเทคนิคมาตรฐานสำหรับการสร้างประเภทขึ้นอยู่กับค่าใน Haskell
* singletonsไลบรารีนี้มีตัวช่วยเทมเพลต Haskell เพื่อสร้างค่าแบบซิงเกิลเช่นเดียว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ในองค์ประกอบแรกของ tuple . ดังที่ Conor McBride กล่าวไว้ในคำตอบที่เกี่ยวข้องว่า "คุณมองสิ่งหนึ่งและในการทำเช่นนั้นเรียนรู้เกี่ยวกับสิ่งอื่น"
นี่เป็นเทคนิคทั่วไปสำหรับประเภทปริมาณที่มีอยู่ เพราะคุณไม่สามารถทำอะไรกับข้อมูลที่คุณไม่รู้ประเภท - ลองเขียนฟังก์ชั่นของdata Something = forall a. Sth a- อัตถิภาวนิยมมักจะมาพร้อมกับหลักฐาน GADT ซึ่งช่วยให้คุณสามารถกู้คืนประเภทเดิมโดยทำการทดสอบการจับคู่รูปแบบ รูปแบบทั่วไปอื่น ๆ สำหรับการดำรงอยู่รวมถึงฟังก์ชั่นการบรรจุเพื่อประมวลผลประเภทของคุณ ( data AWayToGetTo b = forall a. HeresHow a (a -> b)) ซึ่งเป็นวิธีที่เรียบร้อยในการทำโมดูลชั้นหนึ่งหรือการสร้างในพจนานุกรมคลาสประเภท ( data AnOrd = forall a. Ord a => AnOrd a) ซึ่งสามารถช่วยเลียนแบบ polymorphism ย่อย
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กำหนด GADT ซึ่งสามารถสร้างได้ก็ต่อเมื่ออาร์กิวเมนต์ประเภทนั้นเหมือนกัน:
data (a :: k) :~: (b :: k) where
Refl :: a :~: a -- short for 'reflexivity'
เมื่อคุณจับคู่รูปแบบReflGHC จะรู้สิ่งa ~ bนั้น นอกจากนี้ยังมีฟังก์ชั่นบางอย่างที่ช่วยให้คุณทำงานกับประเภทนี้: เราจะใช้gcastWithในการแปลงระหว่างประเภทที่เทียบเท่าและTestEqualityเพื่อกำหนดว่าสองNattys เท่ากันหรือไม่
ในการทดสอบความเท่าเทียมกันของทั้งสองNattyของเรากำลังจะจำเป็นที่จะต้องใช้ประโยชน์จากความจริงที่ว่าถ้าตัวเลขสองมีค่าเท่ากันแล้วสืบทอดนอกจากนี้ยังเท่ากับ ( :~:เป็นสอดคล้องกันมากกว่าS):
congSuc :: (n :~: m) -> (S n :~: S m)
congSuc Refl = Refl
รูปแบบการจับคู่บนReflด้านซ้ายมือช่วยให้ GHC n ~ mรู้ว่า ด้วยความรู้นั้นมันเป็นเรื่องเล็กน้อยS n ~ S mดังนั้น GHC จึงช่วยให้เรากลับมาใหม่ได้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) ในกรณีความล้มเหลว นี่คล้ายกับเทคนิค Haskell มาตรฐานในการใช้Either String aเพื่อส่งคืนข้อความแสดงข้อผิดพลาดแม้ว่าข้อความพิสูจน์จะมีประโยชน์มากกว่าการคำนวณสตริงมาก!
ดังนั้นจะจบการเดินทางด้วยการเป่านกหวีดของเทคนิคบางอย่างที่ใช้กันทั่วไปในการเขียนโปรแกรม Haskell การเขียนโปรแกรมประเภทนี้ใน Haskell นั้นเจ๋งมาก แต่ก็น่าอึดอัดใจในเวลาเดียวกัน การแบ่งข้อมูลที่ต้องพึ่งพาทั้งหมดของคุณให้เป็นตัวแทนจำนวนมากซึ่งหมายถึงสิ่งเดียวกัน - NatชนิดNatชนิดNatty nซิงเกิล - ค่อนข้างยุ่งยากจริงๆแม้จะมีเครื่องกำเนิดรหัสเพื่อช่วยให้สำเร็จรูป นอกจากนี้ยังมีข้อ จำกัด เกี่ยวกับสิ่งที่สามารถเลื่อนระดับประเภท แม้ว่ามันจะยั่วเย้า! ใจกระวนกระวายใจที่เป็นไปได้ - ในวรรณคดีมีตัวอย่างใน Haskell พิมพ์อย่างยิ่งprintfอินเตอร์เฟซฐานข้อมูลเครื่องยนต์เค้าโครง UI ...
หากคุณต้องการอ่านเพิ่มเติมมีวรรณกรรมที่เพิ่มขึ้นเกี่ยวกับ Haskell ที่พิมพ์ได้ทั้งที่เผยแพร่และบนไซต์เช่น Stack Overflow เป็นจุดเริ่มต้นที่ดีคือHasochismกระดาษ - กระดาษผ่านไปเช่นนี้มาก (ผู้อื่น) การอภิปรายในส่วนที่เจ็บปวดในรายละเอียดบาง Singletonsกระดาษแสดงให้เห็นถึงเทคนิคของค่าเดี่ยว (เช่น) สำหรับข้อมูลเพิ่มเติมเกี่ยวกับการพิมพ์แบบพึ่งพาโดยทั่วไปการสอนของAgdaเป็นจุดเริ่มต้นที่ดี นอกจากนี้Idrisยังเป็นภาษาที่ใช้ในการพัฒนา (โดยประมาณ) ที่ออกแบบมาให้เป็น "Haskell ที่ขึ้นกับประเภท"Natty