お布団宇宙ねこ

にゃーん

Haskellで空白区切りの入力を受け取る

ku00.hatenablog.com

前回、AOJ の問題をいくつか解いたときに空白区切りの入力の扱いに悩んでいましたが、前回より良い方法が見つかったのでメモしておきます。

<$> を使おう

入力として2つの整数が空白区切りで渡されるとき、それぞれの整数を a, b という変数で取りたいとします。すると、次のようなコードになります。

(例: "123 456")

Before

l <- getLine
let (a:b:[]) = map read $ words l :: [Int]

After

import Control.Applicative

[a, b] <- map read . words <$> getLine

まず <- の右側から見ていきましょう。

Before では、 getLine したものを一度 l に束縛し、それから words , map read の順で関数を適用することで [Int] 型の値を取ることができました。

それを After では、 <$> を使うことで、一時的な変数に束縛することなく words , map read を適用できます。

一方 <- の左側ですが、これは事前に与えられる数が分かっているので素直に2つの要素を持つリストに束縛しました。 (x:xs) のような文法はパターンマッチで使う方が一般的でしょう(むしろ変数に束縛するのに有用なケースってあるのだろうか)。

<$> とは

<$>Control.ApplicativeData.Functor にある関数で、アプリカティブ値やファンクター値に対して普通の関数を適用する関数です。

*Main Lib> show <$> Just 1
Just "1"
*Main Lib> (+10) <$> [1..5]
[11,12,13,14,15]

では、 IO () に対して適用できるのはなぜかというと、 IO ()モナドであり、またアプリカティブ・ファンクターでもあり、ファンクターでもあるからです。これは GHCi の info オプションでも確認できます。アプリカティブファンクターがモナドスーパークラスになっているので当たり前なのですが...すっかり抜けていました。

*Main Lib> :i IO ()
instance Monad IO -- Defined in ‘GHC.Base’
instance Functor IO -- Defined in ‘GHC.Base’
instance Applicative IO -- Defined in ‘GHC.Base’
instance Monoid () -- Defined in ‘GHC.Base’

そして、今回のコード map read . words <$> getLine では何が起きているのかというと、

  1. getLine が入力を受け取り IO String を返す
  2. <$> によって IO String から String を取り出し words に適用する( ["123", "456"] )
  3. その後 map read を適用する( [123, 456] )

という流れになっています。

型推論ができない場合

<$> を使うと IO () の中身に対して普通の関数を適用できるようになるのですが、その後の map read で型推論ができない場合があります。その場合は、以下のように型注釈を使って明示的に型を教えてあげる必要があります。

[a, b, c] <- map (read :: String -> Int) . words <$> getLine

まとめ

モナドに埋もれがちですが、アプリカティブファンクターやファンクターも重要であることを再認識したのでしっかり復習しようと思いました。また、 read に型注釈を付けるときにはいつも read x :: Int みたいな使い方しかしてなかったので、関数全体の型注釈ができるのは知りませんでした。

参考文献