なぜこれらすべてのファンクターとモナドが必要なのですか?

Haskellに関する記事では、ファンクター、特にモナドが頻繁に見つかります。

しばしば、「いくつかの新しいモナドについて可能な限り」と「有用な何かについて書く」というコメントが少なくとも見つかることがあります。

私の意見では、これは、これらのすべてのファンクターとモナドが必要な理由を人々が理解していないことを示しています。



この記事は、関数型言語、特にHaskellの力がファンクターとモナドの力でもあることを示す試みです。





ネットデータ



これは、非常に人為的で、おそらく役に立たない例を使用してこれを示すことを試みますが、共通のコードを使用してそれを再利用することの重要性に重点が置かれます。



「クリーン」という用語は、プログラミングでオーバーロードされます。

たとえば、「Rubyは純粋にオブジェクト言語です」というフレーズは、「Rubyはすべてがオブジェクトである言語です」と理解しています。

しかし、「Haskellは純粋な関数型言語です」という語句は、「Haskellは副作用のない関数型言語です」と理解する必要があります。

この記事では、別の文脈で「クリーン」という用語を使用します。

「クリーンデータ」は、受信したいデータです。

基本的に、プリミティブ型は数値、文字列、場合によってはより複雑なもの、たとえば画像や複数の値です。

したがって、「ダーティデータ」は、必要な情報に加えて、追加情報を含むデータです。



ここでプログラムを作成します。

module Main where foo = undefined --   main :: IO () main = do putStrLn "Input a: " a <- getLine --  1    putStrLn "Input b: " b <- getLine --  2    print (foo ab) --   
      
      





プログラムは簡単に不名誉です-ユーザーに2行を入力して、計算結果を表示するように依頼します。

Haskellはすでにコードをコンパイルできますが、関数fooはまだ定義されていないことがわかります(常にプログラムがクラッシュします)。



次に、「クリーン」データのみを使用して、関数をより詳細に書き換えます。

 pure1arg :: Int -> Int pure1arg = (+ 1) --   ,   1  pure2args :: Int -> Int -> Int pure2args = (+) --  ,   2  unsafe2args :: Int -> Int -> Int unsafe2args = div --   ,   2  foo :: String -> String -> Int foo ab = unsafe2args extraUnsafeE unsafeC --     ,     where unsafeA :: Int unsafeA = read a --           unsafeB :: Int unsafeB = read b --  unsafeA    unsafeC :: Int unsafeC = pure1arg unsafeB --    1     reallyUnsafeD :: Int reallyUnsafeD = pure2args unsafeA unsafeC --    2     extraUnsafeE :: Int extraUnsafeE = unsafe2args unsafeA reallyUnsafeD --    2 .  2   .
      
      





ご覧のとおり、関数foo



は基本的に[何があっても]整数の除算と合計の混合物です。

ほとんどの関数型プログラミング言語では、クリーンなデータに基づいて関数を簡単かつ簡単に作成できます。



すべてが素晴らしいように思えます-シンプルでエレガントなプログラム。 しかし、ネツシキ!

関数の結果は、私たちが望むよりもはるかに複雑です。

私たちが理解しているように、 0



で割ることは不可能であり、ユーザーは数字ではなく左の文字列を入力でき、文字列を数字に変換するときにエラーをスローできます。 私たちのコードは安全ではないことが判明しました。

このような問題を解決するための必須のアプローチは、ブランチを使用するか例外を使用するかの2つのグループに分けられます。 多くの場合、両方のアプローチが組み合わされています。

これらのアプローチは非常に効果的であるため、主に関数型言語で使用されます。

それに直面しましょう-Haskellには例外がありますが、それらは未発達であり、改革する必要があり、最良の方法で捉えられていません。 そして最も重要なこと-ほとんどの場合、それらは単に必要ではありません。

しかし、それ以上ではありません-それは可能です。

したがって、分岐と例外を使用してコードを書き直そうとします。

 module Main where import Control.Exception (IOException, catch) printError :: IOException -> IO () printError = print pure2args :: Int -> Int -> Int pure2args = (+) pure1arg :: Int -> Int pure1arg = (+ 1) unsafe2args :: Int -> Int -> Int unsafe2args ab = if b == 0 then error "Error 'unsafe2args' : wrong 2nd argument = 0" --unsafe source of IOException else div ab foo :: String -> String -> Int foo ab = unsafe2args extraUnsafeE unsafeC where unsafeA :: Int unsafeA = read a --unsafe source of IOException unsafeB :: Int unsafeB = read b --unsafe source of IOException unsafeC :: Int unsafeC = pure1arg unsafeB reallyUnsafeD :: Int reallyUnsafeD = pure2args unsafeA unsafeC extraUnsafeE :: Int extraUnsafeE = unsafe2args unsafeA reallyUnsafeD main :: IO () main = do putStrLn "Input a: " a <- getLine putStrLn "Input b: " b <- getLine catch (print (foo ab)) printError --      IOException
      
      





