Help us understand the problem. What is going on with this article?

Haskell初心者がFizzBuzzに挑戦してみました。

More than 3 years have passed since last update.

動機

Haskellの本を読んでは見たものの、まだ自分で自由にプログラムを書けない初心者です。でも、初心者だからこそ、初めて学習した時の感じ方、考え方を述べておくことは大事かな、と思って、今の自分の目線で感じたことを書いてみました。

Haskellとは

遅延評価を持つ純粋関数型プログラミング言語です。
関数型プログラミング言語は、関数型プログラミングに適したコンピュータ言語です。
関数型プログラミングの定義については、諸説あって難しいみたいなのですが、とりあえず自分の今の理解として「関数のメリットを最大限に活かしたプログラミング方法」だと思います。
Haskellでは、関数のメリットを最大限に生かすため、強い型定義と型推論、純粋関数と純粋でない関数の区別、モナドの導入、無名関数、関数の部分適用・関数合成・カリー化、遅延評価、末尾再帰最適化など、いろいろな仕組みが用意されています。

Haskellは関数型プログラミングを説明する上で良く題材として扱われます。

FizzBuzzとは

 1からNまでの整数が与えられた時に、以下のルールでメッセージを表示するプログラム(または遊び)

  1. 3で割り切れる数に対しては、"Fizz"
  2. 5で割り切れる数に対しては、"Buzz"
  3. 3でも5でも割り切れる数に対しては、"FizzBuzz"。
  4. 上記以外は、その整数そのものを答える

命令型のプログラム言語(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の回答でも同様に出来ます。
また、isFizzisBuzzという変数が導入されました。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

とても面白い回答方法がありました。

http://itchyny.hatenablog.com/entry/2015/12/27/150000

具体的には、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で文字列または数値を表示しています。
言葉で書くと複雑ですが、上記リンク先では非常に丁寧に書いてあり、とても勉強になりました!

KNaito
I love JavaScript, Html, PHP, Python, Java, Ruby, Perl, C, C++, Objective-C and Haskell. My stack overflow address is http://stackoverflow.com/users/3535002/knaito
http://d.hatena.ne.jp/knight_9999/
asial
開発中に体験した技術についての情報をつづります
http://www.asial.co.jp
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away