web-dev-qa-db-ja.com

Data.Voidの不条理な機能は何に役立ちますか?

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での例ですが、誰かが依存型付けされた言語を使用したい場合、私は文句を言うつもりはありません...

89
Luis Casillas

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は純粋であるため、このタイプからは何も取得できません。

54
Philip JF

自由変数によってパラメーター化されたラムダ項のこの表現を検討してください。 (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型の例外を発生させる可能性のある計算をキャプチャすることを想像してください。このアプローチを使用して、不正な動作のリスクを均一に文書化できます。この設定で完全に適切に動作する計算を行うには、eVoidにして、次を使用します。

_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があなたの使い方です。

58
pigworker

「発生しない」ケースを徹底的に処理するタイプセーフな方法として、場合によっては役立つと考えています。

これはまさに正しいです。

absurdconst (error "Impossible")ほど有用ではないと言えます。ただし、型は制限されているため、その唯一の入力はVoid型(意図的に無人のままにされるデータ型)になります。つまり、absurdに渡すことができる実際の値はありません。型チェッカーがVoid型の何かにアクセスできると考えているコードのブランチになった場合、まあ、あなたは不条理状況。したがって、基本的にabsurdを使用して、このコードブランチに到達しないことをマークします。

「Ex falso quodlibet」は、文字通り「[a] false [proposition]から、何でも続く」という意味です。そのため、タイプがVoidであるデータを保持していることに気付いた場合、手元に誤った証拠があることがわかります。したがって、間違った命題から何かが続くので、(absurdを介して)必要なany穴を埋めることができます。

absurdの使用例を含むConduitの背後にあるアイデアに関するブログ投稿を書きました。

http://unknownparallel.wordpress.com/2012/07/30/pipes-to-conduits-part-6-leftovers/#running-a-pipeline

35
Dan Burton

通常、これを使用して、見かけ上部分的なパターンマッチを回避できます。たとえば、 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
13
Daniel Wagner

空のデータ型 を表す方法はさまざまです。 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パッケージ。)

11
Petr Pudlák

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の一種です。

0
user1747134