汚れたデータ



Haskell(および多くの関数型言語)には、このような問題にふさわしい答えがあります。

主な強みは代数データ型にあります。



上記の例を考慮すると、機能が低下する可能性があることは明らかです。

解決策は、null許容のデータ型を使用することです。

ML言語とScalaでは、この型はOption



と呼ばれ、HaskellではMaybe a



と呼ばれます。

 import Prelude hiding (Maybe) --       .       data Maybe a = Nothing | Just a deriving Show
      
      





deriving



部分に注意を払うのではなく、コンパイラにデータ型をそれ自体で文字列に変換できるように依頼するだけです。

すなわち

 show Nothing == "Nothing" show (Just 3) == "Just 3"
      
      





データがNothing



場合はデータタイプはNothing



、データがある場合はJust a



です。

ご覧のとおり、データ型は冗長な情報が含まれているため「ダーティ」です。

関数をより正しく、より安全に、例外なく書き直しましょう。



まず第一に、安全なアナログへの落下を引き起こした機能を置き換えます。

 maybeResult2args :: Int -> Int -> Maybe Int maybeResult2args ab = if b == 0 then Nothing --safe else Just (div ab) ... maybeA :: Maybe Int maybeA = readMaybe a --safe maybeB :: Maybe Int maybeB = readMaybe b --safe
      
      





これで、これらの関数は、フォールする代わりに、結果がNothing



Just



ます。



しかし、残りのコードはこれらの関数に依存しています。 何度もテストされた機能を含め、ほとんどすべての機能を変更する必要があります。

 pure2args :: Int -> Int -> Int pure2args = (+) safePure2args :: Maybe Int -> Maybe Int -> Maybe Int safePure2args ab = case a of Nothing -> Nothing Just a' -> case b of Nothing -> Nothing Just b' -> Just (pure2args a' b') pure1arg :: Int -> Int pure1arg = (+ 1) safePure1arg :: Maybe Int -> Maybe Int safePure1arg a = case a of Nothing -> Nothing Just a' -> Just (pure1arg a') maybeResult2args :: Int -> Int -> Maybe Int maybeResult2args ab = if b == 0 then Nothing else Just (div ab) foo :: String -> String -> Maybe Int foo ab = case maybeE of Nothing -> Nothing Just e -> case maybeC of Nothing -> Nothing Just c -> maybeResult2args ec where maybeA :: Maybe Int maybeA = readMaybe a maybeB :: Maybe Int maybeB = readMaybe b maybeC :: Maybe Int maybeC = safePure1arg maybeB maybeD :: Maybe Int maybeD = safePure2args maybeA maybeC maybeE = case maybeA of Nothing -> Nothing Just a1 -> case maybeD of Nothing -> Nothing Just d -> maybeResult2args a1 d printMaybe :: Show a => Maybe a -> IO () printMaybe Nothing = print "Something Wrong" printMaybe (Just a) = print a main :: IO () main = do putStrLn "Input a: " a <- getLine putStrLn "Input b: " b <- getLine printMaybe (foo ab)
      
      





ご覧のとおり、単純なプログラムは非常にモンスターのようなコードになっています。

多くのラッパー関数、多くの冗長コード、多くの変更。

しかし、これはまさに多くの関数型プログラミング言語が停止する場所です。

これらの言語では、多くのADTを作成する可能性があるにもかかわらず、コードでADTがあまり使用されない理由を理解できます。



ADTと一緒に暮らすことはできますが、同様のバッカナリアはありませんか それはあなたができることが判明しました。



ファンクター



最初は、ファンクターが助けになります。



ファンクターは、 fmap



関数が存在するデータ型です。

 class Functor f where fmap :: (a -> b) -> fa -> fb
      
      





その中置同義語と同様に:

 (<$>) :: Functor f => (a -> b) -> fa -> fb (<$>) = fmap
      
      





データ型のすべての値に対して、次の条件が常に満たされるようにします。



アイデンティティ条件:

fmap id == id





構成条件:

