19

以前、Parsecの適用可能なファンクターインスタンスのみを使用するようにモナディックコードを変換することについて質問しました。残念ながら、私は文字通り尋ねた質問に答えるいくつかの返信を受け取りましたが、実際には多くの洞察を与えませんでした。では、もう一度試してみましょう...

これまでの私の知識を要約すると、適用可能なファンクターは、モナドよりもいくらか制限されているものです。「少ないほど多い」という伝統では、コードが実行できることを制限すると、狂ったコード操作の可能性が高まります。とにかく、多くの人は、モナドの代わりにアプリケーションを使用することが、可能な場合は優れたソリューションであると信じているようです。

Applicativeクラスはで定義されています。Control.ApplicativeそのHaddockのリストは、クラスメソッドとユーティリティ関数をそれらの間にある膨大な数のクラスインスタンスでうまく分離し、画面上のすべてを一度にすばやく表示することを困難にします。しかし、適切な型署名は

pure ::    x              -> f x
<*>  :: f (x -> y) -> f x -> f y
 *>  :: f  x       -> f y -> f y
<*   :: f  x       -> f y -> f x
<$>  ::   (x -> y) -> f x -> f y
<$   ::    x       -> f y -> f x

完全に理にかなっていますよね?

さて、Functorすでに私たちに与えますfmap、それは基本的に<$>です。xつまり、からへの関数が与えられた場合、yをにマップすることができf xますf yApplicative2つの本質的に新しい要素を追加します。1つはpure、とほぼ同じタイプですreturn(およびさまざまな圏論クラスの他のいくつかの演算子)。もう1つは<*>、関数のコンテナーと入力のコンテナーを取得して、出力のコンテナーを生成する機能を提供するです。

上記の演算子を使用すると、次のようなことを非常にうまく行うことができます

foo <$> abc <*> def <*> ghi

これにより、N-ary関数を取得し、任意のNに簡単に一般化できる方法でN個のファンクターからその引数を取得できます。


これだけ私はすでに理解しています。私がまだ理解していない2つの主要なことがあります。

まず、関数*>、、<*および<$。それらのタイプから、、、<* = constおよび*> = flip const<$類似したものである可能性があります。おそらく、これはこれらの関数が実際に何をするかを説明していません。(??!)

次に、Parsecパーサーを作成する場合、通常、各解析可能エンティティは次のようになります。

entity = do
  var1 <- parser1
  var2 <- parser2
  var3 <- parser3
  ...
  return $ foo var1 var2 var3...

アプリケーションファンクターでは、このように中間結果を変数にバインドできないため、最終段階でそれらを収集する方法に戸惑います。これを行う方法を理解するのに十分なほど、アイデアに頭を悩ませることはできませんでした。

4

5 に答える 5

26

<*および関数は非常に単純です。これらは。*>と同じように機能し>>ます。が存在しないことを除いて、<*は同じように機能します。基本的に、与えられた場合、最初に「実行」し、次に「実行」して、の結果を返します。の場合、最初に「do」、次に「do」を実行しますが、の結果を返します。(もちろん、「する」の適切な意味について。)<<<<a *> babba <* baba

<$関数はただfmap constです。したがってa <$ b、に等しいfmap (const a) b。「アクション」の結果を破棄し、代わりに定数値を返すだけです。型を持つControl.Monad関数は、と書くことができます。voidFunctor f => f a -> f ()() <$

これらの3つの関数は、アプリケーションファンクターの定義の基本ではありません。(<$実際、どのファンクターでも機能します。)これも、>>モナドの場合と同じです。特定のインスタンスに合わせて最適化するのを簡単にするために、クラスに参加していると思います。

適用可能なファンクターを使用する場合、ファンクターから値を「抽出」することはありません。モナドでは、これが何を>>=し、何をfoo <- ...脱糖するかです。代わりに、とを直接使用して、ラップされた値を関数に渡し<$>ます<*>。したがって、例を次のように書き直すことができます。

foo <$> parser1 <*> parser2 <*> parser3 ...

中間変数が必要な場合は、次のletステートメントを使用できます。

let var1 = parser1
    var2 = parser2
    var3 = parser3 in
foo <$> var1 <*> var2 <*> var3

あなたが正しく推測したように、pureは単なる別名ですreturn。したがって、共有構造をより明確にするために、これを次のように書き直すことができます。

pure foo <*> parser1 <*> parser2 <*> parser3

