Haskell入門から学ぶ~IO型～

Haskellのド素人が入門書を手に独学で学んだことを忘れないために綴っていくシリーズ。

#### Haskell入門 第4章から抜粋
入門Haskellプログラミングも参考にしてます。

### IO型とは（まとめ）
1. 型クラス`Monad`のインスタンスの1種
2. 引数を持たない関数を扱う
3. 戻り値を持たない関数を扱う
4. 参照透過でない関数を扱う

### IO型とは（詳細）
`()`は空のタプルとみることができるので`Nothing`と同じと考えられる。

**型クラスMonadの定義**

| メソッド|定義|説明 |
|:---------|:----|:----|
| return|a -> m a|`汎用型a`を`Monad a`に変換する|
| (>>=) |m a -> (a -> m b) -> m b|`Monad a`に関数を適用後`Monad b`を返却|

```hs
Prelude> :i Monad
class Applicative m => Monad (m :: * -> *) where
  (>>=) :: m a -> (a -> m b) -> m b
  (>>) :: m a -> m b -> m b
  return :: a -> m a
  fail :: String -> m a
  {-# MINIMAL (>>=) #-}
        -- Defined in ‘GHC.Base’
instance Monad (Either e) -- Defined in ‘Data.Either’
instance Monad [] -- Defined in ‘GHC.Base’
instance Monad Maybe -- Defined in ‘GHC.Base’
instance Monad IO -- Defined in ‘GHC.Base’
instance Monad ((->) r) -- Defined in ‘GHC.Base’
instance Monoid a => Monad ((,) a) -- Defined in ‘GHC.Base’
```
**代表的なIOインスタンスの関数**

```hs
Prelude> -- putStrLnは文字列を受け取るが値を返さない
Prelude> :t putStrLn
putStrLn :: String -> IO ()
Prelude> -- getLineは引数をとらずにIO String型を返す
Prelude> :t getLine
getLine :: IO String
Prelude> -- randomRIOはランダム値（参照透過ではない値）を返す
Prelude> import System.Random
Prelude System.Random> :t randomRIO
randomRIO :: Random a => (a, a) -> IO a
```
#### IOアクション
`getLine`と`putStrLn`を使って簡単なプログラムを実装。

| メソッド|定義|説明 |
|:---------|:----|:----|
| getLine|IO String|IO String型を返す|
| putStrLn|String -> IO ()|文字列を受け取るが値を返さない|

```hs
Prelude> :t getLine >>= putStrLn
getLine >>= putStrLn :: IO ()
Prelude> let echo = getLine >>= putStrLn :: IO ()
Prelude> echo
HogeHoge
HogeHoge
```
**解説**
1. `getLine`は`IO String`を返す
2. `>>=`は`IOアクション`を連結する
3. `putStrLn`は`IO String`を引数にとる

#### >>=演算子でIOアクションを複数連結
二つ以上のIOアクションを連結する場合はラムダ関数を使えば実現できる。

**ラムダ関数を使った実装例**

```hs
Prelude> -- ラムダ関数を使った実装例（ウォーミングアップ）
Prelude> getLine >>= (\s -> putStrLn s)
hogehoge
hogehoge
Prelude> -- ラムダ関数を使った実装例（実践）
Prelude> let joinLine = getLine >>= (\s1 -> getLine >>= (\s2 -> putStrLn $ "Input1 : " ++ s1 ++ ", Input2 : " ++ s2))
Prelude> joinLine
ABCDE
12345
Input1 : ABCDE, Input2 : 12345
```
**>>= と returnを使って実装**

```hs
Prelude> let joinLines = getLine >>= \s1 -> getLine >>= \s2 -> (return $ "Input1 : " ++ s1 ++ ", Input2 : " ++ s2) >>= n
Prelude> joinLines
Dog
Cat
Input1 : Dog, Input2 : Cat
```
### >>=・returnからdo記法へ
上記サンプルでは`getLine`を2回呼び出した後、`putStrLn`を呼び出している。
**上記サンプルをレイアウト記法を使って書き直してみる。**

```hs
Prelude> :{
Prelude| main :: IO ()
Prelude| main =
Prelude|   getLine >>= \x ->  -- 戻り値1をラムダ関数で受け取る
Prelude|   getLine >>= \y ->  -- 戻り値2をラムダ関数で受け取る
Prelude|   putStrLn ("Input1 : " ++ x) >>  -- IO x::Stringを受け取る
Prelude|   putStrLn ("Input2 : " ++ y)     -- IO y::Stringを受け取る
Prelude| :}
Prelude>
Prelude> main
Hoge
Hoge
Input1 : Hoge
Input2 : Hoge
```

**上記サンプルをdo記法に書き換える**

```hs
Prelude> :{
Prelude| main :: IO ()
Prelude| main = do
Prelude|   x <- getLine -- IO StringからStringを取得する
Prelude|   y <- getLine -- IO StringからStringを取得する
Prelude|   putStrLn ("Input1 : " ++ x) -- Stringを受け取る
Prelude|   putStrLn ("Input2 : " ++ y) -- Stringを受け取る
Prelude| :}
Prelude> main
Hoge
Fuga
Input1 : Hoge
Input2 : Fuga
```

