LoginSignup
14
7

More than 5 years have passed since last update.

ElixirでFizzBuzz錬金術

Last updated at Posted at 2018-08-15

プログラミングは文学である。1つのアルゴリズムにいろんな表現の仕方があり、そこに芸術性を見出すことが出来る。

しかし芸術とはゴミを作ることでもあるのだ。

なにこれ

アルケミストの遊びです。ここでは敢えていろんなゴミを錬金してみましょう。

基本的に真似しちゃダメですが、「こんな書き方もできるのか」とか思って調べてみるきっかけになればと思います。

1. if の濫用

defmodule FizzBuzz do
  def say(n) do
    if Integer.mod(n, 15) == 0 do
      "FizzBuzz"
    else
      if Integer.mod(n, 3) == 0 do
        "Fizz"
      else
        if Integer.mod(n, 5) == 0 do
          "Buzz"
        else
          n
        end
      end
    end
  end
end

for n <- 1..30 do
  IO.puts FizzBuzz.say(n)
end

Elixir で条件分岐には if はあんまり使いません。Non-alchemist が Elixir 書くと稀によくこうなる。

2. cond を使う

defmodule FizzBuzz do
  def say(n) do
    cond do
      Integer.mod(n, 15) == 0 -> "FizzBuzz"
      Integer.mod(n, 3)  == 0 -> "Fizz"
      Integer.mod(n, 5)  == 0 -> "Buzz"
      :else                   -> n
    end
  end
end

for n <- 1..30 do
  IO.puts FizzBuzz.say(n)
end

まあギリギリ及第点いくかいかないかくらいですかね。 :else がチャームポイント。

3. case で guard に rem を使う

defmodule FizzBuzz do
  def say(n) do
    case n do
      n when rem(n, 15) == 0 -> "FizzBuzz"
      n when rem(n, 3)  == 0 -> "Fizz"
      n when rem(n, 5)  == 0 -> "Buzz"
      n                      -> n
    end
  end
end

for n <- 1..30 do
  IO.puts FizzBuzz.say(n)
end

結構Elixirっぽくなってきました。ですがこれは冗長です。

(今回 npos_integer なので、Integer.mod/2rem/2 の違いは問題になりません)

4. 複数句による関数定義でパターンマッチ

defmodule FizzBuzz do
  def say(n) when rem(n, 15) == 0, do: "FizzBuzz"
  def say(n) when rem(n, 3)  == 0, do: "Fizz"
  def say(n) when rem(n, 5)  == 0, do: "Buzz"
  def say(n), do: n
end

for n <- 1..30 do
  IO.puts FizzBuzz.say(n)
end

最後の def をalignすべきかどうかが迷いどころ。

5. for ではなく Enum.each/2

defmodule FizzBuzz do
  def say(n) when rem(n, 15) == 0, do: "FizzBuzz"
  def say(n) when rem(n, 3)  == 0, do: "Fizz"
  def say(n) when rem(n, 5)  == 0, do: "Buzz"
  def say(n), do: n
end

Enum.each(1..30, fn n ->
  IO.puts FizzBuzz.say(n)
end)

別に30個の :ok を並べたいわけではないので Enum.each/2 を使います。

小休止

ここまで来てやっとElixirとしてまともなコードになったような気がします。

此処からが本番ですよ。

6. defmodule しない

fizz_buzz = fn
  n when rem(n, 15) == 0 -> "FizzBuzz"
  n when rem(n, 3)  == 0 -> "Fizz"
  n when rem(n, 5)  == 0 -> "Buzz"
  n                      -> n
end

Enum.each(1..30, fn n ->
  IO.puts fizz_buzz.(n)
end)

fn の引数部でもパターンマッチが出来て、複数の枝を扱うことが出来ます。

fn で作った関数を呼ぶときは fizz_buzz.(n) というふうに . が必要になります。

7. 複数句による関数定義でたらい回す

defmodule FizzBuzz do
  def say(n), do: say(n, rem(n, 3))

  def say(n, 0), do: say(n, "Fizz", rem(n, 5))
  def say(n, _), do: say(n, "",     rem(n, 5))

  def say(_, fizz, 0), do: fizz <> "Buzz"
  def say(n, "",   _), do: n
  def say(_, fizz, _), do: fizz
end

Enum.each(1..30, fn n ->
  IO.puts FizzBuzz.say(n)
end)

なにも全部同じ名前にしなくても…とは思いますが、きっと私なりの美学がそこにあるのでしょう。

8. Stream.unfold/2を使う

Stream.unfold({1, 1, 1}, fn
  {n, 3, 5} -> {"FizzBuzz", {n + 1, 1,     1    }}
  {n, 3, b} -> {"Fizz",     {n + 1, 1,     b + 1}}
  {n, f, 5} -> {"Buzz",     {n + 1, f + 1, 1    }}
  {n, f, b} -> {n,          {n + 1, f + 1, b + 1}}
end) \
|> Stream.take(30) \
|> Enum.each(&IO.puts/1)

(そのままiexにコピペして実行できるように \ を入れてます)

ガラッとアプローチを変えて、FizzBuzzストリームを作るという方針にしてます。{n, f, b} がaccumulatorになっていて、3つとも1ずつ増やしてf3になったら"Fizz"b5になったら"Buzz"、という具合です。

Enum.take/2 ではなく Stream.take/2 を使ってるのもポイント。

9. Eitherっぽいものを作って香ばしく

# Not Either but chemistrical
defmodule Ether do
  defstruct [
    which: :left,
    value: nil,
  ]

  @type t :: %__MODULE__{
    which: :right | :left,
    value: any,
  }

  def right(val), do: %__MODULE__{which: :right, value: val}
  def left(val),  do: %__MODULE__{which: :left,  value: val}

  def lift2(
    fun,
    %__MODULE__{which: :right, value: v1},
    %__MODULE__{which: :right, value: v2}
  ), do: %__MODULE__{which: :right, value: fun.(v1, v2)}
  def lift2(
    _,
    %__MODULE__{which: :right, value: v1},
    %__MODULE__{which: :left}
  ), do: %__MODULE__{which: :left, value: v1}
  def lift2(
    _,
    %__MODULE__{which: :left},
    %__MODULE__{which: :right, value: v2}
  ), do: %__MODULE__{which: :left, value: v2}
  def lift2(
    _,
    %__MODULE__{which: :left, value: v1},
    %__MODULE__{which: :left}
  ), do: %__MODULE__{which: :left, value: v1}
end

defimpl String.Chars, for: Ether do
  def to_string(%Ether{value: val}), do: Kernel.to_string(val)
end

defmodule FizzBuzz do
  def say(n) do
    Ether.lift2(&<>/2, fizz(n), buzz(n))
  end

  defp fizz(n) when rem(n, 3) == 0, do: Ether.right("Fizz")
  defp fizz(n), do: Ether.left(n)

  defp buzz(n) when rem(n, 5) == 0, do: Ether.right("Buzz")
  defp buzz(n), do: Ether.left(n)
end

Enum.each(1..30, fn n ->
  IO.puts FizzBuzz.say(n)
end)

lift2 が本当に lift2 になってるかはちょっと怪しいです。どこで改行するかも悩みどころ。

IO.puts/1 に直接渡すために String.Chars プロトコルを実装しています。そのために defstruct

前半はただの準備なので、defmodule FizzBuzz do からが実際のコードと思うと結構きれいに見えます(断定)。

10. (それっぽいタイトル)

# あなたの錬金したゴミ、あるいはアート



14
7
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
14
7