haskell에서 "차원을 유형으로 구울 수"있습니까?


20

벡터와 행렬을 다루는 라이브러리를 작성한다고 가정 해보십시오. 호환되지 않는 차원의 연산이 컴파일 타임에 오류를 생성하도록 차원을 유형으로 구울 수 있습니까?

예를 들어 도트 제품의 서명을 다음과 같습니다.

dotprod :: Num a, VecDim d => Vector a d -> Vector a d -> a

여기서 d유형에는 단일 정수 값 (이 벡터의 치수를 나타냄)이 포함됩니다.

각 정수에 대해 별도의 유형을 (수동으로) 정의하고이라는 유형 클래스로 그룹화 하여이 작업을 수행 할 수 있다고 가정합니다 VecDim. 이러한 유형을 "생성"하는 메커니즘이 있습니까?

아니면 같은 것을 성취하는 더 좋고 간단한 방법일까요?


3
예, 정확하게 기억한다면 Haskell에이 기본적인 수준의 의존적 타이핑을 제공하는 라이브러리가 있습니다. 나는 좋은 대답을 제공 할만 큼 익숙하지 않습니다.
Telastyn

주위를 둘러 보면 tensor라이브러리가 재귀 적 data정의를 사용하여 매우 우아하게 달성 하는 것으로 보입니다 . noaxiom.org/tensor-documentation#ordinals
mitchus

이것은 스칼라이며, 하스켈이 아니지만, 부적합한 치수를 방지하기 위해 종속 유형을 사용하는 것과 관련하여 벡터의 "유형"이 일치하지 않는 관련 개념이 있습니다. chrisstucchio.com/blog/2014/…
데니스

답변:


32

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- 즉 우리는이 타입 수준의 자연수를. 참고 유형 SZ구성원 값이 없습니다 - 종류의 유일한 유형 *값이 살고됩니다.

이제 알려진 길이의 벡터를 나타내는 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 하기 때문에 인스턴스에 넣지 않았습니다 . 또한 컴파일러에서 생성 한의 인스턴스를 사용하고 있습니다 .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 보다 클 때마다 출력에 하나를 추가하고 재귀 호출에서 하나씩 줄입니다. (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에 나타나게 하려면 "전 스펙트럼"종속 유형이 필요하지만 승격 된 유형 생성자 만 제공합니다. 달리 말하면, 타입 생성자 와 값 레벨에는 나타나지 않습니다. 특정 . 의 런타임 표현을 위해 싱글 톤 값정해야합니다 . *DataKindsSZNat

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) 

dotAVec들, 우리는 그들의 길이가 동일하다는 GHC 증명해야합니다. Data.Type.Equality형식 인수가 동일한 경우에만 생성 할 수있는 GADT를 정의합니다.

data (a :: k) :~: (b :: k) where
    Refl :: a :~: a  -- short for 'reflexivity'

에 패턴 일치를 Refl하면 GHC는이를 알고 a ~ b있습니다. 이 유형으로 작업하는 데 도움이되는 몇 가지 기능도 있습니다. gcastWith동등한 유형 간을 변환하고 TestEqualityNatty값이 같은지 여부를 결정하는 데 사용합니다 .

두 가지의 평등을 테스트하려면 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 vn ~ 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


@ Benjamin 참고로, 마지막에 Idris 링크가 끊어진 것 같습니다.
Erik Eidt

@ErikEidt 죄송합니다. 지적 해 주셔서 감사합니다! 업데이트하겠습니다.
Benjamin Hodgson

14

이를 의존적 입력 이라고 합니다. 이름을 알고 나면 원하는 것보다 더 많은 정보를 찾을 수 있습니다. Idris 라는 흥미로운 하스켈 같은 언어 가 있습니다. 필자는 YouTube에서 찾을 수있는 주제에 대해 몇 가지 훌륭한 프레젠테이션을했습니다.


그것은 의존하는 타이핑이 아닙니다. 의존적 인 타이핑은 런타임에 타입에 대해 이야기하지만 컴파일 타임에 타입에 대한 차원의 베이킹을 쉽게 할 수 있습니다.
DeadMG

4
@DeadMG 반대로, 의존적 인 타이핑 은 컴파일 타임 에 대해 이야기 합니다 . 런타임시 유형 은 종속 입력이 아닌 반영입니다. 내 대답에서 볼 수 있듯이 유형으로 치수를 굽는 것은 일반적인 차원에서 쉽지 않습니다. (당신은 정의 할 수 있습니다 , 등등하지만 어떤 질문의 질문이 아니다.)newtype Vec2 a = V2 (a,a)newtype Vec3 a = V3 (a,a,a)
벤자민 호지 슨

글쎄, 값은 런타임에만 표시되므로 중단 문제를 해결하지 않으면 컴파일 타임에 값에 대해 실제로 이야기 할 수 없습니다. 내가 말하는 것은 C ++에서도 차원에 대해서만 템플릿을 작성할 수 있으며 정상적으로 작동한다는 것입니다. 하스켈에 해당하는 것이 없습니까?
DeadMG

4
@DeadMG "풀 스펙트럼"의존적으로 형식화 된 언어 (Agda와 같은)는 실제로 형식 언어에서 임의의 용어 수준 계산을 허용합니다. 지적한 바와 같이, 이로 인해 중단 문제를 해결하려고 시도 할 위험이 있습니다. 가장 의존적으로 유형이 지정된 시스템 인 afaik 은 Turing이 완료되지 않아이 문제를 해결합니다 . 저는 C ++ 사용자는 아니지만 템플릿을 사용하여 종속 유형을 시뮬레이션 할 수 있다는 사실에 놀라지 않습니다. 템플릿은 모든 종류의 독창적 인 방법으로 남용 될 수 있습니다.
Benjamin Hodgson

4
@BenjaminHodgson pi 형식을 시뮬레이션 할 수 없으므로 템플릿을 사용하여 종속 형식을 수행 할 수 없습니다. "정규"의존형은 필요한 Pi (x : A). B함수 AB x어디 x에서 함수의 인수인지를 주장해야합니다. 여기서 함수의 반환 유형은 인수로 제공된 표현식 에 따라 다릅니다 . 그러나이 모든 것을 지울 수 있습니다. 컴파일 시간 만입니다.
Daniel Gratzer
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.