#### 参照透過でない関数の実装
`randomRIO`は最小値と最大値を引数にランダム値を生成する関数。
実行するたびに違う値が返却されるので参照透過ではない。

```hs
Prelude System.Random> :{
Prelude System.Random| min :: Int
Prelude System.Random| min = 1
Prelude System.Random| max :: Int
Prelude System.Random| max = 6
Prelude System.Random| main :: IO ()
Prelude System.Random| main = do
Prelude System.Random|  dice <- randomRIO (min, max)
Prelude System.Random|  putStrLn(show dice)
Prelude System.Random| :}
Prelude System.Random> main
4
Prelude System.Random> main
6
```

#### do記法
main関数（「入門Haskellプログラミング」曰く関数ではないとのことだけど）ではよく、`do記法`が用いられているが、`do記法`の何が嬉しいのか。

| 記号       |概要|
|:-----------------|:------------------|
| do表記|IOアクションを組み合わせることができる便利な記法|
| a <- IO a |`IO a`から`a型`だけを取り出すことができる|
| let a = b |通常の型を扱う|


```hs
Prelude> :{
Prelude| helloPerson :: String -> String
Prelude| helloPerson name = "Hello " ++ name ++ "!"
Prelude|
Prelude| hello :: IO ()
Prelude| hello = do
Prelude|     putStrLn "Hello! What is your name?"
Prelude|     -- getLine <-を使って値をbind
Prelude|     -- nameへはIO StringではなくString型が入る
Prelude|     name <- getLine
Prelude|     -- =を使って値をbind
Prelude|     let statement = helloPerson name
Prelude|     putStrLn statement
Prelude| :}
Prelude>
Prelude> hello
Hello! What is your name?
Jonasan
Hello Jonasan!
```

#### コマンドライン引数
| メソッド|定義|説明 |
|:---------|:----|:----|
| getArgs|getArgs :: IO [String]|`IO [String]`を受け取る|

#### 環境変数

| メソッド|定義|説明 |
|:---------|:----|:----|
| getEnv x|getEnv :: String -> IO String |指定された環境変数を取得しIO Stringへ詰めこむ|

```hs
Prelude System.Environment> import System.Environment
Prelude System.Environment> :{
Prelude System.Environment| main :: IO ()
Prelude System.Environment| main = do
Prelude System.Environment|   path <- getEnv "PATH"
Prelude System.Environment|   putStrLn path
Prelude System.Environment| :}
Prelude System.Environment> main
C:\sr\global-project\. -- 以下省略
```

#### 標準入出力関数

| メソッド|定義|説明 |
|:---------|:----|:----|
|putChar|putChar :: Char -> IO ()| 標準出力へ1文字出力|
|putStr|putStr:: String-> IO ()| 標準出力へ文字列出力|
|putStrLn|putStrLn:: String-> IO ()| 標準出力へ文字列を改行して出力|
|getChar|getChar:: IO Char| 標準入力から1文字取得|
|getString|getString:: IO String| 標準入力から文字列取得|

```hs
Prelude> :{
Prelude| main :: IO ()
Prelude| main = do
Prelude|   x <- getChar
Prelude|   print x
Prelude|   x <- getChar
Prelude|   print x
Prelude|   x <- getChar
Prelude|   print x
Prelude|   xs <- getLine
Prelude|   putStrLn xs
Prelude| :}
Prelude> main
get char
'g'
'e'
't'
 char
```
#### ファイル操作

| メソッド|定義|説明 |
|:---------|:----|:----|
|writeFile|writeFile :: FilePath -> String -> IO ()| FilePathにStringを書き込む|
|readFile|readFile :: FilePath -> IO String|FilePathから読み込む|
|appendFile|appendFile :: FilePath -> String -> IO ()| FilePathにStringを追記する|
|FilePath|type FilePath = String|GHC.IOで定義されてるStringのシノニム|

```hs
Prelude> writeFile "sample.txt" "test write" -- smple.txtファイルに書き込む
Prelude> readFile "sample.txt"
"write.\n"
Prelude> appendFile "sample.txt" "append" -- 追記
Prelude> readFile "sample.txt"
"write.\nappend"
```

#### 標準入出力の遅延IO

| メソッド|定義|説明 |
|:---------|:----|:----|
|getContents|getContents :: IO String| 標準入力から終端文字があらわれるまで取得を受け付ける|

`getContents`は標準入力から終端文字があらわれるまで入力を受け続けるが、標準入力アクションを受け取るたびに逐次入力を受け取って処理を実行する。

```sample.hs
import Data.Char

main::IO () -- control Cで中断
main = do
  xs <- getContents
  putStrLn $ map toUpper xs
```

**実行例**

```hs
C:\Users\kirakira\Documents\haskell\Example>stack runghc sample.hs
haskell send message test
-- 入力アクションを受け取ったので処理を実行
HASKELL SEND MESSAGE TEST
send message
-- 再び入力アクションを受け取ったので処理を実行
SEND MESSAGE
```