หากต้องการขยายคำตอบของ @ 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
อินสแตนซ์เพราะมันได้รับยุ่ง ยังทราบว่าผมใช้จากอินสแตนซ์คอมไพเลอร์ที่สร้างจาก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
s เข้าด้วยกัน สำหรับสิ่งนี้เราใช้ฟังก์ชั่นระดับประเภท :
type family (n :: Nat) :+: (m :: Nat) :: Nat where
Z :+: m = m
(S n) :+: m = S (n :+: m)
type family
การประกาศนี้จะแนะนำฟังก์ชั่นเกี่ยวกับประเภทที่เรียกว่า:+:
- ในคำอื่น ๆ มันเป็นสูตรสำหรับตัวตรวจสอบชนิดในการคำนวณผลรวมของจำนวนธรรมชาติสอง มันถูกกำหนดแบบเรียกซ้ำ - เมื่อใดก็ตามที่ตัวถูกดำเนินการด้านซ้ายมีค่ามากกว่าZ
ero เราจะเพิ่มหนึ่งตัวในเอาต์พุตและลดลงทีละหนึ่งในการเรียกซ้ำ (มันเป็นการออกกำลังกายที่ดีในการเขียนฟังก์ชั่นชนิดซึ่งคูณสองNat
s) ตอนนี้เราสามารถ+++
คอมไพล์ได้:
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'
เมื่อคุณจับคู่รูปแบบRefl
GHC จะรู้สิ่งa ~ b
นั้น นอกจากนี้ยังมีฟังก์ชั่นบางอย่างที่ช่วยให้คุณทำงานกับประเภทนี้: เราจะใช้gcastWith
ในการแปลงระหว่างประเภทที่เทียบเท่าและTestEquality
เพื่อกำหนดว่าสองNatty
s เท่ากันหรือไม่
ในการทดสอบความเท่าเทียมกันของทั้งสอง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