これが物事を明らかにすることを願っています。

さて、ちょっとしたメモです。解析には、アプリケーションファンクター関数を使用することをお勧めしますただし、それらがより理にかなっている場合にのみ使用する必要があります。十分に複雑なものの場合、モナドバージョン(特にdo表記を使用)は実際にはより明確になります。人々がこれを推奨する理由は

foo <$> parser1 <*> parser2 <*> parser3

より短くて読みやすい

do var1 <- parser1
   var2 <- parser2
   var3 <- parser3
   return $ foo var1 var2 var3

本質的に、これf <$> a <*> b <*> cは本質的にリフトされた関数アプリケーションのようなものです。<*>関数適用の代わりと同じように、スペース(関数適用など)の代わりになることを想像できfmapます。これにより、なぜ使用するのかを直感的に理解できるはずです。これ<$>は、のリフトバージョンのようなものです$

于 2013-03-04T19:52:49.823 に答える
13

ここでいくつかコメントできます。お役に立てば幸いです。これは、それ自体が間違っているかもしれないという私の理解を反映しています。

pure珍しい名前です。通常、関数はそれらが生成するものを参照して名前が付けられますが、pure xそれx純粋です。pure x純粋なを「運ぶ」アプリケーションファンクターを生成しますx。もちろん「キャリー」は概算です。例:pure 1 :: ZipList Intは、純粋な値ZipListを運ぶ、です。Int1

<*>、、、は関数*><*なくメソッドです(これはあなたの最初の懸念に答えます)。fそれらの型は一般的ではなく(関数の場合のように)、特定のインスタンスによって指定されるように特定的です。だからこそ、彼らは確かに、、だけ$ではflip constありませんconst。特殊なタイプは、組み合わせのセマンティクスをf指定します。通常のアプリケーションスタイルのプログラミングでは、組み合わせはアプリケーションを意味します。ただし、ファンクターを使用すると、「キャリア」タイプで表される追加の次元が存在します。には、「コンテンツ」、がありますが、「コンテキスト」もあります。ff xxf

「アプリケーションファンクター」スタイルは、エフェクトを使用して「アプリケーションスタイル」プログラミングを可能にすることを目的としていました。ファンクター、キャリア、コンテキストのプロバイダーによって表される効果。「適用可能」とは、機能的なアプリケーションの通常の適用可能なスタイルを指します。アプリケーションを示すためだけに書くf xことは、かつては革新的なアイデアでした。追加の構文はもう必要ありません(funcall f x)でした。CALLステートメントも、この余分なものもありませんでした。組み合わせアプリケーションでした...そうではなく、エフェクトを使用する場合は、エフェクトを使用してプログラミングするときに、特別な構文が必要になるようです。殺された獣が再び現れた。

そこで、効果を伴うアプリケーションプログラミングが登場し、組み合わせが単なるアプリケーションを意味するようになりました。特別な(おそらく効果的な)コンテキストで、実際そのようなコンテキストにある場合は。したがって、a :: f (t -> r)b :: f tの場合、(ほぼプレーンな)組み合わせa <*> bは、(タイプの)特定のコンテキストでの、運ばれるコンテンツ(またはタイプt -> rt)のアプリケーションです。f

モナドとの主な違いは、モナドは非線形であるということです。の

do {  x        <-  a
   ;     y     <-  b x
   ;        z  <-  c x y
   ;               return 
     (x, y, z) }

計算b xはに依存しx、とc x yの両方xに依存しyます。関数はネストされています:

a >>= (\x ->  b x  >>= (\y ->  c x y  >>= (\z ->  .... )))

前の結果( 、 )に依存しbないc場合は、計算ステージに再パッケージ化された複合データを返すようにすることで、これをフラットにすることができます(これは2番目の懸念に対処します)。xy

a  >>= (\x       ->  b  >>= (\y-> return (x,y)))       -- `b  ` sic
   >>= (\(x,y)   ->  c  >>= (\z-> return (x,y,z)))     -- `c  `
   >>= (\(x,y,z) ->  ..... )

そして、これは本質的に適用可能なスタイルです(b、によって生成されるc値とは関係なく、事前に完全に知られています)。したがって、組み合わせによって、さらに組み合わせに必要なすべての情報を含むデータが作成され、「外部変数」が不要な場合(つまり、前の段階で生成された値に関係なく、すべての計算がすでに完全にわかっている場合)、次のことができます。このスタイルの組み合わせを使用してください。xa

