LoginSignup
3
1

More than 5 years have passed since last update.

すごいE本をElixirでやる(6章)

Last updated at Posted at 2015-07-16

より

6.1 関数型っぽくいこう!

Elixir で無名関数を評価するときは f.() という形式にする.
f() ではなく . がついた形であることに注意すること.

defmodule Hhfuns do
  def one, do: 1
  def two, do: 2

  def add(x, y), do: x.() + y.()
end

Hhfuns.add(Hhfuns.one, Hhfuncs.two)
#> ** (BadFunctionError) expected a function, got: 1
#>     orgmode_elixir_src.exs:5: Hhfuns.add/2
#>     (elixir) lib/code.ex:307: Code.require_file/2

Hhfuns.add(1, 2)
#> ** (BadFunctionError) expected a function, got: 1
#>     orgmode_elixir_src.exs:5: Hhfuns.add/2
#>     (elixir) lib/code.ex:307: Code.require_file/2

Hhfuns.add(&Hhfuns.one/0, &Hhfuns.two/0) # => 3

関数をモジュールの外から渡すための新しい記述

Elixir だと &(モジュール).(関数)/(アリティ) という形式になる.

defmodule Hhfuns do
  def increment([]), do: []
  def increment([h|t]), do: [h+1|increment(t)]

  def decrement([]), do: []
  def decrement([h|t]), do: [h-1|decrement(t)]

  def map(_, []), do: []
  def map(f, [h|t]), do: [f.(h)|map(f,t)]

  def incr(x), do: x + 1
  def decr(x), do: x - 1
end

l = [1,2,3,4,5]
Hhfuns.increment(l) # => [2, 3, 4, 5, 6]
Hhfuns.decrement(l) # => [0, 1, 2, 3, 4]
Hhfuns.map(&Hhfuns.incr/1, l) # => [2, 3, 4, 5, 6]
Hhfuns.map(&Hhfuns.decr/1, l) # => [0, 1, 2, 3, 4]

6.2 無名関数

defmodule Hhfuns do
  def increment([]), do: []
  def increment([h|t]), do: [h+1|increment(t)]

  def decrement([]), do: []
  def decrement([h|t]), do: [h-1|decrement(t)]

  def map(_, []), do: []
  def map(f, [h|t]), do: [f.(h)|map(f,t)]

  def incr(x), do: x + 1
  def decr(x), do: x - 1
end

f = fn() -> :a end
f.() # => :a

l = [1,2,3,4,5]
Hhfuns.map(fn(x) -> x + 1 end, l) # =>  [2, 3, 4, 5, 6]
Hhfuns.map(fn(x) -> x - 1 end, l) # =>  [0, 1, 2, 3, 4]

# Elixir ではこんな風にも書ける
Hhfuns.map(&(&1 + 1), l) # =>  [2, 3, 4, 5, 6]
Hhfuns.map(&(&1 - 1), l) # =>  [0, 1, 2, 3, 4]

Elixir での無名関数は

f = fn
  (0) -> "ぜろ"
  (1) -> "いち"
  (_) -> "そのた"
end

f.(0) # => "ぜろ"
f.(1) # => "いち"
f.(2) # => "そのた"

となる.(今調べるまで Elixir でも無名関数のパターンマッチができることを知らなかった!)

また無名関数を &(...) とも書ける.
この場合無名関数の中では引数 1 は &1 ,引数 2 は &2 のように表されている.

prepare_alarm = fn(room) ->
  IO.puts("Alarm set in #{room}")
  fn() ->
    IO.puts("Alarm tripped in #{room}! Call Batman!")
  end
end

alarm_ready = prepare_alarm.("bathroom") # => Alarm set in bathroom
alarm_ready.()                           # => Alarm tripped in bathroom! Call Batman!

関数のスコープとクロージャは同じ

defmodule Hhfuns do
  def a do
    secret = "pony"
    fn -> secret end
  end

  def b(f) do
    "a/0's password is " <> f.()
  end
end

Hhfuns.b(Hhfuns.a) # => a/0's password is pony

defmodule Hhfuns do
  def map(_, []), do: []
  def map(f, [h|t]), do: [f.(h)|map(f,t)]
end

:math.pow(5, 2) # => 25.0
base = 2
power_of_two = fn(x) -> :math.pow(base, x) end

Hhfuns.map(power_of_two, [1,2,3,4]) # => [2.0, 4.0, 8.0, 16.0]

