29
22

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.

長年break文を使い続けたプログラマーが、break文の無いElixirを学んで、遅延評価にたどり着いた話

Posted at

はじめに

私はPythonのプログラムを使う時、breakを使う事がよくあります。
繰り返し処理の中で、繰り返しを中断したい状況になったら、直ぐに中断できて便利ですよね

Elixirには、繰り返し文(Pythonのfor, while等)がありません。なので繰り返しを中断するためのbreak文がありません。

Elixirでは、どうしているか、例題を設定して、Elixirの場合の記述を書いてみます。

課題1

heavy_process()があります。
"heavy!"と表示します。
trueかfalseを返す関数です。
これを10回実行して10回ともtrueが返ってきたら、true, 一度でもfalseが返ったらfalseを返す関数を作成する。

シンプルな実装

  def get_result_enum() do
    1..10
    |> Enum.map(fn _i -> heavy_process()end)
    |> Enum.all?()
  end

  def heavy_process() do
    IO.puts("heavy!")
    hd Enum.take_random([true, false], 1)
  end
end

実行結果

iex> Tutorial012.get_result_enum          
heavy!
heavy!
heavy!
heavy!
heavy!
heavy!
heavy!
heavy!
heavy!
heavy!
false

「一度でもfalseが返ったらfalseとする。」を自分では作成せず、Enum.all?に任せています。
プログラムを見ただけで、処理の内容が理解できるとおもいます。
Enumって楽しいですね。

課題2

課題1に、
heavyと表示するのは、非常に大変な処理で、できるだけ実行回数を少なくしたい。
という条件が追加されたらどうでしょうか?

reduce_whileを使う

Enumには、途中でやめれれるreduce_whileがあります。
:haltを返せば中断できます。

  def get_result_reduce_while do
    1..10
    |> Enum.reduce_while(false, fn _i, _acc ->
      ret = heavy_process()
      if ret, do: {:cont, true}, else: {:halt, false}
    end)
  end

実行結果

iex> Tutorial012.get_result_reduce_while()
heavy!
false

heavyの呼び出しが、10回ではなくなってます。
目的は達成できてます。しかし、なにかわざわざ複雑なものを使ってる感じです。
reduce_whileを無理使ってるような。
accとか使ってないのでreduceの必要性もありません。
これだったら、Pythonのbreakの方がよっぽどわかりやすい。

例外を使った脱出

Elixirでは、try-throwを使えば、処理を中断する事ができます。
heavy_process()を実行して、falseが返ったら、throwして中断するようにしてみます。
Enum.map()やEnum.all?()を使う必要はないので、forで書いてみます。
※Elixirのforは、Pythonの内包のforのようなものです。

  def get_result_for_with_break() do
    try do
      for _i <- 1..10 do
        ret = heavy_process()
        if ret == false do
          throw :break
        end
      end
      true
    catch
      :throw, :break -> false
    end
  end

実行結果

iex> Tutorial012.get_result_for_with_break()
heavy!
false

heavyの呼び出しが、10回ではなくなってます。
中断できています。

Streamを使う方法

try-catchを使った処理でやりたいことはできています。
悪くはないと思います。
Enum.all?()はリストの要素をひとつづつ確認していきますが、一つでもfalseがあると処理を中断してfalseを返します。
遅延評価をするStreamを使う事で、課題2を実現できます。

  def get_result_stream() do
    1..10
    |> Stream.map(fn _i -> heavy_process()end)
    |> Enum.all?()
  end

実行結果

iex(22)> Tutorial012.get_result_stream()        
heavy!
false

heavyの呼び出しが、10回ではなくなってます。
Enum.all?()の記述のわかりやすさを生かしつつ、中断もできています。
Elixirって楽しいですね。

遅延評価

課題1のプログラムが10回実行しているのは、Enum.all?()の前に、10回実行した結果を作っているためです。
all?()には、課題2で解決したい、余計な処理をしない仕組みが備わっています。
Enum.all?()がリストの内容を実際に調べる時に都度、heavy_process()を呼び出すようにできれば、無駄な実行をなくせます。
これを実現するのが、遅延評価です。

Pythonにも遅延評価(yield)があるので、同じことはできると思います。
しかし、不幸か幸いか、Pythonには、break文があるので、for文でbreakする実装をいつもしていたので、やったことはありません。

やりたい事 Elixir Python
繰り返しの中断 try-catch break
遅延評価 Stream yield

まとめ

  • 遅延評価を使う事で、そもそも中断処理が不要にできる事もある。
  • Elixirで、break的な事がしたくなったら、try-throwを使えばできる
  • 繰り返しを中断できるreduce_whileを使うのも一つの方法
  • Elixirは楽しい!
29
22
7

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
29
22

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?