Haskell I / Oモナドなしの説明

この記事では、モナド全般について理解することなく、Haskellで入出力を行う方法を説明します。 最も単純な例から始めて、徐々に複雑な例に移ります。 記事を最後まで読むことも、セクションの後に停止することもできます。以降の各セクションでは、新しいタスクに対処できます。 Graham Hattonの著書"Programming in Haskell"の第1章から第6章のボリュームで、Haskellの基礎の紹介を想定しています。 [注 翻訳者:「はじめに」、「最初のステップ」、「タイプとクラス」、「関数の定義」、「リストからの選択」、「再帰関数」の章

主な機能


このチュートリアルでは、4つの標準I / O関数を使用します。

シンプルなI / O


I / Oの最も単純で便利な形式:ファイルを読み取り、その内容で何かを実行し、結果をファイルに書き込みます。
メイン:: IO()
メイン=行う
    src <-readFile "file.in"
    writeFile "file.out"(srcを操作)

操作::文字列->文字列
操作= ...はあなたの機能です

このプログラムは、file.inを読み取り、その内容に対して機能する関数を実行し、結果をfile.outに書き込みます。 main関数にはすべてのI / Oが含まれ、operate関数はpureです。 オペレーションを作成するとき、I / Oの詳細を理解する必要はありません。 Haskellでのプログラミングの最初の2年間は、このモデルのみを使用しましたが、それで十分でした。

アクションリスト


前のセクションで説明したテンプレートではタスクに不十分な場合、次のステップはアクションのリストを使用することです。 メイン関数は次のように書くことができます。
メイン:: IO()
メイン=行う
     x1 <-expr1
     x2 <-expr2
     ...
     xN <-exprN
    リターン()

最初にdoキーワードがあり、次に命令xI <- exprIシーケンスがxI <- exprIreturn ()終了します。 各命令では、矢印の左側にあるタイプtサンプル(ほとんどの場合は単なる変数)があり、右側にあるのはタイプIO tの式です。 サンプルに関連付けられた変数は、後続の指示で使用できます。 タイプがIO tと異なる式を使用する場合は、 xI <- return (exprI)を記述する必要があります。 関数はreturn :: a -> IO areturn :: a -> IO a任意の値を取り、それをIOタイプに「ラップ」します。

簡単な例として、コマンドライン引数を受け取り、最初の引数で指定されたファイルを読み取り、その内容を処理し、2番目の引数で指定されたファイルに書き込むプログラムを作成できます。
メイン:: IO()
メイン=行う
     [arg1、arg2] <-getArgs
     src <-readFile arg1
     res <-return(srcを操作)
     _ <-writeFile arg2 res
    リターン()

operateはまだ純粋な機能です。 do後の最初の行do 、パターンマッチングを使用do 、コマンドライン引数を抽出します。 2行目は、最初の引数で名前が指定されているファイルを読み取ります。 3行目は、 operate src純粋な値にreturnを使用しています。 4行目は、結果をファイルに書き込みます。 これは有用な結果を生まないので、 _ <-書いて無視します。

I / Oを簡素化


このアクションリストテンプレートは非常に難しく、通常は次の3つのルールでコードを簡素化します。
  1. _ <- x代わりに、 _ <- xだけを書くことができます。
  2. 最後から2番目の行に接続矢印( <- )がなく、式のタイプがIO ()である場合、 return ()ある最後の行を削除できます。
  3. x <- return ylet x = y置き換えることができます(変数名を繰り返し使用しない場合)。

これらのルールを使用して、例を書き換えることができます。
メイン:: IO()
メイン=行う
     [arg1、arg2] <-getArgs
     src <-readFile arg1
     let res = srcを操作します
     writeFile arg2 res

ネストされたI / O


これまでのところ、 main関数のみにIOタイプがありますが、このタイプの新しい関数を作成して、コードの繰り返しを避けることができます。 たとえば、美しいヘッダーを出力するヘルパー関数を作成できます。
 title ::文字列-> IO()
タイトルstr = do
     putStrLn str
     putStrLn(レプリケート(長さstr) '-')
     putStrLn ""

main内でこの関数を数回使用できます。
メイン:: IO()
メイン=行う
    タイトル「こんにちは」
    タイトル「さようなら」

IOの戻り値


これまでに作成した関数はすべてIO()型であり、I / Oを実行できますが、興味深い結果を生成することはできません。 return x値をreturn xは、 doブロックの最後の行にreturn xを書き込みます。 命令型言語のreturnとは異なり、このreturnは最後の行になければなりません。
 readArgs :: IO(文字列、文字列)
 readArgs = do
     xs <-getArgs
     let x1 = length xs> 0 then xs !!  0その他 "file.in"
     let x2 = if length xs> 1 then xs !!  1個の「file.out」
    リターン(x1、x2)

この関数は、コマンドラインの最初の2つの引数、またはコマンドラインに2つ未満の引数があった場合はデフォルト値を返します。 これでプログラムで使用できます:
メイン:: IO()
メイン=行う
     (arg1、arg2)<-readArgs
     src <-readFile arg1
     let res = srcを操作します
     writeFile arg2 res

現在、2つ未満の引数が指定されている場合、プログラムはクラッシュしませんが、デフォルトのファイル名を使用します。

I / Oアクションを選択


これまでのところ、順番に実行されるI / O命令の静的リストのみを見てきました。 ifを使用すると、実行するアクションを選択できます。 たとえば、ユーザーが引数を入力していない場合、これを報告できます。
メイン:: IO()
メイン=行う
     xs <-getArgs
     null xsの場合
         putStrLn「引数が入力されていません」
     他にやる
         putStrLn(「入力済み」++ xsを表示)

アクションを選択するには、 doブロックの最後のステートメントをifdo 、各ブランチでdoを続行するdoがあります。 唯一の微妙な点は、 elseifより少なくとも1スペース分インデントしなければならないということelse 。 これはHaskellの定義の誤りと広く見なされていますが、現時点ではこの余分なスペースは不可欠です。

休息


HaskellのI / Oを知らずに読み始めてここに来た場合は、休憩することをお勧めします(お茶とケーキを飲む;あなたはそれに値する)。 上記の機能は、命令型言語でできることのすべてであり、有用な出発点です。 関数型プログラミングが関数を値として扱うよりはるかに効率的な方法を提供するように、値とI / Oアクションの両方を考慮することができます。これについては、この記事の残りで扱います。

IO値を操作する


これまで、すべての命令はすぐに実行されましたが、IOタイプの変数を作成することもできます。 上記のtitle関数を使用して、次のように記述できます。
メイン:: IO()
メイン=行う
     let x = title "ようこそ"
     x
     x
     x

<-を介してアクションを実行する代わりに、 IO自体の値を変数xます。 xIO ()型であるため、行にを記述して、それに記述されたアクションを実行できます。 x 3回書くと、このアクションが3回実行されます。

引数としてアクションを渡す


IO値を関数の引数として渡すこともできます。 前の例では、 title "Welcome"アクションを3回実行しましたが、どうすれば50回実行できますか? アクションと数値を受け取り、このアクションを適切な回数実行する関数を作成できます。
 replicateM_ :: Int-> IO()-> IO()
 replicateM_ n act = do
     n == 0の場合、実行
        リターン()
     他にやる
        行動する
         replicateM_(n-1)行為

ここでは、停止するタイミングを決定するアクションの選択と、実行を継続する再帰を使用しました。 これで、前の例を次のように書き換えることができます。
メイン:: IO()
メイン=行う
     let x = title "ようこそ"
     replicateM_ 3 x

もちろん、命令型言語のforステートメントを使用するとreplicateM_関数と同じことができますが、Haskellの柔軟性により、新しい制御命令を定義できます。これは非常に強力なツールです。 Control.Monadで定義されているreplicateM_関数は私たちのものと似ていますが、より一般的です。 バージョンの代わりに使用できます。

データ構造のIO


IO値が引数としてどのように渡されるかを見てきましたので、リストやタプルなどのデータ構造に入れることができるのは驚くことではありません。 sequence_関数はアクションのリストを取り、それらを順番に実行します:
 sequence_ :: [IO()]-> IO()
 sequence_ xs = do
     null xsの場合
        リターン()
     他にやる
        ヘッドXS
         sequence_(テールxs)

リストに要素がない場合、 sequence_return ()終了します。 リストに要素がある場合、 sequence_head xsを使用して最初のアクションを選択して実行し、残りのtail xsリストでsequence_を呼び出しsequence_replicateM_と同様に、 sequence_より一般的な方法でControl.Monadに既に存在します。 これで、 sequence_を使用してreplicateM_を簡単に書き換えることができsequence_
 replicateM_ :: Int-> IO()-> IO()
 replicateM_ n act = sequence_(n actを複製)

パターンマッチング


Haskellでは、 null/head/tailよりもパターンマッチングを使用する方がはるかに自然です。 doブロックに命令が1つだけある場合は、 doという単語を削除できます。 たとえば、 sequence_の定義ではsequence_これは等号の後とthen後に実行できます。
 sequence_ :: [IO()]-> IO()
 sequence_ xs =
     null xsの場合
        リターン()
     他にやる
        ヘッドXS
         sequence_(テールxs)

IOを心配することなく、似たような状況のようにifをマッチングに置き換えることができます:
 sequence_ :: [IO()]-> IO()
 sequence_ [] = return()
 sequence_(x:xs)= do
     x
     sequence_ xs

最後の例


最後の例として、コマンドラインで指定された各ファイルを使用していくつかの操作を実行するとします。 学んだことを使って、次のように書くことができます。
メイン:: IO()
メイン=行う
     xs <-getArgs
     sequence_(マップoperateFile xs)

 operateFile :: FilePath-> IO()
操作ファイルx = do
     src <-readFile x
     writeFile(x ++ ".out")(srcを操作)

操作::文字列->文字列
操作= ...

プログラムでのI / Oの設計


Haskellプログラムは通常、純粋な関数を呼び出す外部アクションラッパーで構成されます。 前の例では、 mainoperateFileはシェルの一部であり、 operateおよび使用するすべての関数は純粋です。 一般的な設計原則として、アクションレイヤーをできるだけ薄くするようにしてください。 シェルは必要な入力を簡単に完了し、クリーンな部分に主な作業を配置します。 Haskellで明示的なI / Oを使用する必要がありますが、最小限に抑える必要があります-純粋なHaskellははるかにきれいです。

次は何ですか


これで、プログラムに必要なI / Oを実行する準備が整いました。 スキルを強化するには、次のリストから何かをすることをお勧めします。

Source: https://habr.com/ru/post/J80396/


All Articles