動機
Haskellの本を読んでは見たものの、まだ自分で自由にプログラムを書けない初心者です。でも、初心者だからこそ、初めて学習した時の感じ方、考え方を述べておくことは大事かな、と思って、今の自分の目線で感じたことを書いてみました。
Haskellとは
遅延評価を持つ純粋関数型プログラミング言語です。
関数型プログラミング言語は、関数型プログラミングに適したコンピュータ言語です。
関数型プログラミングの定義については、諸説あって難しいみたいなのですが、とりあえず自分の今の理解として「関数のメリットを最大限に活かしたプログラミング方法」だと思います。
Haskellでは、関数のメリットを最大限に生かすため、強い型定義と型推論、純粋関数と純粋でない関数の区別、モナドの導入、無名関数、関数の部分適用・関数合成・カリー化、遅延評価、末尾再帰最適化など、いろいろな仕組みが用意されています。
Haskellは関数型プログラミングを説明する上で良く題材として扱われます。
FizzBuzzとは
1からNまでの整数が与えられた時に、以下のルールでメッセージを表示するプログラム(または遊び)
- 3で割り切れる数に対しては、"Fizz"
- 5で割り切れる数に対しては、"Buzz"
- 3でも5でも割り切れる数に対しては、"FizzBuzz"。
- 上記以外は、その整数そのものを答える
命令型のプログラム言語(Cとか)では、forなどのループ命令と、ifなどの条件分岐命令を正しく取得しているかを確認する課題としてよく使われます。
挑戦1
まずは問題を具体化するために、N=100としてみます。また、画面に表示するのでIO(IOモナド)が必要になるため、Control.Monadを読み込みます。(C言語でいうところの、stdioを読み込んでいるような感じ)
なので、任意のNに対して、FizzBuzzを解いて画面出力する部分をfizzBuzz
関数として独立させると、プログラムはおおまかに
import Control.Monad
main = fizzBuzz 100
fizzBuzz :: Integer -> IO()
fizzBuzz num = いろいろ
という形になるはず、、。
でもって、まずはCとかJavaと同じように、for文とif文で実装することを考えてみました。
うーん、Haskellのfor文は、forM_というのがあるけれど、これはdo文の中でしか使えない(Monadでしか使えない)ようなので、
fizzBuzz num = do
forM_ [1..num] $ \i -> do
-- iを使った処理をいろいろ
こんな感じにしてみました。[1..num]
は、1
からnum
までの配列です。「\i -> do iを使った処理をいろいろ」は、引数がiの無名関数です。
$
は、何もしない演算子ですが、優先度を下げることで、\i -> do
以下の部分を関数として扱っています。
for文の部分はこれで良いとして、まずはベタにif文で条件分岐して、表示する実装をしてみます。すると、こんな感じに。
fizzBuzz num = do
forM_ [1..num] $ \i -> do
if (i `mod` 3 == 0)
then
if (i `mod` 5 == 0)
then
print "FizzBuzz"
else
print "Fizz"
else
if (i `mod` 5 == 0)
then
print "Buzz"
else
print $ show i
できました!
ソースコード全体では、次になります。
import Control.Monad
main = fizzBuzz 100
fizzBuzz :: Integer -> IO()
fizzBuzz num = do
forM_ [1..num] $ \i -> do
if (i `mod` 3 == 0)
then
if (i `mod` 5 == 0)
then
print "FizzBuzz"
else
print "Fizz"
else
if (i `mod` 5 == 0)
then
print "Buzz"
else
print $ show i
実際に動作させてみると(上記をfizzbuzz.hc
で保存して)
$ runghc fizzbuzz.hc
"1"
"2"
"Fizz"
"4"
"Buzz"
"Fizz"
"7"
"8"
"Fizz"
"Buzz"
"11"
"Fizz"
"13"
"14"
"FizzBuzz"
"16"
....
となり、成功です!
挑戦2
しかし、挑戦1のコードは初心者とはいえ、あまりにダメな感じのするコードです、、。
その理由は、いろいろとあるのですが、まずはコード全体がdo文に囲まれていること。
Haskellは、純粋関数型プログラミング言語であって、なるべく「理想的な純粋関数の世界」で仕事をすることが良いとされているハズです。
初心者なので、あまり強気には言えませんが、そんな気がします。
けれども(自分の今の理解では、ですが)do文は、「理想的世界では出来ないことを仕方なく『現実世界(と、ここでは呼ぶことにします。すみません)で処理』している」ものだと思います。
挑戦1のコードは、現実世界での処理で問題を解いている訳なので、ダメな感じがするのです。
さらに、追加していうと、iが5で割り切れるかどうかの処理が2回も記述してあります。これはどう考えても無駄です。CやJavaで実装する場合でも、判定は一回だけして、それをフラグ変数などに代入して判断するはずです。そもそも、このプログラムはi
しか変数使ってない、、。
ということで、次のように変更してみます。
import Control.Monad
main = fizzBuzz 100
fizzBuzz :: Integer -> IO()
fizzBuzz num =
forM_ [1..num] $ \i -> do
let isFizz = i `mod` 3 == 0
let isBuzz = i `mod` 5 == 0
let str1 = if isFizz
then "Fizz"
else ""
let str2 = if isBuzz
then "Buzz"
else ""
let str3 = if (not isFizz && not isBuzz)
then show i
else ""
print $ str1 ++ str2 ++ str3
少し解説すると、まず、for文の前のdoがなくなりました。doの次が1つしか文がないときは、doはなくてもOKだったからです。これは、挑戦1の回答でも同様に出来ます。
また、isFizz
やisBuzz
という変数が導入されました。do文の中の「現実世界の処理」で変数に値を代入する場合は、let文を使います。
また、各判定した結果を文字列変数str1
,str2
,str3
に代入し、最後にそれを結合して表示するようにしてみました!
これも、挑戦1のときと同様に正しく動作します!
挑戦3
挑戦2で、繰り返し使われる内容を変数にしましたが、それでもまだダメな感じは残ったままです。それはやっぱり、挑戦2の最初で述べたように、do文の中で問題を解いているから、、です。
ということで、for文の中の作業のうち、表示する文字列を求める処理と、その文字列を表示する処理を分けてみます。このうち、前者の部分を別関数にすれば、かなり改善されてきます。
プログラムの概要は
import Control.Monad
main = fizzBuzz 100
fizzBuzz :: Integer -> IO()
fizzBuzz num =
forM_ [1..num] $ \i -> do
print $ makeMessage i
こんな感じ。新しく登場したmakeMessage
は、次のような純粋関数です。
makeMessage :: Integer -> String
ようやく新しい関数が登場しました。関数が出てくると、関数型プログラミングをしている気になってきます!(笑)
さて、makeMessage
を具体的にどう実装するかなのですが、ここでちょっと困ります。挑戦2のときは、do文内で実行していたので、変数に値を入れたり、if文
で判定したり出来ました。けれども、Haskellの純粋関数では、if文は使えますが、変数に値を一度入れて、それからそれをif文で判定して、その結果を文字列に入れて、、、
といったロジック処理は書けない(まったく書けない訳じゃないけど、命令型プログラミングと同様には書けない)です。
純粋関数の基本は、一回だけ分岐して戻り値を返すだけ、のハズです。
分岐方法は、基本的にはパターンマッチがよく使われるのですが、論理式だと出来ないはず、、、。
そこで思い出したのが、ガード! ガードなら、条件式をかけます。しかも、where句を使えば、条件式の結果を一度変数に入れておける!
ということで、makeMessage
関数は次のようになりました。
makeMessage :: Integer -> String
makeMessage num
| isFizz && isBuzz = "FizzBuzz"
| isFizz = "Fizz"
| isBuzz = "Buzz"
| otherwise = show num
where
isFizz = num `mod` 3 == 0
isBuzz = num `mod` 5 == 0
わかりやすい、、、。なんかこう、初心者でも前よりだいぶ良くなった感じがわかります。
コードで何をしているのかも、一目瞭然で、いかにも「宣言的」な書き方になりました。
ソースコードの全体は、次のようになります。
import Control.Monad
main = fizzBuzz 100
fizzBuzz :: Integer -> IO()
fizzBuzz num =
forM_ [1..num] $ \i -> do
print $ makeMessage i
makeMessage :: Integer -> String
makeMessage num
| isFizz && isBuzz = "FizzBuzz"
| isFizz = "Fizz"
| isBuzz = "Buzz"
| otherwise = show num
where
isFizz = num `mod` 3 == 0
isBuzz = num `mod` 5 == 0
挑戦4
これでだいぶ良くなったとは思うのですが、少し気に入らないところがあります。
それは、for文です。for文で関数を回しているのが、まだ命令型プログラミングっぽい感じがします。
関数型ならforよりmapを使うはず!
ということで、mapM_関数を使うことを考えてみます。
mapM_関数は、
mapM_ :: Monad m => (a -> m b) -> [a] -> m ()
こんな型定義になっているので、a -> m b
型の関数を第一引数に、[a]
という配列を第二引数にとります。
だとすると、
fizzBuzz num =
forM_ [1..num] $ \i -> do
print $ makeMessage i
の部分は
fizzBuzz num =
mapM_ (\i -> print $ makeMessage i) [1..num]
と書き換えられます。これはこれでmapM_
に書き換えられたので良い、、、ですが、無名関数のところがまだカッコ悪いです。
要は、i
を受け取ってprint
する関数を用意すればいいのだから、関数合成でいけるはず。
ということで、
fizzBuzz num = mapM_ (print . makeMessage) [1..num]
でいけました。短くなったので、ついでに一行にしています。
全体では
import Control.Monad
main = fizzBuzz 100
fizzBuzz :: Integer -> IO()
fizzBuzz num = mapM_ (print . makeMessage) [1..num]
makeMessage :: Integer -> String
makeMessage num
| isFizz && isBuzz = "FizzBuzz"
| isFizz = "Fizz"
| isBuzz = "Buzz"
| otherwise = show num
where
isFizz = num `mod` 3 == 0
isBuzz = num `mod` 5 == 0
です。
なんだかすごくシンプルで良い感じになりました。幸せな気持ちになりますね。
おまけ
mapM_
は、IOアクションをすべて実行して空のIOを返しているだけなので、むしろ、アクションをすべて>>
演算子で捨ててしまう
という書き方のが良いかもしれません。その場合、fizzBuzz
関数は
fizzBuzz num = foldr1 (>>) $ map ( print . makeMessage ) [1..num]
で良いようです。(foldl1
でも同じ)
mapM_
というモナド用のメソッドがなくなり、純粋関数のmap
になったのは良い感じがします。でもまだ、>>
というMonad用のメソッドが使われています。
そもそも、アクションの配列を使った処理ではなくて、まずは純粋関数だけの処理で表示する全部の文字列をすべて結合してしまい、最後に一度だけ表示する、という方法はどうでしょうか? それだと、改行を加える必要がありますが、こんな感じが良いように思います。
fizzBuzz num = putStr $ foldr1 (++) $ map (++ "\n") $ map makeMessage [1..num]
これだと、表示するためのアクションはputStr
の1回だけになります。(改行を反映するため、print
ではなくputStr
にしました)
こちらの方が関数型プログラミングとしては良いのかも。
間違いや改善点などお気付きの方、あるいは、この部分がわかりにくいというご指摘の方がいましたら、どんどん連絡してください。
追記 2016/06/15
とても面白い回答方法がありました。
具体的には、FizzBuzzを
import Control.Monad (guard)
import Data.Maybe (fromMaybe)
import Data.Monoid ((<>))
main :: IO()
main = mapM_ (putStrLn . fizzbuzz) [1..100]
fizzbuzz :: Integer -> String
fizzbuzz = let (d ~> s) n = s <$ guard (n `mod` d == 0)
in fromMaybe . show <*> 3 ~> "Fizz" <> 5 ~> "Buzz"
で解くというものです。ガード条件を使う代わりに、guard関数を使い、Maybe Alternativeを使って「条件を満たせば文字列、そうでなければNothing」となる関数(~>)
を定義しています。次に、それをMonoidとして結合した上で、fromMaybeで文字列または数値を表示しています。
言葉で書くと複雑ですが、上記リンク先では非常に丁寧に書いてあり、とても勉強になりました!