しかし、モナディックチェーンにそのような「外部」変数の値に依存するブランチがある場合(つまり、モナディック計算の前の段階の結果)、それから線形チェーンを作成することはできません。それは本質的にモナディックです。


例として、その論文の最初の例は、「モナディック」がどのように機能するかを示しています

sequence :: [IO a] → IO [a]
sequence [ ] = return [ ]
sequence (c : cs) = do
  {  x       <-  c
  ;      xs  <-  sequence cs  -- `sequence cs` fully known, independent of `x`
  ;              return 
    (x : xs) }

実際には、この「フラットで線形」なスタイルで次のようにコーディングできます。

sequence :: (Applicative f) => [f a] -> f [a]
sequence []       = pure []
sequence (c : cs) = pure (:) <*> c <*> sequence cs
                  --     (:)     x     xs

ここでは、モナドが以前の結果を分岐する機能を使用することはできません。


優れたPetrPudlákの答えに関するメモ:ここでの私の「用語」では、彼pairアプリケーションなしの組み合わせです。これは、Applictive FunctorsがプレーンFunctorsに追加するものの本質は、組み合わせる能力であることを示しています。その後、アプリケーションは古き良きによって達成されます。これは、おそらくより良い名前として組み合わせファンクターを示唆しています(更新:実際には、「MonoidalFunctors」が名前です)。fmap

于 2013-03-04T21:10:09.543 に答える
8

ファンクター、Applicative、モナドは次のように表示できます。これらはすべて、一種の「効果」と「価値」を備えています。Identity(「効果」および「値」という用語は単なる概算であることに注意してください。実際には、またはのように副作用や値は必要ありませんConst。)

  • を使用Functorして内部で可能な値を変更できますがfmap、内部のエフェクトでは何もできません。
  • 、を使用すると、Applicative効果なしで値を作成でき、効果をpureシーケンスして、それらの値を内部で組み合わせることができます。ただし、効果と値は別々です。効果をシーケンス処理する場合、効果は前の効果の値に依存することはできません。<*これは、に反映され<*>ます*>。これらは効果をシーケンスし、それらの値を結合しますが、内部の値を調べることはできません。

    Applicativeこの代替関数セットを使用して定義できます。

    fmap     :: (a -> b) -> (f a -> f b)
    pureUnit :: f ()
    pair     :: f a -> f b -> f (a, b)
    -- or even with a more suggestive type  (f a, f b) -> f (a, b)
    

    pureUnit効果がない場合)そしてそれらから定義pure<*>ます(そしてその逆も同様です)。ここでpairは、2つの効果をシーケンスし、両方の値を記憶しています。Applicativeこの定義は、それがモノイダル関数であるという事実を表しています。

    pairここでfmap、、、pureUnitおよびいくつかのプリミティブな適用値で構成される任意の(有限の)式について考えてみます。使用できるルールがいくつかあります。

    fmap f . fmap g           ==>     fmap (f . g)
    pair (fmap f x) y         ==>     fmap (\(a,b) -> (f a, b)) (pair x y)
    pair x (fmap f y)         ==>     -- similar
    pair pureUnit y           ==>     fmap (\b -> ((), b)) y
    pair x pureUnit           ==>     -- similar
    pair (pair x y) z         ==>     pair x (pair y z)
    

    これらのルールを使用して、sを並べ替え、 sを外側pairにプッシュし、sを削除できるため、最終的にはそのような式を次のように変換できます。fmappureUnit

    fmap pureFunction (x1 `pair` x2 `pair` ... `pair` xn)
    

    また

    fmap pureFunction pureUnit
    

    したがって、実際には、最初にを使用してすべての効果をまとめてpairから、純粋関数を使用して内部の結果の値を変更できます。

  • を使用Monadすると、効果は前のモナディック値の値に依存する可能性があります。これはそれらをとても強力にします。

于 2013-03-04T21:53:02.063 に答える
6

すでに与えられた答えは素晴らしいですが、私が明確に説明したい小さな(っぽい)点が1つあり、それは、、およびと関係が<*あり<$ます*>

例の1つは

do var1 <- parser1
   var2 <- parser2
   var3 <- parser3
   return $ foo var1 var2 var3

これは、と書くこともできますfoo <$> parser1 <*> parser2 <*> parser3

