LoginSignup
14
12

More than 5 years have passed since last update.

Elixirの無名関数と変数のスコープ

Last updated at Posted at 2016-03-10

無名変数の基本

ネタ元は, 最近読みだしたこの本です.

Elixirでは, 以下の様な構文で無名関数を生成します.

  fn
    parameter-list -> body
    parameter-list -> body
    ...
  end

見ての通り, 名前付きの関数と同様に, 引数にマッチするパターンによって挙動を別々に定義することができます.
また, Elixirでは, 関数は基本型となっているので, 当然変数に代入したり, 変数経由で呼び出したりすることができます.

例として
- 3つの引数を受け取り, 第一引数と第二引数がともに0ならば"FooBuzz"を, 第一引数のみが0ならば"Foo"を, 第二引数のみが0の時は"Buzz"を, それ以外の時は第三引数をそのまま返す
という問題を考えてみます.
(この問題は↑の本に問題としてのっていたものなので、ネタバレになったらごめんね)

iex(14)> foobuzzbar = fn
...(14)>   (0, 0, _) -> "FooBuzz"
...(14)>   (0, _, _) -> "Foo"
...(14)>   (_, 0, _) -> "Buzz"
...(14)>   (_, _, z) -> z
...(14)> end
#Function<18.54118792/3 in :erl_eval.expr/5>
iex(16)> foobuzzbar.(0, 0, "Bar")
foobuzzbar.(0, 0, "Bar")
"FooBuzz"
iex(17)> foobuzzbar.(0, 1, "Bar")
foobuzzbar.(0, 1, "Bar")
"Foo"
iex(18)> foobuzzbar.(1, 0, "Bar")
foobuzzbar.(1, 0, "Bar")
"Buzz"
iex(19)> foobuzzbar.(1, 1, "Bar")
foobuzzbar.(1, 1, "Bar")
"Bar"

少し注意が必要なのは, 呼び出すときに変数と引数の間に「.」が必要なこと.
よく書き忘れます.

関数は変数に代入可能な値であるので, 関数へ引数として渡すこともできます.(高階関数というやつですね)

iex(20)> Enum.map [1, 2, 3], fn x -> x * 2 end
Enum.map [1, 2, 3], fn x -> x * 2 end
[2, 4, 6]

無名関数生成用に「&」という名前のヘルパ関数が用意されており, この関数を使って書き直すと

iex(21)> Enum.map [1, 2, 3], &(&1 * 2)
Enum.map [1, 2, 3], &(&1 * 2)
[2, 4, 6]

こうなります.
&1は第一引数のことで, もっと多くの引数を使いたい場合は&2,&3と増やしていきます.
高階関数を(無理やり)使って最初のFooBuzz問題を書きなおしてみると, こんな感じになるのかな?

iex(1)> foo = fn
...(1)>   (0, _, _) -> "Foo"
...(1)>   (_, _, _) -> ""
...(1)> end
#Function<18.54118792/3 in :erl_eval.expr/5>
iex(3)> buzz = fn
...(3)>   (_, 0, _) -> "Buzz"
...(3)>   (_, _, _) -> ""
...(3)> end
#Function<18.54118792/3 in :erl_eval.expr/5>
iex(5)> bar = fn
...(5)>   (0, _, _) -> ""
...(5)>   (_, 0, _) -> ""
...(5)>   (_, _, bar) -> bar
...(5)> end
#Function<18.54118792/3 in :erl_eval.expr/5>
iex(7)> foobuzzbar = fn
...(7)>   (x, y, z, funcs) -> (for f <- funcs, into: [], do: f.(x, y, z)) |> Enum.join
...(7)> end
#Function<4.54118792/4 in :erl_eval.expr/5>
iex(9)> foobuzzbar.(0, 0, "Bar", [foo, buzz, bar])
"FooBuzz"
iex(10)> foobuzzbar.(0, 1, "Bar", [foo, buzz, bar])
"Foo"
iex(11)> foobuzzbar.(1, 0, "Bar", [foo, buzz, bar])
"Buzz"
iex(12)> foobuzzbar.(1, 1, "Bar", [foo, buzz, bar])
"Bar"