Elixir でのスコープの再定義はこんな感じ.
Elixir の場合は ^ がないと再束縛できる(できてしまう)のでエラーにならないことに注意すること.

fn ->
  a = 1
  fn -> ^a = 2 end
end.().()
#> orgmode_elixir_src.exs:6: warning: the result of the expression is ignored (suppress the warning by assigning the expression to the _ variable)
#> orgmode_elixir_src.exs:14: warning: no clause will ever match
#> ** (MatchError) no match of right hand side value: 2
#>     orgmode_elixir_src.exs:14: anonymous fn/0 in :elixir_compiler_0.__FILE__/1
#>     (elixir) lib/code.ex:307: Code.require_file/2

fn ->
  a = 1
  fn(a) -> ^a = 2 end
end.().(2) # => 2

6.3 map、filter、foldなど

defmodule Hhfuns do
  # 偶数だけを保持する
  def even(l), do: Enum.reverse(even(l, []))
  defp even([], acc), do: acc
  defp even([h|t], acc) when rem(h, 2) === 0, do: even(t, [h|acc])
  defp even([_|t], acc), do: even(t, acc)

  # 60歳以上の男性だけ保持する
  def old_men(l), do: Enum.reverse(old_men(l, []))
  defp old_men([], acc), do: acc
  defp old_men([person = {:male, age} | people], acc) when age > 60 do
    old_men(people, [person|acc])
  end
  defp old_men([_|people], acc), do: old_men(people, acc)

  def filter(pred, l), do: Enum.reverse(filter(pred, l, []))
  defp filter(_, [], acc), do: acc
  defp filter(pred, [h|t], acc) do
    case pred.(h) do
      true  -> filter(pred, t, [h|acc])
      false -> filter(pred, t, acc)
    end
  end
end

numbers = Enum.to_list(1..10)
Hhfuns.filter(&(rem(&1, 2) === 0), numbers) # => [2, 4, 6, 8, 10]
Hhfuns.even(numbers)                        # => [2, 4, 6, 8, 10]

people = [{:male, 45}, {:female, 67}, {:male, 66}, {:female, 12}, {:unknown, 174}, {:male, 74}]
Hhfuns.filter(fn ({gender, age}) -> gender === :male && age > 60 end, people) # => [{:male, 66}, {:male, 74}]
Hhfuns.old_men(people)                                                        # => [{:male, 66}, {:male, 74}]

pred は predicate (述語) の略.

ちなみに Lisp で evenp みたいに最後が p になっているのもこれと同じ意味で,
Ruby での ? マークで終わるメソッドと同じような慣例だ.

Fold について.

defmodule Hhfuns do
  # リストの最大値を見つける
  def max([h|t]), do: max2(t, h)
  defp max2([], max), do: max
  defp max2([h|t], max) when h > max, do: max2(t, h)
  defp max2([_|t], max), do: max2(t, max)

  # リストの最小値を見つける
  def min([h|t]), do: min2(t, h)
  defp min2([], min), do: min
  defp min2([h|t], min) when h < min, do: min2(t, h)
  defp min2([_|t], min), do: min2(t, min)

  # リストの全要素の合計を出す
  def sum(l), do: sum(l, 0)
  defp sum([], s), do: s
  defp sum([h|t], s), do: sum(t, h + s)

  def fold(_, start, []), do: start
  def fold(f, start, [h|t]), do: fold(f, f.(h, start), t)
end

list = [1,7,3,5,9,0,2,3]
[h|t] = list

Hhfuns.fold(fn(a, b) when a > b -> a
              (_, b) -> b
            end,
            h, t) # => 9
Hhfuns.max(list) # => 9

Hhfuns.fold(fn(a, b) when a < b -> a
              (_, b) -> b
            end,
            h, t) # => 0
Hhfuns.min(list) # => 0

Hhfuns.fold(fn(a, b) -> a + b end, 0, Enum.to_list(1..6)) # => 21
Hhfuns.sum(Enum.to_list(1..6)) # => 21

引数の順番が Elixir の慣例とは異なっていることに気づいたが,実装したあとだったのでそのまま書いておく.

def fold([h|t], start, f) が Elixir Way だと思う.第一引数に subject,最後に function がくるはず.

Elixir で fold に相当する Enum.reduce/3 の引数をみたら,予想通りだった.うむ.

3
1
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
3
1