Data.Void
の- absurd
関数には次のシグネチャがあります。Void
は、そのパッケージによってエクスポートされた論理的に無人のタイプです。
-- | Since 'Void' values logically don't exist, this witnesses the logical
-- reasoning tool of \"ex falso quodlibet\".
absurd :: Void -> a
これは、型としての命題の対応により、有効な式⊥ → a
に対応するというドキュメントの発言を得るのに十分なロジックを知っています。
私が困惑し、興味を持っているのは、この機能がどのような実用的なプログラミングの問題に役立つかということです。 「発生しない」ケースを徹底的に処理するタイプセーフな方法としておそらく役立つと考えていますが、そのアイデアが実現しているかどうかを判断するためのCurry-Howardの実用的な使い方については十分に知りません。まったく正しい。
編集:できればHaskellでの例ですが、誰かが依存型付けされた言語を使用したい場合、私は文句を言うつもりはありません...
Haskellは厳密ではないので、人生は少し大変です。一般的な使用例は、不可能なパスを処理することです。例えば
simple :: Either Void a -> a
simple (Left x) = absurd x
simple (Right y) = y
これはやや有用であることが判明しました。 Pipes
の単純型を検討してください
data Pipe a b r
= Pure r
| Await (a -> Pipe a b r)
| Yield !b (Pipe a b r)
これは、Gabriel GonzalesのPipes
ライブラリからの標準パイプタイプの厳密化され簡略化されたバージョンです。これで、決して生成されないパイプ(つまり、消費者)を次のようにエンコードできます。
type Consumer a r = Pipe a Void r
これは本当に決して降伏しません。この意味は、Consumer
の適切なフォールドルールは
foldConsumer :: (r -> s) -> ((a -> s) -> s) -> Consumer a r -> s
foldConsumer onPure onAwait p
= case p of
Pure x -> onPure x
Await f -> onAwait $ \x -> foldConsumer onPure onAwait (f x)
Yield x _ -> absurd x
または、ignore消費者を扱う場合の歩留りケースを使用できます。これは、このデザインパターンの一般的なバージョンです。必要なときに多態性データ型とVoid
を使用して、可能性を取り除きます。
おそらくVoid
の最も古典的な使用法はCPSにあります。
type Continuation a = a -> Void
つまり、Continuation
は決して戻らない関数です。 Continuation
は「not」の型バージョンです。これからCPSのモナドを取得します(古典的な論理に対応)
newtype CPS a = Continuation (Continuation a)
haskellは純粋であるため、このタイプからは何も取得できません。
自由変数によってパラメーター化されたラムダ項のこの表現を検討してください。 (Bellegarde and Hook 1994、Bird and Paterson 1999、Altenkirch and Reus 1999の論文を参照してください。)
_data Tm a = Var a
| Tm a :$ Tm a
| Lam (Tm (Maybe a))
_
確かにこれをFunctor
にすると、名前変更の概念をキャプチャし、Monad
を置換の概念をキャプチャできます。
_instance Functor Tm where
fmap rho (Var a) = Var (rho a)
fmap rho (f :$ s) = fmap rho f :$ fmap rho s
fmap rho (Lam t) = Lam (fmap (fmap rho) t)
instance Monad Tm where
return = Var
Var a >>= sig = sig a
(f :$ s) >>= sig = (f >>= sig) :$ (s >>= sig)
Lam t >>= sig = Lam (t >>= maybe (Var Nothing) (fmap Just . sig))
_
次に、closedの用語を考えます。これらは_Tm Void
_の住民です。任意の自由変数を持つ用語に閉じた用語を埋め込むことができるはずです。どうやって?
_fmap absurd :: Tm Void -> Tm a
_
もちろん、キャッチは、この関数が用語を正確に何も実行しないことです。しかし、それはunsafeCoerce
よりも正直な感じです。そして、それがvacuous
が_Data.Void
_に追加された理由です...
または、評価者を作成します。以下は、b
に自由変数がある値です。
_data Val b
= b :$$ [Val b] -- a stuck application
| forall a. LV (a -> Val b) (Tm (Maybe a)) -- we have an incomplete environment
_
ラムダをクロージャーとして表しただけです。評価者は、a
の自由変数をb
を超える値にマッピングする環境によってパラメーター化されます。
_eval :: (a -> Val b) -> Tm a -> Val b
eval g (Var a) = g a
eval g (f :$ s) = eval g f $$ eval g s where
(b :$$ vs) $$ v = b :$$ (vs ++ [v]) -- stuck application gets longer
LV g t $$ v = eval (maybe v g) t -- an applied lambda gets unstuck
eval g (Lam t) = LV g t
_
ご推察通り。任意のターゲットでクローズドタームを評価するには
_eval absurd :: Tm Void -> Val b
_
より一般的には、Void
が単独で使用されることはほとんどありませんが、ある種の不可能性を示す方法で型パラメーターをインスタンス化する場合に便利です(たとえば、ここでは閉じた用語で自由変数を使用) 。多くの場合、これらのパラメーター化された型には、パラメーターの演算を型全体の演算に持ち上げる高階関数が付属しています(例:ここでは、fmap
、_>>=
_、eval
)。そのため、absurd
の汎用操作としてVoid
を渡します。
別の例として、_Either e v
_を使用してv
を提供するが、e
型の例外を発生させる可能性のある計算をキャプチャすることを想像してください。このアプローチを使用して、不正な動作のリスクを均一に文書化できます。この設定で完全に適切に動作する計算を行うには、e
をVoid
にして、次を使用します。
_either absurd id :: Either Void v -> v
_
安全に実行するため、または
_either absurd Right :: Either Void v -> Either e v
_
安全でないコンポーネントを安全でない世界に埋め込むため。
ああ、最後の1つ、「起こらない」を処理することです。これは、カーソルが配置できないすべての場所で、一般的なジッパー構造で表示されます。
_class Differentiable f where
type D f :: * -> * -- an f with a hole
plug :: (D f x, x) -> f x -- plugging a child in the hole
newtype K a x = K a -- no children, just a label
newtype I x = I x -- one child
data (f :+: g) x = L (f x) -- choice
| R (g x)
data (f :*: g) x = f x :&: g x -- pairing
instance Differentiable (K a) where
type D (K a) = K Void -- no children, so no way to make a hole
plug (K v, x) = absurd v -- can't reinvent the label, so deny the hole!
_
正確に関連していなくても、残りを削除しないことにしました。
_instance Differentiable I where
type D I = K ()
plug (K (), x) = I x
instance (Differentiable f, Differentiable g) => Differentiable (f :+: g) where
type D (f :+: g) = D f :+: D g
plug (L df, x) = L (plug (df, x))
plug (R dg, x) = R (plug (dg, x))
instance (Differentiable f, Differentiable g) => Differentiable (f :*: g) where
type D (f :*: g) = (D f :*: g) :+: (f :*: D g)
plug (L (df :&: g), x) = plug (df, x) :&: g
plug (R (f :&: dg), x) = f :&: plug (dg, x)
_
実際には、関連性があるかもしれません。冒険好きなら、この 未完成の記事 はVoid
を使用して自由変数を持つ用語の表現を圧縮する方法を示しています
_data Term f x = Var x | Con (f (Term f x)) -- the Free monad, yet again
_
Differentiable
およびTraversable
ファンクターf
から自由に生成された構文で。 _Term f Void
_を使用して、フリー変数のない領域を表し、[D f (Term f Void)]
を使用して、フリーのない領域をトンネリングするtubesを表します変数を、孤立した自由変数、または2つ以上の自由変数へのパスのジャンクションに追加します。いつかその記事を終えなければなりません。
値のない(または少なくとも、礼儀正しい会社で言う価値のない)タイプの場合、Void
は非常に便利です。そして、absurd
があなたの使い方です。
「発生しない」ケースを徹底的に処理するタイプセーフな方法として、場合によっては役立つと考えています。
これはまさに正しいです。
absurd
はconst (error "Impossible")
ほど有用ではないと言えます。ただし、型は制限されているため、その唯一の入力はVoid
型(意図的に無人のままにされるデータ型)になります。つまり、absurd
に渡すことができる実際の値はありません。型チェッカーがVoid
型の何かにアクセスできると考えているコードのブランチになった場合、まあ、あなたは不条理状況。したがって、基本的にabsurd
を使用して、このコードブランチに到達しないことをマークします。
「Ex falso quodlibet」は、文字通り「[a] false [proposition]から、何でも続く」という意味です。そのため、タイプがVoid
であるデータを保持していることに気付いた場合、手元に誤った証拠があることがわかります。したがって、間違った命題から何かが続くので、(absurd
を介して)必要なany穴を埋めることができます。
absurd
の使用例を含むConduitの背後にあるアイデアに関するブログ投稿を書きました。
通常、これを使用して、見かけ上部分的なパターンマッチを回避できます。たとえば、 this answer からデータ型宣言の近似値を取得します。
data RuleSet a = Known !a | Unknown String
data GoRuleChoices = Japanese | Chinese
type LinesOfActionChoices = Void
type GoRuleSet = RuleSet GoRuleChoices
type LinesOfActionRuleSet = RuleSet LinesOfActionChoices
次に、次のようにabsurd
を使用できます。
handleLOARules :: (String -> a) -> LinesOfActionsRuleSet -> a
handleLOARules f r = case r of
Known a -> absurd a
Unknown s -> f s
空のデータ型 を表す方法はさまざまです。 1つは空の代数データ型です。別の方法は、∀α.αのエイリアスにすることです
_type Void' = forall a . a
_
haskellでは-これがSystem Fでのエンコード方法です( Proofs and Types の第11章を参照)。これらの2つの記述はもちろん同型であり、同型は\x -> x :: (forall a.a) -> Void
と_absurd :: Void -> a
_によって目撃されます。
場合によっては、通常、関数の引数、または Data.Conduit などのより複雑なデータ型に空のデータ型が現れる場合、明示的なバリアントを好むことがあります。
_type Sink i m r = Pipe i i Void () m r
_
場合によっては、通常、空のデータ型が関数の戻り値の型に含まれる多相バリアントを好むことがあります。
absurd
は、これら2つの表現間で変換を行うときに発生します。
たとえば、 callcc :: ((a -> m b) -> m a) -> m a
は(暗黙の)_forall b
_を使用します。また、タイプ_((a -> m Void) -> m a) -> m a
_の場合もあります。なぜなら、コンティネーションの呼び出しは実際には返されず、制御を別のポイントに転送するからです。継続で作業したい場合は、定義できます
_type Continuation r a = a -> Cont r Void
_
(_type Continuation' r a = forall b . a -> Cont r b
_を使用できますが、ランク2タイプが必要です。)そして、vacuousM
はこの_Cont r Void
_を_Cont r b
_に変換します。
(また、特定のパッケージの使用状況(逆依存関係)を検索するために haskellers.com を使用できることに注意してください。たとえば、voidパッケージ。)
Idrisのような依存型の言語では、おそらくHaskellよりも便利です。通常、実際に関数に押し込めない値をパターン一致するトータル関数では、無人タイプの値を作成し、absurd
を使用してケース定義を完成させます。
たとえば、この関数は、そこに存在するタイプレベルのコストトレインを持つリストから要素を削除します。
shrink : (xs : Vect (S n) a) -> Elem x xs -> Vect n a
shrink (x :: ys) Here = ys
shrink (y :: []) (There p) = absurd p
shrink (y :: (x :: xs)) (There p) = y :: shrink (x :: xs) p
2番目のケースでは、空のリストに特定の要素があると言っていますが、これは馬鹿げています。ただし、一般に、コンパイラはこれを認識していないため、多くの場合明示的に指定する必要があります。その後、コンパイラは関数定義が部分的でないことを確認でき、より強力なコンパイル時の保証が得られます。
命題であるカリー・ハワードの観点からすると、absurd
は矛盾による証明のQEDの一種です。