また, みなさんがjavascriptで散々書いてきたであろう, 関数を返す関数というものも当然使うことができます.

iex(13)> greeter = fn name -> (fn -> "Hello #{name}" end) end
#Function<6.54118792/1 in :erl_eval.expr/5>
iex(14)> elixir_greeter = greeter.("Elixir")
#Function<20.54118792/0 in :erl_eval.expr/5>
iex(15)> elixir_greeter.()
"Hello Elixir"

この例のように, Elixirではスコープ内で未定義の変数が使われた場合, 順番に外のスコープをたどっていって同名の変数を探し, 見つかった場合自動的にその値を束縛するようになっています.(lexical scopeってやつかな?)
なので, 関数内で定義されていないはずの変数nameが使えているわけです.

実際に使ってみてつまずく

ここまでの基本項目を大体理解できたと思ったので, 別の小さな問題を考えてみることにしました.
問題の内容は, 自前の残高を持っており, それに対する入出金を行える銀行口座をモデル化した関数という, 大昔にちょっとだけ読んだSICP本に載っていたようなものです.

javascriptで書いた場合

account_maker = 
  function(_balance) { 
    var balance = _balance; 

    return function(delta) { 
              balance += delta; 
              return balance
           }
  }

実際に, Chromeのディベロッパツールで動かしてみると

$ account_maker = function(_balance) { var balance = _balance; return function(delta) { balance += delta; return balance}}
> function (_balance) { var balance = _balance; return function(delta) { balance += delta; return balance}}
$
$ account1 = account_maker(100)
> function (delta) { balance += delta; return balance}
$ account1(10)
> 110
$
$ account2 = account_maker(1000)
> function(delta) { balance += delta; return balance}
$ account2(-500)
> 500
$
$ account1(100)
> 210
$ account2(100)
> 600

期待する動きとしてはこんな感じです.

Elixirに書き直してみる

上記javascriptを, Elixirにそのまま移植してみると

account_maker =
  fn balance_ ->
    balance = balance_

    fn delta ->
      balance = balance + delta
      balance
    end
  end

こんな感じかな?
実際に動かしてみると...

iex(23)> account_maker =
...(23)>   fn balance_ ->
...(23)>     balance = balance_
...(23)>
...(23)>     fn delta ->
...(23)>       balance = balance + delta
...(23)>       balance
...(23)>     end
...(23)>   end
#Function<6.54118792/1 in :erl_eval.expr/5>
iex(25)> account1 = account_maker.(100)
#Function<6.54118792/1 in :erl_eval.expr/5>
iex(27)> account1.(10)
110
iex(29)> account1.(100)
200 #<-- 最初の10円入金が反映されてない

期待通りの挙動はせず.
どうして, このような挙動になるのか色々考えてみたのですが, おそらくjavascriptの変数は基本的にグローバルなもので, ローカルなスコープの中で未定義のまま使われた場合は,
その変数の宣言(var)が見つかるまでスコープを一つずつ外へとたどっていき, 見つかった時点でその外側の変数と同じものとなるのに対し,
Elixirの変数は, スコープ内で未定義の変数が使われた場合, 外のスコープに同名の変数を探しに行くところまでは同じですが, 見つかった時点で同じ値を束縛はするものの, 変数としては全く別ものとして定義されるので,
ローカルスコープ内でどれだけ変数の値を変えても(正確には値を変える、ではないですが), それは外の変数になんの影響も与えないのでしょう.
(この部分, あやふやなのでもっと正確な説明ができる方おりましたらお願いします)

なので, 関数の外に可変な変数を定義するという方法はうまくいきそうにない.
ではどうしたらよいか?
しばし考えて、変更後の値を元にまた新しい関数を作って返すという方法ならうまくいくかな?という考えで書いたのが次のようなコード.

