やわらかテック

興味のあること。業務を通して得られた発見。個人的に試してみたことをアウトプットしています🍵

【レポート】第5回清流elixir勉強会in丸の内を開催しました【再帰関数】

トピック

今回で第5回目の勉強会を僕の運営しているコミュニティで開催することができました
清流elixir

先週にはfukuoka.exを運営されているpiacereさんと Twitterで繋がらせて頂きまして多くの方に清流elixirの名を知って頂けました
本当にあざます!!!
やる気がめっさ出ました!!

清流elixir-infomation
開催場所: 丸の内(愛知)
参加人数: 3 -> 4 update!!
コミュニティ参加人数 : 5 -> 8 update!!
20190525現在

第5回の活動内容

関数型言語といったらやっぱり再帰関数でしょってことで軽いノリでこのテーマに
【自分的レシピ】elixirでの再帰関数の動かし方でも触れたように
関数型言語には一般的にfor文のような、いわゆるループ処理は用意されていない
じゃあ、どうやって書くのよ?という問いに対する答えは「再帰関数」を使おう

再帰関数ってなんぞ

関数が自分自身を呼び出す処理のこと
5回だけ「"hello"」と出力する再帰関数を作るとする
コードに落とし込むとこんな感じ

defmodule Sample do
  def hello do
    IO.puts("hello")
    hello()
  end
end

Sample.hello()

関数が自分自身を呼び出しているのが分かる
ただし、このままこのコードを実行するとまずい。無限再帰になってしまう
「いつ停止するの?」という問題がある
今回は5回「"hello"」を出力させた時点で再帰を終了させる必要がある
これは「停止性」といい、本当にこのコードが終了するかを保証する必要がある(停止性の議論)

たとえば5回で終了させたいならばこんな風に書き換える

defmodule Sample do
  def hello(counter) do
    if counter == 5 do
      :fin
    else
      IO.puts("hello")
      hello(counter+1)
    end
  end
end

Sample.hello(0)
# hello
# hello
# hello
# hello
# hello

きちんと再帰が停止した
helloの引数に追加したcounterというのはアキュムレーターと呼ばれるもので
貯蔵庫のような意味があり、状態を保管するために使用している
今回の場合は何回呼び出したか?ということをカウントするためのカウンターとして扱っている
アキュムレーターについての詳しく話はこちらで解説しているのでぜひ

ただこのコードはElixirっぽいくない上にダサいので書き換える

defmodule Sample do
  def hello(counter) when counter == 5, do: :fin
  def hello(counter) do
    IO.puts("hello")
    hello(counter+1)
  end
end

Sample.hello(0)
# hello
# hello
# hello
# hello
# hello

これでも上手く動く

defmodule Sample do
  def hello(5), do: :fin
  def hello(counter) do
    IO.puts("hello")
    hello(counter+1)
  end
end

参加者の方が面白い書き方を発見した(凄い
「helloにdefaultの値を持たせたらhelloの呼び出し時に0を渡す必要がなくなるのでは?」 fmfm...

defmodule Sample do
  def hello(5), do: :fin
  def hello(counter \\ 0) do
    IO.puts("hello")
    hello(counter+1)
  end
end

Sample.hello()

実行すると上手く動くが以下のような警告が出る

warning: definitions with multiple clauses and default values require a header. Instead of:
def foo(:first_clause, b \ :default) do ... end
def foo(:second_clause, b) do ... end

errorの意味は分かるが、修正の方法が思いつかず
再帰関数作るときはアキュムレーターにdefault引数渡すの良くなさそうということで一旦落ち着いた
詳しい原因をご存知の方は教えてください

再帰関数で色々作って遊んでみる

f:id:takamizawa46:20190525130609j:plain:w450

配列の先頭の要素を取得して出力する
headとtailを使って実装する
リストが空となった場合に停止するように関数を実装する

sample = [1,2,3,4,5]
[head | tail] = sample
defmodule Sample do
  def fetch([]), do: :fin
  def fetch([head | tail]) do
    IO.puts(head)
    fetch(tail)
  end
end

Sample.fetch([1,2,3,4,5])
# 1
# 2
# 3
# 4
# 5

配列の要素を合計する
アキュムレーターに先頭の要素をどんどん足していく

defmodule Sample do
  def sum([], accum), do: accum
  def sum([head | tail], accum), do: sum(tail, accum+head)
end

IO.puts(Sample.sum([1,2,3,4,5], 0))
#15

このままだと呼び出し時に0を渡すという操作があり
アキュムレーターの知識がない人には煩わしいのでhelper関数というものでラップする

defmodule Sample do
  def sum(lst), do: _sum(lst, 0)
  defp _sum([], accum), do: accum
  defp _sum([head | tail], accum), do: _sum(tail, accum+head)
end

IO.puts(Sample.sum([1,2,3,4,5]))
#15

これならリストを渡すだけでsumをアキュムレーターを意識せずに使用することが可能

配列の大きさをカウントする

defmodule Sample do
  def length([], accum), do: accum
  def length([_head | tail], accum), do: length(tail, accum+1)
end

IO.puts(Sample.length([1,2,3,4,5], 0))
#5

良い感じですね
言語に実装されているEnumの関数は再帰関数を使えば再現することが可能
明日から新たなモジュールを開発できる。やったぜ

悲しいお知らせ

本日の勉強会で作成した再帰関数はお察しの通り、大体はEnumの関数やらの組み合わせで作成することが可能
再帰関数を作成するまえに必ず、この処理はEnumやらを使って実装することが出来ないかを考える必要がある
再帰関数で書いてもいいけどね。スピード感を大事にしたい

嬉しいお知らせ

20190601(土)に東京で開催される
Erlang & Elixir Fest 2019に行って参ります
恥ずかしながら、東京にいくのは人生で2度目で迷わずに行けるかが心配..

清流elixirを代表して参加しようと思っていたら
いつも勉強会に来て頂ける参加者の方が全員参加するようでワロタ

こんなすげー方達の話を聞ける機会は滅多にないので楽しんできます
次回の勉強会はElixirの実装力を上げるために競プロ問題をいくつかElixirで解いてみようと思ってます