よそ様の記事で「Haskellで書かれたおもしろいFizzBuzz」という記事がありますが、自分なりに説明し直してみます。
参考「Haskellで書かれたおもしろいFizzBuzz ― Haskellで読めないコードに遭遇した時に解読する方法を徹底解説! - プログラムモグモグ」
参考「I Did A Haskell: fizzbuzz : r/haskell」
0. まとめ
元のコードは以下の通りです。
let (m ~> str) x = str <$ guard (x `mod` m == 0)
in map (fromMaybe . show <*> 3 ~> "fizz" <> 5 ~> "buzz")
コードを読みやすくするために書き直すと以下のようになります。
import Data.Foldable (for_)
import Data.Maybe (fromMaybe)
fizzBuzz :: Integer -> String
fizzBuzz x = fromMaybe (show x) fizzBuzzMaybe
where
fizzMaybe
| x `mod` 3 == 0 = Just "Fizz"
| otherwise = Nothing
buzzMaybe
| x `mod` 5 == 0 = Just "Buzz"
| otherwise = Nothing
fizzBuzzMaybe = fizzMaybe <> buzzMaybe
main :: IO ()
main = do
for_ [1..20] $ \x -> do
putStrLn $ fizzBuzz x
一時的に文字列 "Fizz"
および "Buzz"
を Maybe
型で扱い、"Fizz"
でも "Buzz"
でもなく Nothing
の場合は show x
を返します。
1. 準備
1.1. let
式を取り除く
Haskell のコーディングスタイルの 1 つとして、そもそも (do
記法等は例外として) let
式を用いない書き方があります。
let
式を用いないようにすると例えば以下のように書けます。
(where
句を使うこともできますが、ここでは単にコードを読みやすくするために演算子および関数を分けて定義します。)
import Control.Monad (guard)
import Data.Foldable (for_)
import Data.Maybe (fromMaybe)
(~>) :: Integer -> String -> Integer -> Maybe String
(m ~> str) x = str <$ guard (x `mod` m == 0)
fizzBuzz :: [Integer] -> [String]
fizzBuzz = map (fromMaybe . show <*> 3 ~> "Fizz" <> 5 ~> "Buzz")
main :: IO ()
main = do
let xs = fizzBuzz [1..20]
for_ xs $ \x -> do
putStrLn x
1.2. map
関数を取り除く
ここでは for_
関数でループして結果を表示するため、map
関数で一度結果をリストにする必要はありません。
map
関数を取り除くと以下のようになります。
import Control.Monad (guard)
import Data.Foldable (for_)
import Data.Maybe (fromMaybe)
(~>) :: Integer -> String -> Integer -> Maybe String
(m ~> str) x = str <$ guard (x `mod` m == 0)
fizzBuzz :: Integer -> String
fizzBuzz = fromMaybe . show <*> 3 ~> "Fizz" <> 5 ~> "Buzz"
main :: IO ()
main = do
for_ [1..20] $ \x -> do
putStrLn $ fizzBuzz x
2. 演算子 ~>
の定義
演算子 ~>
は以下のように定義されています。
(~>) :: Integer -> String -> Integer -> Maybe String
(m ~> str) x = str <$ guard (x `mod` m == 0)
意味的には以下と同じになります。
(~>) :: Integer -> String -> Integer -> Maybe String
(m ~> str) x = do
guard $ x `mod` m == 0
pure str
(~>) :: Integer -> String -> Integer -> Maybe String
(m ~> str) x =
if x `mod` m == 0
then Just str
else Nothing
(~>) :: Integer -> String -> Integer -> Maybe String
(m ~> str) x
| x `mod` m == 0 = Just str
| otherwise = Nothing
参考「guard - Control.Monad」
参考「(<$) - Data.Functor」
参考「[Haskell] 条件分岐してモナドを返す - Qiita」
参考「[Haskell] モナド 演算子 まとめ - Qiita」
つまり、m ~> str
は x
が m
の倍数のときに Just str
を返す関数 (\x -> if x `mod` m == 0 then Just str else Nothing) :: Integer -> Maybe String
を返します。
fizzMaybe :: Integer -> Maybe String
fizzMaybe = 3 ~> "Fizz"
buzzMaybe :: Integer -> Maybe String
buzzMaybe = 5 ~> "Buzz"
意味的には以下と同じになります。
fizzMaybe :: Integer -> Maybe String
fizzMaybe x
| x `mod` 3 == 0 = Just "Fizz"
| otherwise = Nothing
buzzMaybe :: Integer -> Maybe String
buzzMaybe x
| x `mod` 5 == 0 = Just "Buzz"
| otherwise = Nothing
3. 演算子 <>
による関数同士の結合
演算子 <>
は Semigroup
同士を結合する演算子です。
以下の理由により、Integer -> Maybe String
型の関数は Semigroup
クラスのインスタンスです:
-
type String = [Char]
-
String
型はリスト型
-
-
Semigroup [a]
- リスト型は
Semigroup
- リスト型は
-
Semigroup a => Semigroup (Maybe a)
-
Semigroup
を含むMaybe
型はSemigroup
-
-
Semigroup b => Semigroup (a -> b)
-
Semigroup
を返す関数はSemigroup
-
参考「String - Data.String」
参考「Semigroup - Data.Semigroup」
よって、以下のように Integer -> Maybe String
型の関数同士を演算子 <>
で結合することができます。
fizzBuzzMaybe :: Integer -> Maybe String
fizzBuzzMaybe = fizzMaybe <> buzzMaybe
意味的には以下と同じになります。
fizzBuzzMaybe :: Integer -> Maybe String
fizzBuzzMaybe x = fizzMaybe x <> buzzMaybe x
4. 演算子 .
による a -> b
型の関数と b -> c -> d
型の関数の合成
演算子 .
は基本的には (b -> c) -> (a -> b) -> a -> c
という関数の合成をしますが、c
を c -> d
に置き換えることで (b -> c -> d) -> (a -> b) -> a -> c -> d
という関数の合成ができることが分かります。
ここでは show :: Integer -> String
関数と fromMaybe :: String -> Maybe String -> String
関数を合成して Integer -> Maybe String -> String
型の関数にします。
f :: Integer -> Maybe String -> String
f = fromMaybe . show
意味的には以下と同じになります。
f :: Integer -> Maybe String -> String
f x = fromMaybe $ show x
5. 演算子 <*>
による r -> a
型の関数と r -> a -> b
型の関数の合成
演算子 <*>
は基本的には Applicative f => f (a -> b) -> f a -> f b
という計算をしますが、f
を (r ->)
に置き換えることで (r -> a -> b) -> (r -> a) -> r -> b
という関数の合成ができることが分かります。
(<*>) :: (r -> a -> b) -> (r -> a) -> r -> b
(<*>) f g x = f x $ g x
(コードが難解になるため、実用的には関数の合成に演算子 <*>
を使用しない方が良いと思います。)
参考「(<*>) - Control.Applicative」
参考「Applicative - Control.Applicative」
ここでは Integer -> Maybe String -> String
型の関数と fizzBuzzMaybe :: Integer -> Maybe String
関数を合成して fizzBuzz :: Integer -> String
関数にします。
fizzBuzzMaybe :: Integer -> Maybe String
fizzBuzzMaybe x = fizzMaybe x <> buzzMaybe x
f :: Integer -> Maybe String -> String
f x = fromMaybe $ show x
fizzBuzz :: Integer -> String
fizzBuzz = f <*> fizzBuzzMaybe
意味的には以下と同じになります。
fizzBuzzMaybe :: Integer -> Maybe String
fizzBuzzMaybe x = fizzMaybe x <> buzzMaybe x
f :: Integer -> Maybe String -> String
f x = fromMaybe $ show x
fizzBuzz :: Integer -> String
fizzBuzz x = f x $ fizzBuzzMaybe x
fizzBuzzMaybe :: Integer -> Maybe String
fizzBuzzMaybe x = fizzMaybe x <> buzzMaybe x
fizzBuzz :: Integer -> String
fizzBuzz x = fromMaybe (show x) $ fizzBuzzMaybe x
6. 一部の関数を戻り値のみに置き換える
fizzMaybe
関数、buzzMaybe
関数および fizzBuzzMaybe
関数の引数は全て同じ値を渡します。
fizzMaybe :: Integer -> Maybe String
fizzMaybe x
| x `mod` 3 == 0 = Just "Fizz"
| otherwise = Nothing
buzzMaybe :: Integer -> Maybe String
buzzMaybe x
| x `mod` 5 == 0 = Just "Buzz"
| otherwise = Nothing
fizzBuzzMaybe :: Integer -> Maybe String
fizzBuzzMaybe x = fizzMaybe x <> buzzMaybe x
fizzBuzz :: Integer -> String
fizzBuzz x = fromMaybe (show x) $ fizzBuzzMaybe x
よって、関数として扱わず戻り値のみを扱っても同様の計算ができます。
fizzBuzz :: Integer -> String
fizzBuzz x = fromMaybe (show x) fizzBuzzMaybe
where
fizzMaybe
| x `mod` 3 == 0 = Just "Fizz"
| otherwise = Nothing
buzzMaybe
| x `mod` 5 == 0 = Just "Buzz"
| otherwise = Nothing
fizzBuzzMaybe = fizzMaybe <> buzzMaybe
fizzMaybe <> buzzMaybe
の計算は以下のようになります。
Nothing <> Nothing = Nothing
Just "Fizz" <> Nothing = Just "Fizz"
Nothing <> Just "Buzz" = Just "Buzz"
Just "Fizz" <> Just "Buzz" = Just "FizzBuzz"
fromMaybe (show x) fizzBuzzMaybe
の計算は以下のようになります。
fromMaybe (show x) Nothing = show x
fromMaybe (show x) (Just "Fizz") = "Fizz"
fromMaybe (show x) (Just "Buzz") = "Buzz"
fromMaybe (show x) (Just "FizzBuzz") = "FizzBuzz"
7. 結果
コード全体は以下のようになります。
import Data.Foldable (for_)
import Data.Maybe (fromMaybe)
fizzBuzz :: Integer -> String
fizzBuzz x = fromMaybe (show x) fizzBuzzMaybe
where
fizzMaybe
| x `mod` 3 == 0 = Just "Fizz"
| otherwise = Nothing
buzzMaybe
| x `mod` 5 == 0 = Just "Buzz"
| otherwise = Nothing
fizzBuzzMaybe = fizzMaybe <> buzzMaybe
main :: IO ()
main = do
for_ [1..20] $ \x -> do
putStrLn $ fizzBuzz x
1
2
Fizz
4
Buzz
Fizz
7
8
Fizz
Buzz
11
Fizz
13
14
FizzBuzz
16
17
Fizz
19
Buzz