fmap (f . g) == fmap f . fmap g







id



は恒等関数です

  id :: a -> a id x = x
      
      





および(.)



-機能構成

  (.) :: (b -> c) -> (a -> b) -> a -> c f . g = \x -> f (gx)
      
      





ファンクターは、特別なfmap



関数を作成した型クラスです。 彼女の引数を見てみましょう-彼女は1つの「クリーンな」関数a- a -> b



を取り、「ダーティ」ファンクター値fa



を取り、出力でファンクター値fb



を取得します。



Maybe



データ型はファンクターです。 ファンクタの法則に違反しないように、 Maybe



タイプのインスタンス(インスタンス)を作成しましょう。

 instance Functor Maybe where fmap _ Nothing = Nothing fmap f (Just a) = Just (fa)
      
      





Maybe



ファンクタで純粋な関数をどのように使用しますか? 非常にシンプル:

 safePure1arg :: Maybe Int -> Maybe Int safePure1arg = fmap pure1arg
      
      





ここに主なものがありますpure1arg



関数を書き直さなかったため、バグを再度テストする必要はなく、すべての点で普遍的でクリーンなままですが、数字ではなくnull値をpure1arg



する安全なバージョンを簡単に作成しました数字。



ただし、 safePure2args



を書き換えようとしているときにファンクターを使用したい場合、失敗します。

ファンクターは、単一のfunctor-dirty引数を持つ関数でのみ機能します。

いくつかのパラメーターを持つ関数に対して何をすべきか?



応用ファンクター



ここで、適用可能なファンクターが私たちの助けになります:



適用可能なファンクターは、2つの関数が定義されているファンクターです: pure



および(<*>)





 class Functor f => Applicative f where pure :: a -> fa (<*>) :: f (a -> b) -> fa -> fb
      
      





そのため、同じデータ型の値については、次のルールが常に真になります。



アイデンティティ条件:

pure id <*> v == v





構成条件:

pure (.) <*> u <*> v <*> w == u <*> (v <*> w)





準同型条件:

pure f <*> pure x == pure (fx)





交換条件:

u <*> pure y == pure ($ y) <*> u







ファンクターと適用ファンクターの主な違いは、ファンクターがファンクター値を介して純粋な関数をドラッグするのに対し、アプリケーションファンクターはファンクター値を介してファンクター関数f (a -> b)



a- f (a -> b)



をドラッグできることです。



たぶん、適用ファンクターであり、次のように定義されます:

 instance Applicative Maybe where pure = Just Nothing <*> _ = Nothing _ <*> Nothing = Nothing (Just f) <*> (Just a) = Just (fa)
      
      





safePure2args



を書き換えるときがsafePure2args





基本的に、関数は最初の引数のファンクターfmap



と残りの引数の適用可能な文字列を組み合わせて書き直されます。

 safePure2args :: Maybe Int -> Maybe Int -> Maybe Int safePure2args ab = pure2args <$> a <*> b
      
      





ただし、排他的に適用可能な関数(モナドスタイル)を使用して関数を書き換えることができます。まず、純粋に適用可能な「純粋な」関数を作成し、引数を適用可能にストリング化します。

 safePure2args :: Maybe Int -> Maybe Int -> Maybe Int safePure2args ab = (pure pure2args) <*> a <*> b
      
      





いいね!

たぶん、 maybeE



ファンクターを使用してmaybeE



関数を同時に書き換えることができますか? ああ。



モナド



maybeResult2args



関数のシグネチャに注目しましょう。

maybeResult2args :: Int -> Int -> Maybe Int





この関数は、入力として「クリーン」な引数を取り、出力に「ダーティ」な結果を生成します。

そのため、実際のプログラミングの大部分では、最も頻繁に遭遇するのはまさにそのような関数です-入力として「純粋な」引数を取り、結果は「ダーティー」です。

そして、そのような関数がいくつかあると、モナドはそれらを一緒に結合するのに役立ちます。



モナドは、 return



(>>=)



関数が存在するデータ型です。

 class Monad m where return :: a -> ma (>>=) :: ma -> (a -> mb) -> mb
      
      





タイプの値についてルールが満たされるように:



左身元:

return a >>= k == ka





正しいアイデンティティ:

m >>= return == m





結合性:

m >>= (\x -> kx >>= h) == (m >>= k) >>= h







便宜上、引数の順序が逆の追加の関数があります。

 (=<<) :: Monad m => (a -> mb) -> ma -> mb (=<<) = flip (>>=)
      
      