defmodule Account do
  def make_account(balance) do
    fn delta ->
      IO.puts balance + delta
      make_account(balance + delta)
    end
  end
end

これまで無名関数の話をしてきましたが, 無名関数で自分自身を再帰的に呼び出す方法が非常にめんどくさそうだったので, 名前付き関数で勘弁してください.
この関数を実際動かしてみると

iex(1)> c("/home/vagrant/phoenix_sample/web/models/account.ex")
account.ex:1: warning: redefining module Account (current version loaded from Elixir.Account.beam)
[Account]
iex(2)> account1 = Account.make_account(100)
account1 = Account.make_account(100)
#Function<0.111007857/1 in Account.make_account/1>
iex(5)> account1 = account1.(10)
account1 = account1.(10)
110
#Function<0.111007857/1 in Account.make_account/1>
iex(6)> account1 = account1.(500)
account1 = account1.(500)
610
#Function<0.111007857/1 in Account.make_account/1>

だいたい欲しいものになってると思う.

structと組み合わせてみる

なんとなく欲しかったコードには近づいているものの, 残金を確認するためにIO.putsを使っていたり
account関数が残金表示も入出金も行っていたりとイマイチちぐはぐな印象を受けるので, structを使って少し整理してみます.

defmodule Account do
  defstruct balance: 0, withdraw: nil, deposit: nil

  def make_account(balance \\ 0) do
    %Account{
      balance: balance,
      withdraw: (fn delta -> make_account(balance - delta) end) ,
      deposit: (fn delta -> make_account(balance + delta) end)
    }
  end
end

実際に使ってみると

iex(1)> c("/home/vagrant/phoenix_sample/web/models/account.ex")                                                                                                                                  
account.ex:1: warning: redefining module Account (current version loaded from Elixir.Account.beam)
[Account]
iex(2)> account1 = Account.make_account(100)
account1 = Account.make_account(100)
%Account{balance: 100,                                                                                                                                                                           
 deposit: #Function<1.16987322/1 in Account.make_account/1>,                                                                                                                                     
 withdraw: #Function<0.16987322/1 in Account.make_account/1>}
iex(3)> account1.balance
account1.balance
100
iex(4)> account1 = account1.deposit.(1000)
account1 = account1.deposit.(1000)
%Account{balance: 1100,                                                                                                                                                                          
 deposit: #Function<1.16987322/1 in Account.make_account/1>,                                                                                                                                     
 withdraw: #Function<0.16987322/1 in Account.make_account/1>}
iex(5)> account1.balance
account1.balance
1100
iex(6)> account1 = account1.withdraw.(500)
account1 = account1.withdraw.(500)
%Account{balance: 600,                                                                                                                                                                           
 deposit: #Function<1.16987322/1 in Account.make_account/1>,                                                                                                                                     
 withdraw: #Function<0.16987322/1 in Account.make_account/1>}
iex(7)> account1.balance
account1.balance
600

なんとなく使い方もオブジェクトっぽくなって, 多言語からの移行してきた人も安心だネ!

本当に欲しかったもの

defmodule Account do
  defstruct balance: 0

  def withdraw(account = %Account{}, delta) do
    %Account{ balance: account.balance - delta }
  end

  def deposit(account = %Account{}, delta) do
    %Account{ balance: account.balance + delta }
  end
end

この問題に関しては, 最初からこれで良かった気がしてきた...

おわりに

以上, 無名関数についてツラツラ書いてみました
全然間違ったこと書いてると, とか, もっといい方法あるよといったご意見ありましたら, つっこみお願いします

参考文献, サイト

Programming Elixir: Functional, Concurrent, Pragmatic, Fun: http://www.amazon.co.jp/Programming-Elixir-Functional-Concurrent-Pragmatic/dp/1937785580

Scoping Rules in Elixir (and Erlang): http://elixir-lang.readthedocs.org/en/latest/technical/scoping.html

Elixir Comprehensions(内包表記) into: http://qiita.com/tbpgr/items/3ce116e53da9611000af

14
12
3

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
12