よそ様の記事で「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