(本節の理解のためには ウォークスルー Haskell > 入出力処理も参考になる)
ファイルに書く(こちらから先に述べます):
writeFile "xxx.text" "hello haskell"
第1引数にファイル名(データ型としては FilePath)を与える。
第2引数として渡した文字列が書き込まれる。
:t writeFile ---> writeFile :: FilePath -> String -> IO ()
ファイルから読む:
readFile "xxx.txt"
replでこう呼ぶと、ファイルの名前が文字列として表示される。
ただし、(これを読みとる習慣をつけておこう)
:t readFile ---> readFile :: FilePath -> IO String
この関数が返す値は、String ではなく IO String (IOモナドにString型のデータが包れた状態、という認識をするといい)。
前に System.IOモジュールにある getLine関数を使ったが、実はこれも
:t getLine ---> getLine :: IO String
というシグニチャを持つ(こちらは引数なしの)IO String を返す関数だった。
ここから値(データ)を取り出して使う方法は2つ。
do構文の中で、変数に bindする(代入に少し似た使い方)。
do
str <- readFile "xxx.txt"
print str
関数にbindする(変数を介さず直接渡す)。
readFile "xxx.txt" >>= print
-- または 逆の書き方をする演算子も用意されている
print =<< readFile "xxx.txt"
オープンして読み書き
do
handle <- openFile "xxx.txt" ReadMode
hGetContents handle >>= print -- 変数経由で(2行に分けて)printに渡してもいい
hClose handle
(他の多くの言語と同様に)クローズを自動的に行わせる枠組みもある。
withFile :: FilePath -> IOMode -> (Handle -> IO r) -> IO r
openFileするときの引数に加えて、そのファイルハンドルに対して入出力アクションを行う関数を渡す、 という使い方がこのシグニチャから推察される。
withFile "xxx.txt" ReadMode (\h -> hGetContents h >>= print)
内部での動作は、関数をいくつか組み合わせることになることが想定される。当面は、 以下の例のようにラムダ式として書いておくのが無難だろう。 ⁺ 以下のように使うと(従来のプログラムをしてきた直観には反して)動作しない。
(withFile "xxx.txt" ReadMode hGetContents) >>= print)
-- または
do
a <- withFile "xxx.txt" ReadMode hGetContents
print a
ファイルに時間間隔をおいて時刻を書き込む:
いくつかの技の組み合わせになる。
import System.IO --> hPutStrLn::Handle -> String -> IO ()
import Control.Concurrent --> threadDelay::Int -> IO ()
import Data.Time.LocalTime --> getZonedTime::IO ZonedTime
chkTime::Int -> Handle -> IO () -- 再帰関数
chkTime 0 _ = return ()
chkTime n h = do
now<-getZonedTime
hPutStrLn h $ show now
threadDelay $ 2*1000*1000 -- 時間間隔 マイクロ秒
chkTime (n - 1) h -- nを1つ減らして再帰
main::IO ()
main=withFile "yyy.txt" WriteMode $ chkTime 10
再帰を使わないバージョンも付記しておく。
replecate で、ユニットのリスト(型としては
[()])を作る。その結果、mainの型は IO [()]
となる。
putTime::Handle->()->IO ()
putTime h p=do
now<-getZonedTime
hPutStrLn h $ show now
threadDelay $ 2*1000*1000
return ()
chkTime::Int->Handle->IO [()]
chkTime n h=mapM (putTime h) $ replicate n ()
main::IO [()]
main=withFile "yyy.txt" WriteMode $ chkTime 10
上記のコードでファイル名を文字列の形で与えているが、そのファイルは カレントディレクトリ(カレントフォルダ)に置かれていることが前提となる。
それ以外の場所に置かれたファイルを指定するためには2つの方法があることを知っておこう。
パス名で表記する。ex:
"c:/Users/myname/Documents/a.txt"
"subfolder/b.txt"
カレントフォルダを(プログラムを起動する前に、またはREPL内で)移動する。
以下の2つのREPLコマンドを知っておくといい。
:!cd
-- カレントフォルダを表示する(上はWindowsの場合。Unixなら pwdコマンド)
-- :! は REPL内から外(REPLを呼び出したシェル)のコマンドを呼び出す方法
:cd somefolder
-- REPL内でカレントフォルダの移動
Haskell処理系ではマジックコメントという概念はないようです。
以下のようなコード(ソースはここ)を試してみて下さい。
main::IO ()
main=do
print "はろー" -- 化ける
putStrLn "はろー" -- 化けない
putStr, putStrLn の系統の名前のメソッドを使えば日本語でも大丈夫、という理解で当面は大丈夫でしょう。ちなみに
:t print --> print :: Show a => a -> IO ()
:t putStrLn --> putStrLn :: String -> IO ()
この2つの関数のシグニチャもチェック。
文字列の扱いについては、様々なデータ型が用意されているので、 より深い理解と活用のためには、以下のようなページを参照下さい。