LoginSignup
6

More than 3 years have passed since last update.

posted at

updated at

Organization

ElixirでFizzBuzz錬金術

プログラミングは文学である。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. (それっぽいタイトル)

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



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
What you can do with signing up
6