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에서만 작동합니다.
타입 시스템 내에서 길이를 나타내는 방법이 필요합니다. 정의에 따라 자연수는 0 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
내적은 목록과 마찬가지로 진행됩니다.
-- 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
s 를 더하는 법을 가르쳐야합니다 . 이를 위해 유형 수준 함수를 사용 합니다 .
type family (n :: Nat) :+: (m :: Nat) :: Nat where
Z :+: m = m
(S n) :+: m = S (n :+: m)
이 type family
선언은 소개 유형에 대한 함수 호출을 :+:
- 즉, 그것은 두 개의 자연수의 합을 계산하는 유형 검사를위한 조리법이다. 재귀 적으로 정의됩니다-왼쪽 피연산자가 Z
ero 보다 클 때마다 출력에 하나를 추가하고 재귀 호출에서 하나씩 줄입니다. (2를 곱하는 형식 함수를 작성하는 것이 좋습니다 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)
불행히도 하스켈은 그렇게하지 않을 것입니다. 인수 의 값 이 반환 유형 (일반적으로 종속 함수 또는 pi 유형 이라고 함) 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에서 3 요소 벡터를 성공적으로 분할했습니다. 그런 다음 끝을 지나는 위치에서 벡터를 분할하려고 할 때 유형 오류가 발생했습니다. 싱글 톤은 유형을 하스켈의 값에 의존하게 만드는 표준 기술입니다.
* singletons
라이브러리 에는 Natty
여러분 과 같은 싱글 톤 값을 생성하는 템플릿 Haskell 도우미가 포함되어 있습니다.
마지막 예. 벡터의 치수를 정적으로 알지 못하는 경우는 어떻습니까? 예를 들어, 목록 형식으로 런타임 데이터에서 벡터를 만들려고하면 어떻게해야합니까? 입력 목록 의 길이 에 따라 벡터 유형 이 필요합니다 . 달리 말하면, 우리는 출력 벡터의 유형이 접기의 각 반복에 따라 변하기 때문에 벡터를 만드는 데 사용할 수 없습니다 . 컴파일러에서 벡터의 길이를 비밀로 유지해야합니다.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
튜플의 첫 번째 요소 . 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
하위 유형 다형성을 모방하는 데 도움이 될 수 있는 유형 클래스 사전 ( )을 빌드하는 것이 포함 됩니다.
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
값이 같은지 여부를 결정하는 데 사용합니다 .
두 가지의 평등을 테스트하려면 Natty
들, 우리가 두 개의 번호가 동일한 경우, 다음 후임자도 동일하다는 사실 메이크업의 사용에 필요 해요 것은 ( :~:
인 합동 이상 S
) :
congSuc :: (n :~: m) -> (S n :~: S m)
congSuc Refl = Refl
Refl
왼쪽의 패턴 일치 는 GHC에게이를 알려줍니다 n ~ m
. 그 지식으로 사소한 S n ~ S m
일이므로 GHC를 사용하면 새로운 것을 Refl
즉시 반환 할 수 있습니다.
이제 TestEquality
간단한 재귀로 인스턴스를 작성할 수 있습니다 . 두 숫자가 모두 0이면 동일합니다. 두 숫자에 전임자가 있으면 전임자가 같으면 동일합니다. (동일하지 않으면을 반환하십시오 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의 종이 - 용지가 약간 상세하게 고통스러운 부분을 논의, (다른 사람의 사이에서)이 매우 예를 통해 간다. 싱글 톤 용지 (예컨대 단일 값의 기술을 설명 ). 의존적 타이핑에 대한 일반적인 정보는 Agda 튜토리얼을 시작하는 것이 좋습니다. 또한 Idris 는 개발에 사용되는 언어로, "종속적 인 유형의 Haskell"로 설계되었습니다.Natty