やっちのわいわい日記

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

Elixir勉強日記4 〜パターンマッチング・制御構造編〜

引き続きElixir勉強日記を書いていきます

ほんとは毎日できるといいんですけどうまくいかないです

毎日動画投稿しているYoutuberさんはすごいな 〜とか思う今日このごろです

f:id:nishikino3:20190611145254j:plain

elixirschool.com

今回はパターンマッチングと制御構造のところをまとめてやっていきをしていきます

そろそろ勉強を進めながらなにかしら作ってみたい気持ちになってきてますね

パターンマッチング

パターンマッチングはElixirの強力な部品で、単純な値やデータ構造、果ては関数でさえもマッチすることができます。 このレッスンではパターンマッチングの使い方から見ていきます。

強そう(小並)

マッチ演算子

Elixirでは、=演算子は実際には代数学での等号に値するマッチ演算子です。このマッチ演算子を通して値を代入し、その後マッチさせることができます。マッチングに成功すると方程式の結果を返します。失敗する場合はエラーを投げます。

iex> x = 1
1

一般的にプログラミング言語=って等号よりは代入(:=)を表すことが多いですがElixirにおいては代数学における等号に値するとあるので認識が違うみたいですね

上の例だと普通に代入しているように見えます

iex> 1 = x
1
iex> 2 = x
** (MatchError) no match of right hand side value: 1

あーーね

左辺に数字があるのは経験上気持ちわるいのですが、等号と同じという役割であるとなんとなく納得ができる感じがします

マッチ演算子はコレクションでも利用できます

# リスト
iex> list = [1, 2, 3]
iex> [1, 2, 3] = list
[1, 2, 3]
iex> [] = list
** (MatchError) no match of right hand side value: [1, 2, 3]

iex> [1 | tail] = list
[1, 2, 3]
iex> tail
[2, 3]
iex> [2|_] = list
** (MatchError) no match of right hand side value: [1, 2, 3]

# タプル
iex> {:ok, value} = {:ok, "Successful!"}
{:ok, "Successful!"}
iex> value
"Successful!"
iex> {:ok, value} = {:error}
** (MatchError) no match of right hand side value: {:error}

コレクションにおいても同様に使えるところをみるとコレクションを集合って考えたら代数学的に等号っていう表現がなんとなくわかるような気がします

ピン演算子

マッチ演算子は左辺に変数が含まれている時に代入操作を行います。 この変数を再び束縛するという挙動は望ましくない場合があります。 そうした状況のために、ピン演算子(^)があります。

ピン演算子で変数を固定すると、新しく再束縛するのではなく既存の値とマッチします。

変数定義・代入したときに使うのがマッチ演算子=で、マッチするか確認するのに使うのがピン演算子なんですかね

iex> x = 1
1
iex> ^x = 2
** (MatchError) no match of right hand side value: 2
iex> {x, ^x} = {2, 1}
{2, 1}
iex> x
2

上の例はピン演算子を使うともともと1が代入されているxと2はマッチしないんですがwwwって怒られてる感じですかね

上の例は分かるピン演算子で違う値に代入しようとしてMatchErrorを出しているからな

だが下の例はどういうことだアァァァァ!!??

落ち着いて考えるならマッチ演算子でタプルの左のxに2が代入されて、ピン演算子で1を代入しようとしたけど値を変える代入なので代入をやめたっていう認識をしました

Elixir 1.2ではマップのキーや関数の節でのピン演算子がサポートされました。

iex> key = "hello"
"hello"
iex> %{^key => value} = %{"hello" => "world"}
%{"hello" => "world"}
iex> value
"world"
iex> %{^key => value} = %{:hello => "world"}
** (MatchError) no match of right hand side value: %{hello: "world"}

この例がマップのキーを文字列からタプルに変えようとしているのに気づくまで1分かかりました

iex> greeting = "Hello"
"Hello"
iex> greet = fn
...>   (^greeting, name) -> "Hi #{name}"
...>   (greeting, name) -> "#{greeting}, #{name}"
...> end
#Function<12.54118792/2 in :erl_eval.expr/5>
iex> greet.("Hello", "Sean")
"Hi Sean"
iex> greet.("Mornin'", "Sean")
"Mornin', Sean"
iex> greeting
"Hello"

ファッ

関数内のgreetingと引数をピン演算子で確認して第一引数が"Hello"だったら"Hi"を表示してそれ以外の場合はそのまま表示してるって感じか

てかしれっと関数のマッチングしてそう

実際に動かすコードでこういう書き方ってするのか私気になります

制御構造

ifとunless

ひょっとすると以前にif/2と出くわしているかもしれませんし、Rubyを使っていればunless/2をご存知でしょう。Elixirではこの2つはほとんど同じように作用しますが、言語の構成要素としてではなく、マクロとして定義されています。この実装はKernel moduleで知ることができます。

Elixirでは偽とみなされる値はnilと真理値のfalseだけだということに、留意すべきです。

falsenilのみが偽となるのは基本的にはRubyと同様の制御がされるっぽいですね

言語の構成要素としてではなくマクロとして定義されているっていうあたりは、モジュールを呼び出す必要がないってことでいいんですかね

case

複数のパターンに対してマッチする必要があるなら、case/2を使うことができます

こちらもRubyと似たような使い方ができそうです

iex> case {:ok, "Hello World"} do
...>   {:ok, result} -> result
...>   {:error} -> "Uh oh!"
...>   _ -> "Catch all"
...> end
"Hello World"

あくまでも関数型言語なので分岐した先は関数っていう感覚で書くっていうのに慣れて生きていきたいです