どこで

 flip :: (a -> b -> c) -> b -> a -> c flip fab = fba
      
      





Maybe



型はモナドであることを理解しています。つまり、そのインスタンス(インスタンス)を定義できます。

 instance Monad Maybe where return = Just (Just x) >>= k = kx Nothing >>= _ = Nothing
      
      





ところで、内部コンテンツと署名を詳しく見ると、次のことがわかります。

pure == return





fmap f xs == xs >>= return . f







maybeE



関数を書き換える時がmaybeE





  maybeE = maybeA >>= (\a1 -> maybeD >>= (maybeResult2args a1))
      
      





はい、それはそれほど美しくないことが判明しました。 これは、1つの変数に対してモナドが美しく書かれているためです。 幸いなことに、多くの追加機能があります。

bind2



関数を書くことができます

 bind2 :: Monad m => (a -> b -> mc) -> ma -> mb -> mc bind2 mf mx my = do x <- mx y <- my mf xy
      
      





  maybeE = bind2 maybeResult2args maybeA maybeD
      
      





または、関数liftM2



を使用してjoin





 liftM2 :: Monad m => (a1 -> a2 -> r) -> m a1 -> m a2 -> mr join :: Monad m => m (ma) -> ma maybeE = join $ liftM2 maybeResult2args maybeA maybeD
      
      





極端な場合、 do



表記を使用しdo



モナドに構文糖を使用できます。

  maybeE = do a1 <- maybeA d <- maybeD maybeResult2args a1 d
      
      





ポンドとモナドの使用の違い



メイン関数を1つの形式に縮小すると、次のように表示されます。

 (<$>) :: Functor f => (a -> b) -> fa -> fb (<*>) :: Applicative f => f (a -> b) -> fa -> fb (=<<) :: Monad f => (a -> fb) -> fa -> fb
      
      





すべては関数にダーティ値を渡すために使用されますが、関数は入力でクリーンな値を期待します。

ファンターは「クリーン」機能を使用します。

適用可能なファンクターは、「汚染」内の「純粋な」関数です。

モナドは、出力で汚い意味を持つ関数を使用します。



ルーチンなしのプログラム



最後に、プログラム全体を完全かつ正確に書き換えることができます。

 module Main where import Control.Monad import Control.Applicative import Text.Read (readMaybe) bind2 :: Monad m => (a -> b -> mc) -> ma -> mb -> mc bind2 mf mx my = do x <- mx y <- my mf xy pure2args :: Int -> Int -> Int pure2args = (+) pure1arg :: Int -> Int pure1arg = (+ 1) maybeResult2args :: Int -> Int -> Maybe Int maybeResult2args ab = if b == 0 then Nothing --safe else Just (div ab) foo :: String -> String -> Maybe Int foo ab = bind2 maybeResult2args maybeE maybeC where maybeA :: Maybe Int maybeA = readMaybe a --safe maybeB :: Maybe Int maybeB = readMaybe b --safe maybeC :: Maybe Int maybeC = fmap pure1arg maybeB maybeD :: Maybe Int maybeD = pure2args <$> maybeA <*> maybeC maybeE :: Maybe Int maybeE = bind2 maybeResult2args maybeA maybeD printMaybe :: Show a => Maybe a -> IO () printMaybe Nothing = print "Something Wrong" printMaybe (Just a) = print a main :: IO () main = do putStrLn "Input a: " a <- getLine putStrLn "Input b: " b <- getLine printMaybe (foo ab)
      
      





コードはシンプルで明確になりました!

同時に、私たちは少しの安全をあきらめませんでした!

ただし、コードはほとんど変更しませんでした!

同時に、純粋な関数は純粋なままでした!

同時に、ルーチンは回避されました!



おわりに



ファンクターやモナドなしで機能的な世界に住むことは可能ですか? できます。

しかし、代数的データ型の力を最大限に活用したい場合、ファンクタとモナドを使用して、さまざまな関数の便利な機能構成を作成する必要があります。

これは、ルーチンの優れた解決策であり、短く、理解可能で、頻繁に再利用されるコードへの道です!



PSさまざまなタイプのデータについて、「クリーン」および「ダーティ」データタイプとの類推は完全に適切ではないことを理解する必要があります。

たとえば、リストの場合

fmap = map





モナド:

  a = do c <- cs d <- ds return (zet cd)
      
      





本当に

 a = [zet cd | c <- cs, d <- ds]
      
      





これは、一目で必ずしも明らかではありません。



All Articles