6
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

「Haskellで書かれたおもしろいFizzBuzz」を再考する

Last updated at Posted at 2023-11-23

よそ様の記事で「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 ~> strxm の倍数のときに 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 同士を結合する演算子です。

参考「(<>) - Data.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 という関数の合成をしますが、cc -> d に置き換えることで (b -> c -> d) -> (a -> b) -> a -> c -> d という関数の合成ができることが分かります。

参考「(.) - Data.Function

ここでは 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
6
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
6
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?