_変数はcase/2命令文の中に含まれる重要な要素です。これが無いと、マッチするものが見あたらない場合にエラーが発生します

Rubyでいうところのdefaultってことなんすかね

iex> case :even do
...>   :odd -> "Odd"
...> end
** (CaseClauseError) no case clause matching: :even

iex> case :even do
...>   :odd -> "Odd"
...>   _ -> "Not Odd"
...> end
"Not Odd"

どうでもいいけどcase/2の引数に定数を渡すの気持ち悪いですね

_を”他の全て”にマッチするelseと考えましょう。

case/2はパターンマッチングに依存しているため、パターンマッチングと同じルールや制限が全て適用されます。既存の変数に対してマッチさせようという場合にはピン^演算子を使わなくてはいけません

iex> pie = 3.14
3.14
iex> case "cherry pie" do
...>   ^pie -> "Not so tasty"
...>   pie -> "I bet #{pie} is tasty"
...> end
"I bet cherry pie is tasty"

この辺り覚えてないと詰みかねないですね

case/2のもう1つの素晴らしい特徴として、ガード節に対応していることがあげられます

iex> case {1, 2, 3} do
...>   {1, x, 3} when x > 0 ->
...>     "Will match"
...>   _ ->
...>     "Won't match"
...> end
"Will match"

ガード節 分岐ができて いい感じ(575)

cond

値ではなく、条件をマッチさせる必要がある時には、cond/1を使うことができます。これは他の言語でいうところのelse ifやelsifのようなものです

引数のないcase/2みたいな認識でいいんすかね

iex> cond do
...>   2 + 2 == 5 ->
...>     "This will not be true"
...>   2 * 2 == 3 ->
...>     "Nor this"
...>   1 + 1 == 2 ->
...>     "But this will"
...> end
"But this will"

caseのように、condはマッチしない場合にエラーを発生させます。これに対処するには、trueになる条件を定義すればよいです

それはそう

with

特殊形式のwith/1はネストされたcase/2文を使うような時やきれいにパイプできない状況に便利です。with/1式はキーワード, ジェネレータ, そして式から成り立っています。

ジェネレータについてはリスト内包表記のレッスンでより詳しく述べますが、今は<-の右側と左側を比べるのにパターンマッチングが使われることを知っておくだけでよいです。

with/1の簡単な例から始め、その後さらなる例を見てみましょう

なんかややこしそうなやつですね

iex> user = %{first: "Sean", last: "Callan"}
%{first: "Sean", last: "Callan"}
iex> with {:ok, first} <- Map.fetch(user, :first),
...>      {:ok, last} <- Map.fetch(user, :last),
...>      do: last <> ", " <> first
"Callan, Sean"

これってuserのマップにfirstlastのkeyがあるか確認して表示してるってことなんすかね

式がマッチに失敗した場合はマッチしない値が返されます

iex> user = %{first: "doomspork"}
%{first: "doomspork"}
iex> with {:ok, first} <- Map.fetch(user, :first),
...>      {:ok, last} <- Map.fetch(user, :last),
...>      do: last <> ", " <> first
:error

ここではMap.fetch/2の戻り値が:errorなのでそいつが返ってきてるということですね

それでは、with/1を使わない長めの例と、それをどのようにリファクタリングできるかを見てみましょう

まずはcase/2で書いた場合ですね

case Repo.insert(changeset) do
  {:ok, user} ->
    case Guardian.encode_and_sign(user, :token, claims) do
      {:ok, jwt, full_claims} ->
        important_stuff(jwt, full_claims)

      error ->
        error
    end

  error ->
    error
end

それをwith/1に置き換えてこうじゃ!

with {:ok, user} <- Repo.insert(changeset),
     {:ok, jwt, full_claims} <- Guardian.encode_and_sign(user, :token, claims),
     do: important_stuff(jwt, full_claims)

優勝!!!wwww

Elixir 1.3からはwith/1でelseを使えます

import Integer

m = %{a: 1, c: 3}

a =
  with {:ok, number} <- Map.fetch(m, :a),
    true <- is_even(number) do
      IO.puts "#{number} divided by 2 is #{div(number, 2)}"
      :even
  else
    :error ->
      IO.puts("We don't have this item in map")
      :error

    _ ->
      IO.puts("It is odd")
      :odd
  end

これはcaseのようなパターンマッチングを提供することで、エラーを扱いやすくします。渡されるのはマッチングに失敗した最初の表現式の値です。

これってwith {:ok, number} <- Map.fetch(m, :a):errorだったらエラーを表示して:errorを返してるってことか!!!

天才!www

第4回まとめ

パターンマッチング

  • =(マッチ演算子)は代入というより等号を意味している.変数の再代入は可能
  • すでに定義されている変数に関しては^(ピン演算子)で値の確認ができる(等号
  • コレクションの要素毎の代入も可能

制御構造

  • if/2unless/2カーネルモジュールが与えている機能でおおよそRubyと同じ動作をする
  • case/2もおおよそRubyと同様に動作するがマッチングしない場合はdefaultではなく_
  • case/2のマッチングにはパターンマッチングと同じ制限が適用されるため既存の変数に対してはピン演算子を使う必要がある
  • cond/1else ifと似たような動作をする
  • case/2がネストしていたりする場合にはwith/1を使うといい感じにリファクタできる場合がある(errorのハンドラーとか)

Elixir特有の要素を掘り出していくとこれ使えるのかどうか疑問に思うことがある機能がある気がしますが、今回やったあたりをスマートに使いこなせるようになりたいですね