Để mở rộng câu trả lời của @ KarlBielefeldt, đây là một ví dụ đầy đủ về cách triển khai vectơ - liệt kê một số phần tử được biết đến tĩnh - trong Haskell. Giữ lấy chiếc mũ của bạn...
{-# 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
Như bạn có thể thấy trong danh sách dài các LANGUAGE
chỉ thị, điều này sẽ chỉ hoạt động với một phiên bản GHC gần đây.
Chúng ta cần một cách biểu diễn độ dài trong hệ thống loại. Theo định nghĩa, một số tự nhiên là zero ( Z
) hoặc nó là sự kế thừa của một số số tự nhiên khác ( S n
). Vì vậy, ví dụ, số 3 sẽ được viết S (S (S Z))
.
data Nat = Z | S Nat
Với phần mở rộng DataKinds , data
khai báo này giới thiệu một loại hàm tạo được gọi Nat
và hai loại được gọi S
và Z
- nói cách khác, chúng ta có các số tự nhiên cấp loại. Lưu ý rằng các loại S
và Z
không có bất kỳ giá trị thành viên nào - chỉ các loại loại *
được cư trú bởi các giá trị.
Bây giờ chúng tôi giới thiệu một GADT đại diện cho các vectơ có độ dài đã biết. Lưu ý chữ ký loại: Vec
yêu cầu một loạiNat
(tức là một Z
hoặc một S
loại) để thể hiện độ dài của nó.
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)
Định nghĩa của vectơ tương tự như các danh sách được liên kết, với một số thông tin cấp độ loại thêm về độ dài của nó. Một vectơ là một trong hai VNil
trường hợp nó có độ dài Z
(ero) hoặc là một VCons
ô thêm một mục vào một vectơ khác, trong trường hợp đó độ dài của nó là nhiều hơn một vectơ khác ( S n
). Lưu ý rằng không có đối số constructor của loại n
. Nó chỉ được sử dụng tại thời gian biên dịch để theo dõi độ dài và sẽ bị xóa trước khi trình biên dịch tạo mã máy.
Chúng tôi đã định nghĩa một loại vectơ mang theo kiến thức tĩnh về chiều dài của nó. Hãy truy vấn loại của một vài Vec
giây để cảm nhận về cách chúng hoạt động:
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
Sản phẩm chấm tiến hành giống như trong danh sách:
-- 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
, mà 'zippily' áp dụng một vectơ các hàm cho một vectơ đối số, là Vec
ứng dụng <*>
; Tôi đã không đặt nó trong một Applicative
ví dụ vì nó trở nên lộn xộn . Cũng lưu ý rằng tôi đang sử dụng foldr
từ phiên bản do trình biên dịch tạo ra Foldable
.
Hãy thử xem:
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
Tuyệt quá! Bạn gặp lỗi thời gian biên dịch khi bạn cố gắng dot
vectơ có độ dài không khớp.
Đây là một nỗ lực tại một hàm để ghép các vectơ lại với nhau:
-- 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)
Độ dài của vectơ đầu ra sẽ là tổng độ dài của hai vectơ đầu vào. Chúng ta cần dạy trình kiểm tra kiểu cách thêm Nat
s với nhau. Đối với điều này, chúng tôi sử dụng một hàm cấp độ :
type family (n :: Nat) :+: (m :: Nat) :: Nat where
Z :+: m = m
(S n) :+: m = S (n :+: m)
type family
Tuyên bố này giới thiệu một hàm trên các loại được gọi :+:
- nói cách khác, đó là một công thức cho trình kiểm tra loại để tính tổng của hai số tự nhiên. Nó được định nghĩa đệ quy - bất cứ khi nào toán hạng bên trái lớn hơn Z
ero, chúng ta sẽ thêm một vào đầu ra và giảm nó một lần trong lệnh gọi đệ quy. (Đây là một bài tập tốt để viết một hàm kiểu nhân hai Nat
s.) Bây giờ chúng ta có thể +++
biên dịch:
infixr 5 +++
(+++) :: Vec n a -> Vec m a -> Vec (n :+: m) a
VNil +++ ys = ys
(VCons x xs) +++ ys = VCons x (concat xs ys)
Đây là cách bạn sử dụng nó:
ghci> VCons 1 (VCons 2 VNil) +++ VCons 3 (VCons 4 VNil)
VCons 1 (VCons 2 (VCons 3 (VCons 4 VNil)))
Cho đến nay thật đơn giản. Thế còn khi chúng ta muốn làm ngược lại với ghép và chia một vectơ thành hai thì sao? Độ dài của vectơ đầu ra phụ thuộc vào giá trị thời gian chạy của các đối số. Chúng tôi muốn viết một cái gì đó như thế này:
-- 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)
nhưng tiếc là Haskell sẽ không cho chúng tôi làm điều đó. Việc cho phép giá trị của n
đối số xuất hiện trong kiểu trả về (cái này thường được gọi là hàm phụ thuộc hoặc loại pi ) sẽ yêu cầu các loại phụ thuộc "toàn phổ", trong khi DataKinds
chỉ cung cấp cho chúng ta các hàm tạo kiểu được quảng bá. Nói cách khác, các hàm tạo kiểu S
và Z
không xuất hiện ở mức giá trị. Chúng tôi sẽ phải giải quyết các giá trị đơn lẻ cho một đại diện trong thời gian nhất định 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)
Đối với một loại nhất định n
(có loại Nat
), có chính xác một thuật ngữ của loại Natty n
. Chúng ta có thể sử dụng giá trị singleton như một nhân chứng thời gian để n
: học về một người Natty
dạy chúng ta về nó n
và ngược lại.
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)
Chúng ta hãy quay nó:
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)
Trong ví dụ đầu tiên, chúng tôi chia thành công một vectơ ba phần tử ở vị trí 2; sau đó chúng tôi đã gặp một lỗi loại khi chúng tôi cố gắng phân tách một vectơ tại một vị trí quá cuối. Singletons là kỹ thuật tiêu chuẩn để tạo một loại phụ thuộc vào giá trị trong Haskell.
* singletons
Thư viện chứa một số trình trợ giúp Mẫu Haskell để tạo các giá trị singleton như Natty
cho bạn.
Ví dụ cuối cùng. Thế còn khi bạn không biết chiều của vectơ tĩnh? Ví dụ: nếu chúng ta đang cố gắng xây dựng một vectơ từ dữ liệu thời gian chạy dưới dạng danh sách thì sao? Bạn cần loại vectơ phụ thuộc vào độ dài của danh sách đầu vào. Nói cách khác, chúng ta không thể sử dụng foldr VCons VNil
để xây dựng một vectơ vì loại vectơ đầu ra thay đổi theo mỗi lần lặp của nếp gấp. Chúng ta cần giữ bí mật độ dài của vectơ từ trình biên dịch.
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
là một kiểu tồn tại : biến kiểu n
không xuất hiện trong kiểu trả về của hàm tạo AVec
dữ liệu. Chúng tôi đang sử dụng nó để mô phỏng một cặp phụ thuộc : fromList
không thể cho bạn biết chiều dài của vectơ một cách tĩnh, nhưng nó có thể trả về một cái gì đó bạn có thể khớp mẫu để tìm hiểu độ dài của vectơ - Natty n
trong phần tử đầu tiên của bộ dữ liệu . Như Conor McBride đặt nó trong một câu trả lời có liên quan , "Bạn nhìn vào một thứ, và khi làm như vậy, hãy tìm hiểu về một thứ khác".
Đây là một kỹ thuật phổ biến cho các loại định lượng tồn tại. Bởi vì bạn thực sự không thể làm bất cứ điều gì với dữ liệu mà bạn không biết loại - hãy thử viết một hàm data Something = forall a. Sth a
- tồn tại thường đi kèm với bằng chứng GADT cho phép bạn khôi phục loại ban đầu bằng cách thực hiện các kiểm tra khớp mẫu. Các mẫu phổ biến khác cho các tồn tại bao gồm đóng gói các hàm để xử lý loại ( data AWayToGetTo b = forall a. HeresHow a (a -> b)
) của bạn , đó là một cách gọn gàng để thực hiện các mô-đun hạng nhất hoặc xây dựng trong một từ điển lớp loại ( data AnOrd = forall a. Ord a => AnOrd a
) có thể giúp mô phỏng đa hình loại phụ.
ghci> fromList [1,2,3]
AVec (Sy (Sy (Sy Zy))) (VCons 1 (VCons 2 (VCons 3 Nil)))
Các cặp phụ thuộc rất hữu ích bất cứ khi nào các thuộc tính tĩnh của dữ liệu phụ thuộc vào thông tin động không có sẵn tại thời điểm biên dịch. Đây là filter
vectơ:
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
hai AVec
s, chúng ta cần chứng minh với GHC rằng độ dài của chúng bằng nhau. Data.Type.Equality
định nghĩa một GADT chỉ có thể được xây dựng khi các đối số kiểu của nó giống nhau:
data (a :: k) :~: (b :: k) where
Refl :: a :~: a -- short for 'reflexivity'
Khi bạn khớp mẫu Refl
, GHC biết điều đó a ~ b
. Ngoài ra còn có một số chức năng giúp bạn làm việc với loại này: chúng tôi sẽ sử dụng gcastWith
để chuyển đổi giữa các loại tương đương và TestEquality
để xác định xem hai Natty
s có bằng nhau không.
Để kiểm tra sự bình đẳng giữa hai Natty
s, chúng ta sẽ cần phải thực hiện sử dụng thực tế là nếu hai số đều bình đẳng, sau đó người kế vị của họ cũng đều bình đẳng ( :~:
là đồng dư trên S
):
congSuc :: (n :~: m) -> (S n :~: S m)
congSuc Refl = Refl
Khớp mẫu Refl
ở phía bên trái cho phép GHC biết điều đó n ~ m
. Với kiến thức đó, điều đó thật tầm thường S n ~ S m
, vì vậy GHC cho phép chúng tôi trả lại một cái mới Refl
ngay lập tức.
Bây giờ chúng ta có thể viết một ví dụ của TestEquality
đệ quy đơn giản. Nếu cả hai số đều bằng 0, chúng bằng nhau. Nếu cả hai số đều có tiền thân, chúng bằng nhau nếu các số trước bằng nhau. (Nếu chúng không bằng nhau, chỉ cần quay lại 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
Bây giờ chúng ta có thể đặt các mảnh lại với nhau thành dot
một cặp có AVec
độ dài không xác định.
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)
Đầu tiên, khớp mẫu trên hàm AVec
tạo để rút ra biểu diễn thời gian chạy của độ dài của vectơ. Bây giờ sử dụng testEquality
để xác định xem những độ dài đó có bằng nhau không. Nếu có, chúng ta sẽ có Just Refl
; gcastWith
sẽ sử dụng bằng chứng bình đẳng đó để đảm bảo rằng nó dot u v
được đánh máy tốt bằng cách loại bỏ n ~ m
giả định ngầm định của nó .
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
Lưu ý rằng, vì một vectơ không có kiến thức tĩnh về độ dài của nó về cơ bản là một danh sách, chúng tôi đã triển khai lại một cách hiệu quả phiên bản danh sách dot :: Num a => [a] -> [a] -> Maybe a
. Sự khác biệt là phiên bản này được triển khai theo các vectơ ' dot
. Đây là điểm: trước khi trình kiểm tra loại sẽ cho phép bạn gọi dot
, bạn phải kiểm tra xem danh sách đầu vào có cùng độ dài hay không testEquality
. Tôi có xu hướng đi if
sai đường vòng, nhưng không phải trong một thiết lập phụ thuộc!
Bạn không thể tránh sử dụng các trình bao bọc hiện sinh ở các cạnh của hệ thống, khi bạn xử lý dữ liệu thời gian chạy, nhưng bạn có thể sử dụng các loại phụ thuộc ở mọi nơi trong hệ thống của mình và giữ các trình bao bọc tồn tại ở các cạnh, khi bạn thực hiện xác thực nhập.
Vì Nothing
không có nhiều thông tin, bạn có thể tinh chỉnh thêm loại dot'
để trả về một bằng chứng cho thấy độ dài không bằng nhau (dưới dạng bằng chứng cho thấy sự khác biệt của chúng không phải là 0) trong trường hợp thất bại. Điều này khá giống với kỹ thuật Haskell tiêu chuẩn sử dụng Either String a
để có thể trả về thông báo lỗi, mặc dù thuật ngữ bằng chứng hữu ích hơn nhiều về mặt tính toán so với chuỗi!
Do đó, kết thúc chuyến tham quan này về một số kỹ thuật phổ biến trong lập trình Haskell được gõ phụ thuộc. Lập trình với các loại như thế này trong Haskell thực sự rất tuyệt, nhưng thực sự rất khó xử cùng một lúc. Việc chia tất cả dữ liệu phụ thuộc của bạn thành nhiều biểu diễn có nghĩa tương tự - Nat
loại, Nat
loại, Natty n
đơn - thực sự khá cồng kềnh, mặc dù có sự tồn tại của trình tạo mã để trợ giúp cho bản tóm tắt. Hiện tại cũng có những hạn chế về những gì có thể được thăng cấp lên cấp độ loại. Mặc dù đó là trêu ngươi! Tâm trí chú ý đến các khả năng - trong tài liệu có các ví dụ trong Haskell về kiểu gõ mạnh printf
, giao diện cơ sở dữ liệu, công cụ bố trí UI ...
Nếu bạn muốn đọc thêm, sẽ có một tài liệu ngày càng tăng về việc gõ Haskell một cách phụ thuộc, cả được xuất bản và trên các trang web như Stack Overflow. Một tốt điểm bắt đầu là các Hasochism giấy - giấy đi qua ví dụ này rất (trong số những người khác), thảo luận về những phần đau đớn trong một số chi tiết. Bài báo Singletons thể hiện kỹ thuật của các giá trị đơn lẻ (chẳng hạn như Natty
). Để biết thêm thông tin về cách gõ phụ thuộc nói chung, hướng dẫn Agda là một nơi tốt để bắt đầu; Ngoài ra, Idris là một ngôn ngữ đang được phát triển (đại khái) được thiết kế thành "Haskell với các loại phụ thuộc".