やっちのわいわい日記

電通大に編入した元高専生の日記。日記を書きつつ編入のこととか勉強のこととか書こうと思っています。編入とかで質問があるかたは@amhflcl0514にDMくださいお

Elixir勉強日記5 〜関数・パイプライン演算子編〜

Elixir勉強日記5回目です

最近はずっとRailsばっかりやっててElixir触るの忘れがちです

引き続き進めて生きます

f:id:nishikino3:20190611145254j:plain

elixirschool.com

今回はついに関数型言語において関数という題で進めることになります...!

5回目まで関数が出てこない関数型言語のドキュメントってある意味素晴らしいですよね

関数

Elixirや多くの関数型言語では、関数は第一級市民(≒ファーストクラスオブジェクト)です。 Elixirにおける関数の種類について、それぞれどう異なっていて、どのように使うのかを学んでいきます

いや関数型言語だから間違ってないけど草

匿名関数

その名前が暗に示している通り、匿名関数は名前を持ちません。 Enumのレッスンで見たように、匿名関数はたびたび他の関数に渡されます。 Elixirで匿名関数を定義するには、fnとendのキーワードが必要です。 これらの内側で、任意の数の引数と->で隔てられた関数の本体とを定義することができます。

iex> sum = fn (a, b) -> a + b end
iex> sum.(2, 3)
5

この辺の記法ってjsやってると違和感ないんでしょうかね

.の存在絶対忘れそう

&省略記法

匿名関数を利用するのはElixirでは日常茶飯事なので、そのための省略記法があります:

iex> sum = &(&1 + &2)
iex> sum.(2, 3)
5

おそらく見当が付いているでしょうが、省略記法では引数を&1、&2、&3などとして扱うことができます。

省略記法いいですけど知らない人見たら結構謎ですね

パターンマッチング

Elixirではパターンマッチングは変数だけに限定されているわけではなく、次の項にあるように、関数へと適用することができます。

Elixirはパターンマッチングを用いてマッチする可能性のある全てのオプションをチェックし、最初にマッチするオプションを選択して実行します:

iex> handle_result = fn
...>   {:ok, result} -> IO.puts "Handling result..."
...>   {:ok, _} -> IO.puts "This would be never run as previous will be matched beforehand."
...>   {:error} -> IO.puts "An error has occurred!"
...> end

iex> some_result = 1
iex> handle_result.({:ok, some_result})
Handling result...

iex> handle_result.({:error})
An error has occurred!

すごい!この分岐なかなか美しいですね!

_が意味するのはこの変数を使わないってことっぽいですかね

2つ目の関数には入らないみたいです

名前付き関数

関数を名前付きで定義して後から呼び出せるようにすることができます。 こうした名前付き関数はモジュール内部でdefキーワードを用いて定義されます。 モジュールについては次のレッスンで学習しますので、今のところ名前付き単体に着目しておきます。

モジュール内部で定義される関数は他のモジュールからも使用することができます。 これはElixirでは特に有用な組み立て部品になります

個人的には関数は名前をつけるものっていう感覚なので別に名前をつけるのが当然っていう感覚はないみたいですね

defmodule Greeter do
  def hello(name) do
    "Hello, " <> name
  end
end

iex> Greeter.hello("Sean")
"Hello, Sean"

関数本体が1行で済むなら、do:を使ってより短くすることができます:

defmodule Greeter do
  def hello(name), do: "Hello, " <> name
end

1行で書けてるけど実際こういう記述ってするんですかね 可読性びみょい気が

パターンマッチングの知識を身にまとったので、名前付き関数を使った再帰を探検しましょう:

defmodule Length do
  def of([]), do: 0
  def of([_ | tail]), do: 1 + of(tail)
end

iex> Length.of []
0
iex> Length.of [1, 2, 3]
3

身にまとったってなんやねんwww

慣れないと気持ち悪い例ですが理解できると美しいですね

これが関数型言語の魅力なんでしょうかね

関数の命名とアリティ

以前言及したとおり、関数は名前とアリティ(引数の数)の組み合わせで命名されます。 つまり、以下のようなことができるということです:

defmodule Greeter2 do
  def hello(), do: "Hello, anonymous person!"   # hello/0
  def hello(name), do: "Hello, " <> name        # hello/1
  def hello(name1, name2), do: "Hello, #{name1} and #{name2}"
                                                # hello/2
end

iex> Greeter2.hello()
"Hello, anonymous person!"
iex> Greeter2.hello("Fred")
"Hello, Fred"
iex> Greeter2.hello("Fred", "Jane")
"Hello, Fred and Jane"

