9

最近これに関する記事を見たと断言できたのですが、見つかりません。

数値 mod のバイナリ エンコーディングを実行する型を作成しようとしていますnが、そのためには、型レベルのナチュラルで述語を記述できる必要があります。

{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE TypeFamilies #-}
{-# LANGUAGE GADTs #-}
{-# LANGUAGE TypeSynonymInstances #-}
{-# LANGUAGE FunctionalDependencies #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE UndecidableInstances #-}
module Modulo where

-- (pseudo-)binary representation of 
-- numbers mod n
--
--  e.g. Mod Seven contains
--    Zero . Zero . Zero $ Stop
--    Zero . Zero . One  $ Stop
--    Zero . One . Zero $ Stop
--    Zero . One . One  $ Stop
--    One . Zero . Zero $ Stop
--    One . Zero . One  $ Stop
--    One . One $ Stop 
data Mod n where
  Stop :: Mod One
  Zero :: Split n => Mod (Lo n) -> Mod n
  One  :: Split n => Mod (Hi n) -> Mod n

-- type-level naturals
data One
data Succ n 
type Two = Succ One

-- predicate to allow us to compare 
-- natural numbers
class Compare n n' b | n n' -> b

-- type-level ordering
data LT
data EQ
data GT

-- base cases
instance Compare One One EQ
instance Compare One (Succ n) LT
instance Compare (Succ n) One GT

-- recurse
instance Compare n n' b => Compare (Succ n) (Succ n') b

-- predicate to allow us to break down
-- natural numbers by their bit structure
--
-- if every number mod n can be represented in m bits, then
class Split n where
  type Lo n -- number of values mod n where the m'th bit is 0
  type Hi n -- number of values mod n where the m'th bit is 1

-- base case, n = 2
-- for this, we only need m=1 bit
instance Split Two where
  type Lo Two = One -- 0 mod 2
  type Hi Two = One -- 1 mod 2

-- recurse
--  if (Lo n) == (Hi n), then n = 2^m, so
--  the values mod (n+1) will require one extra bit
instance (Split n, Compare (Lo n) (Hi n) EQ) => Split (Succ n) where
  type Lo (Succ n) = n    -- all the numbers mod n will be prefixed by a 0
  type Hi (Succ n) = One  -- and one extra, which will be 10...0

-- recurse
--  if (Lo n) > (Hi n), then n < 2^m, so
--  the values mod (n+1) still only require m bits
instance (Split n, Compare (Lo n) (Hi n) GT) => Split (Succ n) where
  type Lo (Succ n) = Lo (n)       -- we've got the same number of lower values
  type Hi (Succ n) = Succ (Hi n)  -- and one more higher value

私の現在の実装では、一連のコンパイラ エラーが吐き出されます。

Nat.hs:60:8:
    Conflicting family instance declarations:
      type Lo Two -- Defined at Nat.hs:60:8-9
      type Lo (Succ n) -- Defined at Nat.hs:74:8-9

Nat.hs:61:8:
    Conflicting family instance declarations:
      type Hi Two -- Defined at Nat.hs:61:8-9
      type Hi (Succ n) -- Defined at Nat.hs:75:8-9

Nat.hs:66:10:
    Duplicate instance declarations:
      instance (Split n, Compare (Lo n) (Hi n) EQ) => Split (Succ n)
        -- Defined at Nat.hs:66:10-62
      instance (Split n, Compare (Lo n) (Hi n) GT) => Split (Succ n)
        -- Defined at Nat.hs:73:10-62

Nat.hs:67:8:
    Conflicting family instance declarations:
      type Lo (Succ n) -- Defined at Nat.hs:67:8-9
      type Lo (Succ n) -- Defined at Nat.hs:74:8-9

Nat.hs:68:8:
    Conflicting family instance declarations:
      type Hi (Succ n) -- Defined at Nat.hs:68:8-9
      type Hi (Succ n) -- Defined at Nat.hs:75:8-9

述語が競合していると思われる場合は、述語を間違って記述しようとしていると思います。

どうすればそれらを正しく行うことができますか?

4

1 に答える 1

14

競合の問題は非常に単純です。重複する型ファミリのルールは非常に単純です。

1 つのプログラムで使用される型ファミリのインスタンス宣言は、重複するインスタンスの右側が重複する型と一致する場合にのみ重複できます。より正式には、インスタンスの左側を構文的に同じにする置換がある場合、2 つのインスタンス宣言が重複します。その場合は常に、インスタンスの右側も同じ置換の下で構文的に等しくなければなりません。

構文的に等しいことを指定していることに注意してください。次の 2 つの例を考えてみましょう。

instance Split Two where
  type Lo Two = One -- 0 mod 2
  type Hi Two = One -- 1 mod 2

instance Split (Succ n) where
  type Lo (Succ n) = Lo (n)  
  type Hi (Succ n) = Succ (Hi n)

Twoとして定義されSucc One、単純な型のシノニムは構文上の同等性のために展開されるため、これらは左側で同等です。しかし、右側はそうではないため、エラーが発生します。

上記のコードからクラス コンテキストを削除したことに気付いたかもしれません。より深刻な問題、そしておそらく上記の競合が発生するとは予想していなかった理由は、重複インスタンスが実際には競合している重複であるということです。クラス コンテキストは、いつものように、インスタンス選択の目的で無視されます。メモリが役立つ場合、関連付けられた型ファミリではこれが 2 倍になります。これは主に、関連付けられていない型ファミリの構文上の利便性であり、クラスによって制約されない可能性があります。予想。

ここで、これら 2 つのインスタンス明確に異なる必要があり、 using の結果に基づいてどちらかを選択する必要があるため、その結果は単なる制約ではなく、型クラスのパラメーターCompareでなければなりません。ここでは型ファミリと関数依存関係を混在させていますが、これは不必要に扱いにくいものです。したがって、上から順に、基本的な型を保持します。

-- type-level naturals
data One
data Succ n 
type Two = Succ One

-- type-level ordering
data LT
data EQ
data GT

Compare関数を型ファミリーとして書き直します。

type family Compare n m :: *
type instance Compare One One = EQ
type instance Compare (Succ n) One = GT
type instance Compare One (Succ n) = LT
type instance Compare (Succ n) (Succ m) = Compare n m

ここで、条件を処理するには、ある種の「フロー制御」型ファミリが必要になります。ここでは、啓発とインスピレーションのために、もう少し一般的なものを定義します。味に特化。

述語、入力、および選択する 2 つの分岐を指定します。

type family Check pred a yes no :: * 

Compareの結果をテストするための述語が必要です。

data CompareAs a
type instance (CompareAs LT) LT yes no = yes 
type instance (CompareAs EQ) LT yes no = no
type instance (CompareAs GT) LT yes no = no
type instance (CompareAs LT) EQ yes no = no 
type instance (CompareAs EQ) EQ yes no = yes
type instance (CompareAs GT) EQ yes no = no
type instance (CompareAs LT) GT yes no = no
type instance (CompareAs EQ) GT yes no = no
type instance (CompareAs GT) GT yes no = yes

確かに、これは恐ろしく退屈な一連の定義であり、型値のより大きなセットを比較するための予後は非常に厳しいものです。より拡張可能なアプローチが存在します (疑似種類のタグと、明白で効果的な解決策であるナチュラルを使用した全単射) が、それはこの回答の範囲を少し超えています。つまり、ここでは型レベルの計算を行っているだけなので、ばかげたことはやめましょう。

この場合、単純に比較結果に対してケース分析関数を定義する方が簡単です。

type family CaseCompare cmp lt eq gt :: *
type instance CaseCompare LT lt eq gt = lt
type instance CaseCompare EQ lt eq gt = eq
type instance CaseCompare GT lt eq gt = gt

今のところ後者を使用しますが、他の場所でより複雑な条件が必要な場合は、一般的なアプローチがより理にかなっています。

ともかく。Splitクラスを関連付けられていない型ファミリに分割できます。

data Oops

type family Lo n :: *
type instance Lo Two = One
type instance Lo (Succ (Succ n)) 
    = CaseCompare (Compare (Lo (Succ n)) (Hi (Succ n)))
                  Oops -- yay, type level inexhaustive patterns
                  (Succ n)
                  (Lo (Succ n))

type family Hi n :: *
type instance Hi Two = One
type instance Hi (Succ (Succ n)) 
    = CaseCompare (Compare (Lo (Succ n)) (Hi (Succ n)))
                  Oops -- yay, type level inexhaustive patterns
                  One
                  (Succ (Hi (Succ n)))

ここで最も重要な点は、(一見冗長に見える) -- の使用です。これは、引数を と区別するために(Succ (Succ n))2 つのコンストラクターが必要であり、これにより、見た競合エラーを回避できるためです。SuccTwo

すべてが完全に型レベルであるため、簡単にするためにここではすべてを型ファミリに移動したことに注意してください。上記の計算に応じて異なる方法で値を処理したい場合 (Modタイプを介した間接的なものも含む)、適切なクラス定義を追加し直す必要がある場合があります。これは、単にタイプに基づいて選択するのではなく、タイプに基づいて用語を選択するために必要なためです。タイプについて。

于 2012-02-28T01:11:03.487 に答える