var2の値は、とは無関係であると仮定しfooます。たとえば、空白を区切るだけです。fooその場合、この空白を無視するためだけに受け入れることも意味がありません。この場合foo、3つではなく2つのパラメーターが必要です。-notationを使用doすると、次のように記述できます。

do var1 <- parser1
   parser2
   var3 <- parser3
   return $ foo var1 var3

<$>のみを使用してこれを記述したい場合<*>、これらの同等の式の1つのようなものにする必要があります。

(\x _ z -> foo x z) <$> parser1 <*> parser2 <*> parser3
(\x _ -> foo x) <$> parser1 <*> parser2 <*> parser3
(\x -> const (foo x)) <$> parser1 <*> parser2 <*> parser3
(const  . foo) <$> parser1 <*> parser2 <*> parser3

しかし、それはより多くの議論を正しく行うのはちょっと難しいです!

ただし、と書くこともできますfoo <$> parser1 <* parser2 <*> parser3fooとの結果が供給されるセマンティック関数を、betweenの結果parser1parser3無視して呼び出すことができます。parser2の欠如は>、無視していることを示すことを意味します。

の結果を無視したいが、他の2つの結果を使用したい場合は、の代わりにを使用して、parser1同様にを書くことができます。foo <$ parser1 <*> parser2 <*> parser3<$<$>

私はこれまで多くの用途を見つけたことがありません。*>通常、;id <$ p1 <*> p2の結果を無視し、 ;p1で解析するパーサー用に記述します。p2これを次のように書くこともできますがp1 *> p2、それはコードの読者の認知的負荷を増大させます。

私はパーサーのためだけにこの考え方を学びましたが、後で一般化されてApplicativesになりました。しかし、この表記はuuparsingライブラリから来ていると思います; 少なくとも10年以上前にユトレヒトで使用しました。

于 2013-03-05T12:27:30.223 に答える
3

非常に役立つ既存の回答にいくつか追加/言い換えたいと思います。

Applicativeは「静的」です。ではpure f <*> a <*> bbに依存しないため、静的に分析aできます。これは、前の質問への回答で私が示しようとしていたことです(ただし、失敗したと思います-申し訳ありません)-実際にはパーサーの連続的な依存関係がなかったため、モナドは必要ありませんでした。

モナドがテーブルにもたらす主な違いは、、(>>=) :: Monad m => m a -> (a -> m b) -> m aまたは、またはjoin :: Monad m => m (m a)です。x <- y内部do表記がある場合は常に、を使用していることに注意してください>>=。これらは、モナドを使用すると、モナドの「内部」の値を使用して、「動的に」新しいモナドを生成できることを示しています。これは、Applicativeでは実行できません。例:

-- parse two in a row of the same character
char             >>= \c1 ->
char             >>= \c2 ->
guard (c1 == c2) >>
return c1

-- parse a digit followed by a number of chars equal to that digit
--   assuming: 1) `digit`s value is an Int,
--             2) there's a `manyN` combinator
-- examples:  "3abcdef"  -> Just {rest: "def", chars: "abc"}
--            "14abcdef" -> Nothing
digit        >>= \d -> 
manyN d char 
-- note how the value from the first parser is pumped into 
--   creating the second parser

-- creating 'half' of a cartesian product
[1 .. 10] >>= \x ->
[1 .. x]  >>= \y ->
return (x, y)

最後に、Applicativesは、@ WillNessで言及されているように、リフトされた関数アプリケーションを有効にします。「中間」の結果がどのように見えるかを理解するために、通常の関数アプリケーションとリフトされた関数アプリケーションの類似点を確認できます。仮定add2 = (+) :: Int -> Int -> Int

-- normal function application
add2 :: Int -> Int -> Int
add2 3 :: Int -> Int
(add2 3) 4 :: Int

-- lifted function application
pure add2 :: [] (Int -> Int -> Int)
pure add2 <*> pure 3 :: [] (Int -> Int)
pure add2 <*> pure 3 <*> pure 4 :: [] Int

-- more useful example
[(+1), (*2)]
[(+1), (*2)] <*> [1 .. 5]
[(+1), (*2)] <*> [1 .. 5] <*> [3 .. 8]

残念ながら、...イライラpure add2 <*> pure 3するのと同じ理由で、結果を意味のある形で印刷することはできません。また、Applicativeのハンドルを取得するために、とその型クラスインスタンスをadd2確認することもできます。Identity

于 2013-03-05T20:56:13.637 に答える