関数名の一覧を上記のコメントに載せました。 例えば、1つめの実装は引数を取らないのでhello/0、2つ目は1つの引数を取るのでhello/1となります。 他の言語におけるオーバーロードとは違い、これらは互いに 異なる 関数として扱われます。 (さっき扱ったパターンマッチングは 同じ 数の引数を取る関数定義が複数ある場合のみ適用されます)

引数の数で異なる関数として認識されるんすね

この辺は他の言語と違うのでしっかり覚えておいた方がよさそうですね

関数とパターンマッチング

※この節ながい

内部では、関数は実行された時の引数をパターンマッチングしています。

マップを受け取るが、特定のキーにだけ関心がある関数が必要であるとしましょう。 私たちは次のようにキーの有無に基づいて引数をパターンマッチすることができます:

defmodule Greeter1 do
  def hello(%{name: person_name}) do
    IO.puts "Hello, " <> person_name
  end
end

関数の受け渡しにパターンマッチング使ってるので色々応用が効きそうですね

この辺りも頭に入れておかないと違和感しかないですね

今度はFredという名前の人物を表すマップを持っているとしましょう。

iex> fred = %{
...> name: "Fred",
...> age: "95",
...> favorite_color: "Taupe"
...> }

Greeter1.hello/1 をfredのマップで実行するとこのような結果となります:

# call with entire map
...> Greeter1.hello(fred)
"Hello, Fred"
# :nameキーを 含まない マップで関数を実行するとどうなるでしょうか?

# call without the key we need returns an error
...> Greeter1.hello(%{age: "95", favorite_color: "Taupe"})
** (FunctionClauseError) no function clause matching in Greeter1.hello/1

    The following arguments were given to Greeter1.hello/1:

        # 1
        %{age: "95", favorite_color: "Taupe"}

    iex:12: Greeter1.hello/1

このような挙動となる理由は、Elixirは関数が実行された際の引数を関数で定義されたアリティに対してパターンマッチさせているためです。

Greeter1.hello/1にデータが届いた際どのように見えるか考えてみましょう:

はぇ〜〜〜〜〜〜〜

# incoming map
iex> fred = %{
...> name: "Fred",
...> age: "95",
...> favorite_color: "Taupe"
...> }

Greeter1.hello/1は次のような引数を期待します:

%{name: person_name}

Greeter1.hello/1では、私たちが渡したマップ(fred)は引数(%{name: person_name})に対して評価されます:

%{name: person_name} = %{name: "Fred", age: "95", favorite_color: "Taupe"}

これは渡されたマップの中にnameに対応するキーを見つけます。 マッチがありました!このマッチの成功によって、右辺のマップ(つまり fredマップ)の中にある:nameキーの値は左辺の変数(person_name)に格納されます。

さて、Fredの名前をperson_nameにアサインしたいが、人物マップ全体の値も保持したいという場合はどうするのでしょう?挨拶を出力した後IO.inspect(fred)を使いたいとしましょう。 この時点では、マップの:nameキーだけをパターンマッチしているので、そのキーの値だけが変数に格納され、関数はFredの残りの値に関する知識を持っていません。

これを保持するためには、マップ全体を変数にアサインして使用できるようにする必要があります。

新しい関数を作ってみましょう:

defmodule Greeter2 do
  def hello(%{name: person_name} = person) do
    IO.puts "Hello, " <> person_name
    IO.inspect person
  end
end

Elixirは引数を渡されたままパターンマッチするということを覚えておいてください。 そのためこのケースでは、それぞれが渡された引数に対してパターンマッチして、マッチした全てのものを変数に格納します。 まずは右辺を見てみましょう:

person = %{name: "Fred", age: "95", favorite_color: "Taupe"}

ここでは、personが評価され、fredマップ全体が格納されました。 次のパターンマッチに進みます:

%{name: person_name} = %{name: "Fred", age: "95", favorite_color: "Taupe"}

これは、マップをパターンマッチしてFredの名前だけを保持したオリジナルのGreeter1関数と同じです。 これによって1つではなく2つの変数を使用することができます:

引数を2つのアリティでそれぞれパターンマッチングしてるのか...

personは%{name: "Fred", age: "95", favorite_color: "Taupe"}を参照します person_nameは"Fred"を参照します これでGreeter2.hello/1を実行したとき、Fredの全ての情報を使用することができます:

