この記事では、モナド全般について理解することなく、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 <- exprI
、
return ()
終了します。 各命令では、矢印の左側にあるタイプ
t
サンプル(ほとんどの場合は単なる変数)があり、右側にあるのはタイプ
IO t
の式です。 サンプルに関連付けられた変数は、後続の指示で使用できます。 タイプが
IO t
と異なる式を使用する場合は、
xI <- return (exprI)
を記述する必要があります。 関数は
return :: a -> IO a
を
return :: 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つのルールでコードを簡素化します。
_ <- x
代わりに、 _ <- x
だけを書くことができます。- 最後から2番目の行に接続矢印(
<-
)がなく、式のタイプがIO ()
である場合、 return ()
ある最後の行を削除できます。 x <- return y
はlet 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
ブロックの最後のステートメントを
if
で
do
、各ブランチで
do
を続行する
do
があります。 唯一の微妙な点は、
else
が
if
より少なくとも1スペース分インデントしなければならないということ
else
。 これはHaskellの定義の誤りと広く見なされていますが、現時点ではこの余分なスペースは不可欠です。
休息
HaskellのI / Oを知らずに読み始めてここに来た場合は、休憩することをお勧めします(お茶とケーキを飲む;あなたはそれに値する)。 上記の機能は、命令型言語でできることのすべてであり、有用な出発点です。 関数型プログラミングが関数を値として扱うよりはるかに効率的な方法を提供するように、値とI / Oアクションの両方を考慮することができます。これについては、この記事の残りで扱います。
IO値を操作する
これまで、すべての命令はすぐに実行されましたが、IOタイプの変数を作成することもできます。 上記の
title
関数を使用して、次のように記述できます。
メイン:: IO()
メイン=行う
let x = title "ようこそ"
x
x
x
<-
を介してアクションを実行する代わりに、
IO
自体の値を変数
x
ます。
x
は
IO ()
型であるため、行に
を記述して、それに記述されたアクションを実行できます。
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プログラムは通常、純粋な関数を呼び出す外部アクションラッパーで構成されます。 前の例では、
main
と
operateFile
はシェルの一部であり、
operate
および使用するすべての関数は純粋です。 一般的な設計原則として、アクションレイヤーをできるだけ薄くするようにしてください。 シェルは必要な入力を簡単に完了し、クリーンな部分に主な作業を配置します。 Haskellで明示的なI / Oを使用する必要がありますが、最小限に抑える必要があります-純粋なHaskellははるかにきれいです。
次は何ですか
これで、プログラムに必要なI / Oを実行する準備が整いました。 スキルを強化するには、次のリストから何かをすることをお勧めします。
- Haskellで多くのコードを記述します。
- 「Haskellでのプログラミング」の8章と9章を読んでください。 セクション8.1から8.4について考えるために約6時間を費やすことを期待してください(わずかな怪我で病院に行くのは良いことです)。
- Monads as containerを読んでください。モナドの優れた入門書です。
- モナドの法則に関するドキュメントを見て、この記事でそれらを使用した場所を見つけてください。
- Control.Monadのすべての関数のドキュメントを読んで、それらを実装してから、プログラムを書くときにそれらを使用してください。
- 状態モナドを実装して使用します 。