(おそらくモジュール境界を使用して) 新しいメンバーを許可できない型クラスを作成することは可能ですか? 完全なインスタンス定義に必要な関数のエクスポートを拒否することはできますが、誰かが無効なインスタンスを生成した場合にのみ実行時エラーが発生します。コンパイル時エラーにすることはできますか?
6 に答える
GHC 7.8.1 から、閉じた型族を宣言できるようになりました。私はそれらの助けを借りて、次のConstraintKinds
ようにできると思います:
type family SecretClass (a :: *) :: Constraint where
SecretClass Int = ()
SecretClass a
型クラスと同等の制約を形成し、ファミリは誰によっても拡張できないため、「クラス」の他のインスタンスを定義することはできません。
(これは実際にはテストできないため、単なる推測ですが、この興味深いリンクのコードは機能するように見えます。)
あなたが達成しようとしていることに応じて、答えは限定されたイエスだと思います。
インターフェース モジュール1から型クラス名自体をエクスポートすることは控えることができますが、型クラス関数の名前は引き続きエクスポートできます。誰も名前を付けることができないため、誰もクラスのインスタンスを作成できません!
例:
module Foo (
foo,
bar
) where
class SecretClass a where
foo :: a
bar :: a -> a -> a
instance SecretClass Int where
foo = 3
bar = (+)
欠点は、誰もあなたのクラスを制約として型を書くことができないということです。コンパイラは引き続き型を推測できるため、これは、人々がそのような型を持つ関数を書くことを完全に妨げるわけではありません。しかし、それは非常に面倒です。
「クローズド」クラスをスーパークラスとして、別の空の型クラスを提供することで、欠点を軽減できます。元のクラスのすべてのインスタンスもサブクラスのインスタンスにし、サブクラスを (すべての型クラス関数と共に) エクスポートしますが、スーパー クラスはエクスポートしません。(わかりやすくするために、公開するすべてのタイプで「シークレット」クラスではなく「パブリック」クラスを使用する必要がありますが、どちらの方法でも機能すると思います)。
例:
{-# LANGUAGE FlexibleInstances, UndecidableInstances #-}
module Foo (
PublicClass,
foo,
bar
) where
class SecretClass a where
foo :: a
bar :: a -> a -> a
class SecretClass a => PublicClass a
instance SecretClass Int where
foo = 3
bar = (+)
instance SecretClass a => PublicClass a
PublicClass
のインスタンスごとにのインスタンスを手動で宣言する場合は、拡張機能なしで実行できますSecretClass
。
現在、クライアント コードは を使用PublicClass
して型クラス制約を記述できますが、 のすべてのインスタンスには同じ型PublicClass
の のインスタンスが必要でありSecretClass
、新しいインスタンスを宣言する方法がないため、 2SecretClass
の型インスタンスをこれ以上作成することはできません。PublicClass
これだけでは理解できないのは、コンパイラがクラスを「クローズド」として扱う能力です。「closed」の唯一の可視インスタンスを選択することで解決できるあいまいな型変数については、依然として文句を言います。
1純粋な意見: 通常は、内部モジュールをインポートして必要なものだけをエクスポートするインターフェイス モジュールを使用して、テスト/デバッグ用に取得できるように、すべてをエクスポートする怖い名前の別の内部モジュールを用意することをお勧めします。輸出する。
2拡張機能を使用すると、重複する新しいインスタンスを誰かが宣言できると思います。たとえば、 for のインスタンスを提供した場合[a]
、誰かが for の新しいインスタンスを宣言し、PublicClass
forのインスタンスに[Int]
ピギーバックする可能性があります。しかし、それには機能がなく、インスタンスを書くことができないとすれば、それで多くのことができるとは思えません。SecretClass
[a]
PublicClass
SecretClass
閉じた型ファミリを介して閉じた型クラスをエンコードできます。閉じた型ファミリは、関連する型ファミリとして本質的にエンコードできます。この解決策の鍵は、関連付けられた型ファミリのインスタンスが型クラス インスタンス内にあり、単相型ごとに 1 つの型クラス インスタンスしか存在できないことです。
このアプローチは、モジュール システムとは無関係であることに注意してください。モジュールの境界に依存する代わりに、どのインスタンスが正当であるかの明示的なリストを提供します。これは、合法的なインスタンスが複数のモジュールやパッケージに分散する可能性があることを意味します。また、同じモジュール内であっても、不正なインスタンスを提供することはできません。
この回答では、次のクラスを閉じて、型Int
andに対してのみインスタンス化できるようにしInteger
、他の型に対してはインスタンス化できないと仮定します。
-- not yet closed
class Example a where
method :: a -> a
まず、閉じた型ファミリを関連付けられた型ファミリとしてエンコードするための小さなフレームワークが必要です。
{-# LANGUAGE TypeFamilies, EmptyDataDecls #-}
class Closed c where
type Instance c a
パラメータc
は型ファミリの名前を表し、パラメータa
は型ファミリのインデックスです。c
forのファミリ インスタンスはa
としてエンコードされInstance c a
ます。もクラス パラメータであるためc
、 のファミリ インスタンスはすべてc
、1 つのクラス インスタンス宣言でまとめて指定する必要があります。
ここで、このフレームワークを使用してクローズド タイプ ファミリーを定義し、MemberOfExample
それをエンコードInt
しInteger
ますOk
。
data MemberOfExample
data Ok
instance Closed MemberOfExample where
type Instance MemberOfExample Int = Ok
type Instance MemberOfExample Integer = Ok
最後に、この閉じた型ファミリを のスーパークラス制約で使用しますExample
。
class Instance MemberOfExample a ~ Ok => Example a where
method :: a -> a
Int
いつものようにとの有効なインスタンスを定義できますInteger
。
instance Example Int where
method x = x + 1
instance Example Integer where
method x = x + 1
Int
ただし、と以外の型に対して無効なインスタンスを定義することはできませんInteger
。
-- GHC error: Couldn't match type `Instance MemberOfExample Float' with `Ok'
instance Example Float where
method x = x + 1
また、有効な型のセットを拡張することもできません。
-- GHC error: Duplicate instance declarations
instance Closed MemberOfExample where
type Instance MemberOfExample Float = Ok
-- GHC error: Associated type `Instance' must be inside a class instance
type instance Instance MemberOfExample Float = Ok
残念ながら、次の偽のインスタンスを書くことができます。
-- Unfortunately accepted
instance Instance MemberOfExample Float ~ Ok => Example Float where
method x = x + 1
しかし、平等制約を解除することは決してできないので、それを何にでも使用できるとは思いません。たとえば、次の場合は拒否されます。
-- Couldn't match type `Instance MemberOfExample Float' with `Ok'
test = method (pi :: Float)
型クラスを、型クラスが持っていたすべての関数を含むデータ宣言 (レコード構文を使用) にリファクタリングできます。インスタンスの固定された有限リストは、とにかくクラスを必要としないように聞こえます。
もちろん、これは基本的にコンパイラがクラスの舞台裏で行っていることです。
これにより、インスタンスのリストを関数としてデータ型にエクスポートできます。それらはエクスポートできますが、データ型のコンストラクターはエクスポートできません。同様に、アクセサー関数のエクスポートを制限し、実際に必要なインターフェイスのみをエクスポートできます。
データ型は、型クラスが持つモジュールの境界を越えるオープンワールドの仮定の影響を受けないため、これはうまく機能します。
型システムの複雑さを追加すると、物事が難しくなることがあります。
列挙されたインスタンスのセットがあることだけに関心がある場合は、次のトリックが役立つ場合があります。
class (Elem t '[Int, Integer, Bool] ~ True) => Closed t where
type family Elem (t :: k) (ts :: [k]) :: Bool where
Elem a '[] = False
Elem a (a ': as) = True
Elem a (b ': bs) = Elem a bs
instance Closed Int
instance Closed Integer
instance Closed Bool
-- instance Closed Float -- ERROR