この例って関数渡す側はタプルで渡すけど関数側ではタプルの一部の情報を変数でもちたいっていうのが前提にあるって感じなんですかね

# call with entire person
...> Greeter2.hello(fred)
"Hello, Fred"
%{age: "95", favorite_color: "Taupe", name: "Fred"}
# call with only the name key
...> Greeter4.hello(%{name: "Fred"})
"Hello, Fred"
%{name: "Fred"}
# call without the name key
...> Greeter4.hello(%{age: "95", favorite_color: "Taupe"})
** (FunctionClauseError) no function clause matching in Greeter2.hello/1

    The following arguments were given to Greeter2.hello/1:

        # 1
        %{age: "95", favorite_color: "Taupe"}

    iex:15: Greeter2.hello/1

入ってきたデータに対して独立してパターンマッチして、関数の中でそれらを使用できるようにしたことで、Elixirは複数の奥行きでパターンマッチするという点を確認しました。

リストの中で%{name: person_name}とpersonの順序を入れ替えたとしても、それぞれがfredとマッチングするので同じ結果となります。

つまり関数渡す側からは順番を気にする必要がないと

変数とマップを入れ替えてみましょう:

defmodule Greeter3 do
  def hello(person = %{name: person_name}) do
    IO.puts "Hello, " <> person_name
    IO.inspect person
  end
end

Greeter2.hello/1で使用した同じデータで実行してみます:

# call with same old Fred
...> Greeter3.hello(fred)
"Hello, Fred"
%{age: "95", favorite_color: "Taupe", name: "Fred"}

%{name: person_name} = person}%{name: person_name}がpersonに対してパターンマッチしているように見えたとしても、実際には それぞれが 渡された引数をパターンマッチしているということを覚えておいてください。

覚えておきます

まとめてくれてありがとうございます↓

まとめ: 関数は渡されたデータをそれぞれの引数で独立してパターンマッチします。 関数の中で別々の変数に格納するためにこれを利用できます。

プライベート関数

他のモジュールから特定の関数へアクセスさせたくない時には関数をプライベートにすることができます。 プライベート関数はそのモジュール自身の内部からのみ呼び出すことが出来ます。 Elixirではdefpを用いて定義することができます:

defmodule Greeter do
  def hello(name), do: phrase() <> name
  defp phrase, do: "Hello, "
end

iex> Greeter.hello("Sean")
"Hello, Sean"

iex> Greeter.phrase
** (UndefinedFunctionError) function Greeter.phrase/0 is undefined or private
    Greeter.phrase()

defpですね!!おぼえましたし

ガード

制御構造レッスンでもガードについて少しだけ触れましたが、これを名前付き関数に適用する方法を見ていきます。 Elixirはある関数にマッチするとそのガードを全てテストします。

以下の例では同じ名前を持つ2つの関数があります。ガードを頼りにして、引数の型に基づいてどちらを使うべきか決定します:

defmodule Greeter do
  def hello(names) when is_list(names) do
    names
    |> Enum.join(", ")
    |> hello
  end

  def hello(name) when is_binary(name) do
    phrase() <> name
  end

  defp phrase, do: "Hello, "
end

iex> Greeter.hello ["Sean", "Steve"]
"Hello, Sean, Steve"

これだと関数はオーバーロードされない感じなんですかね

型が指定されているから別の関数として扱われてるってことなの...?

ちょっと調べたらガード節が書いてあると同名関数でも別の処理をされるっぽいですね

デフォルト引数

引数にデフォルト値が欲しい場合、引数 \ デフォルト値の記法を用います

バックスラッシュふたつ

イコール置きたいところですがそれやるとさっきのパターンマッチングの例の通り動作して正常に動かないんですかね?

間違えそう

defmodule Greeter do
  def hello(name, language_code \\ "en") do
    phrase(language_code) <> name
  end

  defp phrase("en"), do: "Hello, "
  defp phrase("es"), do: "Hola, "
end

iex> Greeter.hello("Sean", "en")
"Hello, Sean"

iex> Greeter.hello("Sean")
"Hello, Sean"

iex> Greeter.hello("Sean", "es")
"Hola, Sean"

この例もしれっとパターンマッチングして呼び出す関数を分けてますね〜(phrase/1)

慣れないと結構気持ち悪い

先ほどのガードの例をデフォルト引数と組み合わせると、問題にぶつかります。 どんな風になるか見てみましょう:

defmodule Greeter do
  def hello(names, language_code \\ "en") when is_list(names) do
    names
    |> Enum.join(", ")
    |> hello(language_code)
  end

  def hello(name, language_code \\ "en") when is_binary(name) do
    phrase(language_code) <> name
  end

  defp phrase("en"), do: "Hello, "
  defp phrase("es"), do: "Hola, "
end

** (CompileError) iex:31: 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

one should write:

    def foo(a, b \\ :default)
    def foo(:first_clause, b) do ... end
    def foo(:second_clause, b) do ... end

def hello/2 has multiple clauses and defines defaults in one or more clauses
    iex:31: (module)

Elixirは複数のマッチング関数にデフォルト引数があるのを好みません。混乱の元になる可能性があります。 これに対処するには、デフォルト引数付きの関数を先頭に追加します:

好まないからコンパイルエラーだぜ!!!

同名関数にそれぞれ同一のデフォルト引数があるのは気持ち悪いからやめてクレメンスってことなんすね

defmodule Greeter do
  def hello(names, language_code \\ "en")

  def hello(names, language_code) when is_list(names) do
    names
    |> Enum.join(", ")
    |> hello(language_code)
  end

  def hello(name, language_code) when is_binary(name) do
    phrase(language_code) <> name
  end

  defp phrase("en"), do: "Hello, "
  defp phrase("es"), do: "Hola, "
end

iex> Greeter.hello ["Sean", "Steve"]
"Hello, Sean, Steve"

iex> Greeter.hello ["Sean", "Steve"], "es"
"Hola, Sean, Steve"

書き直す例もあってありがたい(なんでこれでいけるのかアレだけどそういうものっていう認識をした)

パイプライン演算子

パイプライン演算子(|>)はある式の結果を別の式に1つ目の引数として渡します。

まってましたパイプライン演算子

導入

プログラミングは厄介になりえます。実際とても厄介なことに、関数呼び出しを深くしすぎると把握するのがとても難しくなります。以下のネストされた関数を考えてみてください:

foo(bar(baz(new_function(other_function()))))

これ!!!!講義でLispなどの関数言語触ったときにカッコ閉じを数えるのつらすぎて無理無理の無理になってた記憶 しかない

ここでは、other_function/0の値をnew_function/1に、new_function/1の値をbaz/1に、baz/1の値をbar/1に、そして最後にbar/1の結果をfoo/1に渡しています。 Elixirではパイプライン演算子を使うことによって構文的な混沌に対し現実的にアプローチします。 パイプライン演算子(|>)は 一つの式の結果を取り、それを渡します。先ほどのコードスニペットをパイプライン演算子で書き直すとどうなるか、見てみましょう。

other_function() |> new_function() |> baz() |> bar() |> foo()

パイプライン演算子は結果を左に取りそれを右側に渡します。

みやすい!優勝!!

この例のセットのためにElixirのStringモジュールを使います。

おおまかに文字列をトークン化する

iex> "Elixir rocks" |> String.split()
["Elixir", "rocks"]

全てのトークンを大文字にする

iex> "Elixir rocks" |> String.upcase() |> String.split()
["ELIXIR", "ROCKS"]

文字列の終わりを調べる

iex> "elixir" |> String.ends_with?("ixir")
true

ベストプラクティス

関数のアリティが1より多いなら括弧を使うようにしてください。括弧の有無はElixirにおいてはたいした問題ではありませんが、あなたのコードを誤解するかもしれない他のプログラマにとっては問題です。もし3つ目の例でString.ends_with?/2から括弧を削除すると, 以下のように警告されます。

iex> "elixir" |> String.ends_with? "ixir"
warning: parentheses are required when piping into a function call. For example:

  foo 1 |> bar 2 |> baz 3

is ambiguous and should be written as

  foo(1) |> bar(2) |> baz(3)

true

関数のカッコの有無ってわりと可読性的な意味で扱い難しい...

まとめ

  • 無名関数fn(a, b) -> a + b end
  • 無名関数省略記法 &(&1 + &2)
  • パターンマッチングによる関数の分岐がしゅごい
  • アリティ(引数の数)が違うと同名でも別の関数として扱われる
  • 関数の引数はパターンマッチングが用いられいる
  • プライベート関数はdefp
  • ガードを用いることで引数の型に基づいて使う関数を決められる def hoge(names) when is_list(names) do...
  • デフォルト引数は\\、ただしガードと組合わせるときは注意
  • 関数はネストしないでパイプライン演算子(|>)を使おう

なかなかElixirが便利な理由が見えてきましたね〜〜

関数型言語のコードってなかなか馴染みがなくて難しいです