はじめに
はじめてElixirを(3ヵ月だけ)触った時の勉強メモを書いています。初心者向けの内容です。
普段は主にPythonを使っているのでPythonと比較したりします。
Elixirとは
Elixirは関数型言語
に分類されるプログラミング言語で、Erlangの仮想マシン(BEAM)上で動作します。
Wikipedia
基本データ型
Elixirでは値が不変(immutable)ですが、変数は不変ではありません。
真理値
真理値として小文字で始まるtrue
とfalse
があります。
false
とnil
以外は真とみなされるので、Pythonと違って0
の場合はtrue
になります。
アトム(atom)
名前が自身の値を表わしている定数です。書き方は3つあります。
書き方①
例: Hoge
, Hoge123
, HogeFuga
- ルール
- アルファベットの大文字で始まる
- 2文字目以降はアルファベット、数字、アンダースコアである
- 用途(慣習)
- キャメルケースでモジュールの名前を記述する
書き方②
例: :hoge123
, :_hoge
, :hoge@fuga
, :hoge?
- ルール
- コロンで始まる
- 2文字目はアルファベットかアンダースコアである
- 3文字目以降には数字または
@
でもよい - 最後の文字は
?
か!
でもよい
- 用途(慣習)
- スネークケースでモジュールの名前以外の目的で使用する
- 例外としてErlangのモジュール名はこの書き方で参照する
書き方③
例: :"hoge/fuga"
, :"hoge@fuga"
- ルール
- ダブルクオートで囲んだ文字列の前にコロンを置く
整数、浮動小数、文字列の紹介は Elixir School へ。
コレクション型
Elixirのコレクションはどんな型も保持することができます。
リスト(List)
[1, 2, 3]
- 特徴
- 連結データ構造:空か
head
(先頭要素)とtail
(それ以降の要素)に分ける - 順番にアクセスするため、添字での参照やサイズの取得は遅い
- 先頭に要素を追加するのは速い
- 連結データ構造:空か
- 用途
- 汎用的なデータ型
リストの関数
iex> list = [1, 2, 3, 4, 5]
iex> [head | tail] = list
iex> head
1
iex> tail
[2, 3, 4, 5]
for x <- [1, 2, 3, 4, 5] do
IO.puts x
end
list = [1, 2, 3, 4, 5]
Enum.fetch(list, 1)
#=> 2
a = [1, 2, 3, 4, 5]
[0 | a]
#=> [0, 1, 2, 3, 4, 5]
a = [1, 2, 3, 4, 5]
[0] ++ a
#=> [0, 1, 2, 3, 4, 5]
cons演算子|
:右辺がリストの場合、左辺の値を右辺のリストの先頭に追加します。
list = [1, 2, 3, 4, 5]
Enum.join(list)
#=> "12345"
Enumモジュールはたくさんの便利関数を提供しています。
https://hexdocs.pm/elixir/Enum.html
タプル(Tuple)
{:ok, 1}
- 特徴
- 要素は順序を持つ
- 特定の要素アクセスやサイズの取得は速い
- 要素の追加や変更などは遅い(その度に新しいタプルを作る)
- リストの
++
演算子やEnum.each/2
関数にあたるものがタプルに存在しない -
for
マクロが使えない
- 用途
- 特殊な用途のデータ型
- 関数の戻り値に使用する
マップ(Map)
%{"hoge" => 1, "fuga" => 2}
- 特徴
- キーは順序を持たない
- 1つのキーが1つの値を持つ
- キーがアトムの場合は簡易表記ができる
-
%{:hoge => 1, :fuga => 2}
と%{fuga: 2, hoge: 1}
は等価
-
- 用途
- 汎用的なデータ型
マップの関数
map = %{"hoge" => 1, "fuga" => 2}
map["hoge"]
#=> 1
map = %{:hoge => 1, :fuga => 2}
map.hoge
#=> 1
キーがアトムの場合、ドット記法を使うことができます。
map = %{"hoge" => 1, "fuga" => 2}
map = %{map | "hoge" => 2}
#=> %{"fuga" => 2, "hoge" => 2}
map = %{"hoge" => 1, "fuga" => 2}
Map.merge(map, %{"hoge" => 2, "piyo" => 3})
#=> %{"fuga" => 2, "hoge" => 2, "piyo" => 3}
その他の関数はMapモジュールにあります。
https://hexdocs.pm/elixir/Map.html
キーワードリスト(Keyword List)
[hoge: 1, fuga: 2]
- 特徴
- キーは順序を持つ
- 同じキーは複数個含まれていても良い(最初のが取得される)
- キーワードリストの正体は下記3つの条件を満たすリスト
- すべての要素はタプル
- タプルの要素数はすべて2
- タプルの第1要素は常にアトム
- 用途
- 特殊な用途のデータ型
- 関数の最後の引数に指定されるのが主な用途(Pythonでいうと
kwargs
)-
IO.inspect hoge: 1, fuga: 2
とIO.inspect [hoge: 1, fuga: 2]
は等価
-
キーワードリストの関数
a = [hoge: 1, fuga: 2]
a[:hoge]
#=> 1
Mapと違ってドット記法が使えません。
a = [hoge: 1, fuga: 2]
a = [{:piyo, 3} | a]
#=> [piyo: 0, hoge: 1, fuga: 2]
a = [hoge: 1, fuga: 2]
a = [piyo: 0] ++ a
#=> [piyo: 0, hoge: 1, fuga: 2]
キーワードリストは特殊なリストなので、キーワードリストを操作する場合、リストと同じ方法が使えます。
Keywordモジュールもあります。
https://hexdocs.pm/elixir/Keyword.html
モジュールと関数
基本例
defmodule Hello do
def greet(name \\ "world") do
IO.puts "Hello, #{name}!"
end
end
Hello.greet("Alice")
#=> Hello, Alice!
Hello.greet()
#=> Hello, world!
Pythonと違ってモジュールの外では関数を定義できません。
関数の戻り値
関数の中で最後に評価された式の値が関数の戻り値になります。
Python等のreturn
に相当する仕組みはありません。
アリティ(arity)
引数を渡さずにHello.greet()
で実行すると次のようなエラーメッセージが表示されます。
** (UndefinedFunctionError) function Hello.greet/0 is undefined or private. Did you mean one of:
* greet/1
Hello.greet()
Hello.greet/0
は未定義またはプライベートだと言っています。ここの0
はアリティで、関数の引数を意味します。
Hello.greet/0
とHello.greet/1
は別物です。
※プライベート関数はdefp
で定義します。
無名関数
文字列やアトムなどと同様に「値」の一種です。次のように無名関数を定義して変数f
にセットします。
f = fn (a, b) -> a + b end
f.(2, 3)
#=> 5
パイプライン演算子(|>)
左辺の関数の戻り値を右辺の関数の第一引数に渡すことができます。
例えば、Enum.sort/1
とEnum.reverse/1
でリストをソートして反転する場合、次のようにシンプルな記述ができます。
[1,3,2,5,4] |> Enum.sort |> Enum.reverse
#=> [5, 4, 3, 2, 1]
構造体
defmodule Player do
defstruct name: "default", level: 1
# defstruct [{:name, "default"}, {:level, 1}] も同じ
end
player = %Player{name: "hoge", level: 10}
#=> %Player{level: 10, name: "hoge"}
player = %Player{:name => "hoge", :level => 10}
#=> %Player{level: 10, name: "hoge"}
defstruct
は構造体を定義するためのマクロです。構造体のキーとなるアトムのリストを渡します。
player = %Player{name: "hoge", level: 10}
player.name
#=> "hoge"
Mapと違って角括弧での取得ができません。
マップとの関係
Elixirの構造体とマップはいずれもErlangのマップの拡張として実装されています。構造体は__struct__
という特別なフィールドを持っています。
Mapの関数は構造体でも使用できます。
player = %Player{name: "hoge", level: 10}
player.__struct__ #=> Player
%Player{} |> is_map
#=> true
モジュールのディレクティブ
Elixirには3つのディレクティブが用意されています。
alias
モジュールのエイリアスを作ることができます。タイピング量を減らす(可読性が上がる)ことが目的です。
defmodule Gstar.Model.Unit.Schema.PlayerUnit do
alias Gstar.Model.Unit.Master.Unit
...
def assign_units(player_id, unit_ids) do
Unit.check_unit_id_exists(unit_ids)
...
別名でエイリアスしたい場合は、:as
オプションを使います。
defmodule Gstar.Model.Unit.Schema.PlayerUnit do
alias Gstar.Model.Unit.Master.Unit, as: MasterUnit
...
複数のモジュールを一度にエイリアスすることもできます。
defmodule Gstar.Model.Unit.Schema.PlayerUnit do
alias Gstar.Model.Unit.Master.{Unit, Skill}
...
import
モジュールの関数やマクロを取り込みたい場合は、import
を使います。
iex> import Enum
iex> sort([3, 2, 1, 4, 5])
[1, 2, 3, 4, 5]
:only
や:except
オプションを使うことで、必要なものだけ取り込むことができます。
iex> import Enum, only: [sort: 1]
iex> sum([1, 2, 3])
** (CompileError) iex:12: undefined function sum/1
iex> sort([3, 2, 1, 4, 5])
[1, 2, 3, 4, 5]
:functions
と:macros
という2つの特別なアトムもあります。
import Enum, only: :functions # 関数だけ取り込む
import Enum, only: :macros # マクロだけ取り込む
require
モジュールで定義したマクロを使う場合は、モジュールをrequire
します。
defmodule MyExample do
require MyMacros
MyMacros.do_stuff
end
require
を含んでいるモジュール(MyExample)をコンパイルする前に指定されたモジュール(MyMacros)のロードが行われるので、マクロ定義が有効になっていることが保証されます。
パターンマッチング
Elixirの開発においてとても大事な概念です。
パターンマッチング (Pattern matching、パターン照合) とは、データを検索する場合に、特定のパターンが出現するかどうか、またどこに> 出現するかを特定する手法のことである。by wiki
マッチ演算子
=
は変数への代入を行う演算子ではなく、マッチ演算子と呼ばれて左辺のパターンと右辺の値をマッチさせます。
左辺が変数の場合は、変数に新しい値が代入(束縛)されます。
iex(1)> [a, b, c] = [100, 200, 300]
[100, 200, 300]
iex(2)> [100, 200, 300] = [a, b, c]
[100, 200, 300]
iex(1)> {a, b, c} = {1, 2}
** (MatchError) no match of right hand side value: {1, 2}
iex(1)> {a, b, c} = [1, 2, 3]
** (MatchError) no match of right hand side value: [1, 2, 3]
ピン演算子(^)
一度束縛された変数に新たな値を束縛することができます。再束縛を防ぐにはピン演算子(pin operator)を使います。
a = 100
^a = 200
** (MatchError) no match of right hand side value: 200
アンダースコア変数
_
は無名変数と呼ばれる特別な変数です。この変数を参照することはできません。
また、アンダースコアで始まる名前の変数は「使用されない変数」という意味になります。後で参照するとコンパイル時に警告が出ます。
逆にアンダースコアで始まらない名前の変数を後で参照しないと、警告が出ます。
パターンマッチングの利用
case {:ok, "Hello, Alice!"} do
{:ok, result} -> result
{:error} -> "Error!"
_ -> "Catch all"
end
#=> "Hello, Alice!"
複数のパターンに対してマッチする必要がある場合、case/2
を使うことができます。
_
変数がないと、マッチしない場合にエラーが発生します。
defmodule Hoge do
def hello(%{name: name}) do
IO.puts "Hello, #{name}!"
end
def hello(_) do
IO.puts "Who are you?"
end
end
Hoge.hello(%{name: "Alice"})
#=> Hello, Alice!
Hoge.hello(%{})
#=> Who are you?
引数がマッチするものを特定してその関数を実行します。
再帰
Elixirではループ処理は再帰で表現をします。
例えばリストの値を合計したい場合は次のようにモジュールを定義します。
defmodule Math do
def sum([]), do: 0
def sum([head | tail]), do: head + sum(tail)
end
IO.puts Math.sum([1, 2, 3, 4, 5])
#=> 15
上記のコードは末尾再帰で最適化できます。
defmodule Math do
def sum(list), do: _sum(list, 0)
defp _sum([], accumulator), do: accumulator
defp _sum([head | tail], accumulator), do: _sum(tail, head + accumulator)
end
IO.puts Math.sum([1, 2, 3, 4, 5])
#=> 15
※この例の場合はEnum.sum/1
を使うのがベストです。
制御構造
if と unless
iex> if String.valid?("Hello"), do: "Valid string!", else: "Invalid string."
"Valid string!"
Elixirではfalse
とnil
だけは偽とみなされます。
unless
は条件が否定される時だけ作用します。
iex> unless is_integer("hello"), do: "Not an Int"
"Not an Int"
case
cond
値ではなく、条件をマッチさせたい場合は、cond
を使うことができます(Pythonでいうとelif
のようなもの)。
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
と同様にマッチしない場合にエラーが発生します。true ->
を定義することで対処できます。
範囲(Range)
1..10
のように範囲を定義します。内部では構造体として表現されます。
範囲の作成とマッチングの最も一般的なフォームはKernel
から自動インポートされる../2
マクロによってです。
iex> range = 1..10
1..10
iex> first .. last = range
1..10
iex> first
1
iex> last
10
iex> range.__struct__
Range
内包表記(Comprehensions)
列挙体(Enumerable)をループするための糖衣構文です。
ジェネレータ
iex> for n <- [1, 2, 3, 4], do: n * n
[1, 4, 9, 16]
上記の式でのn <- [1, 2, 3, 4]
がジェネレータです。リスト内包表記で使われる値を生成します。
列挙可能なものなら何でも右側の式に渡すことができます。
パターンマッチングにも対応しています。マッチしない場合、値は無視されます。
iex> values = [good: 1, good: 2, bad: 3, good: 4]
[good: 1, good: 2, bad: 3, good: 4]
iex> for {:good, n} <- values, do: n * n
[1, 4, 16]
入れ子になったループのように、複数のジェネレータを指定することができます。
iex> for n <- [1, 2], m <- [3, 4], do: n * m
[3, 4, 6, 8]
フィルタ
必要な値だけを通すのにフィルタを利用することもできます。
iex> require Integer
iex> for n <- 1..4, Integer.is_odd(n), do: n * n
[1, 9]
フィルターはnil
かfalse
以外の全ての値を通します。
内包表記の返り値
内包表記は結果としてリストを返します。:into
オプションを使うことで、リスト以外のデータ構造を生成することができます。
:into
はCollectable
プロトコルを実装している構造体を指定できます。
iex> for {k, v} <- [one: 1, two: 2, three: 3], do: {k, v}
[one: 1, two: 2, three: 3]
iex> for {k, v} <- [one: 1, two: 2, three: 3], into: %{}, do: {k, v}
%{one: 1, three: 3, two: 2}
内包表記のスコープ
内包表記の内側で代入された変数は、その内包表記の中だけで有効です。外側のスコープの変数に影響しません。
まとめ
Elixirの基礎知識はある程度網羅したつもりですが、ほかにまだまだあります。
書籍を読むなら『プログラミングElixir』は必見です。
それと、『Elixir/Phoenix 初級』シリーズというElixirとフレームワークのPhoenixを紹介する本が分かりやすいので、入門書としては良い選択だと思います。
参考サイト
https://elixirschool.com/ja/
https://elixir-lang.org/getting-started/
https://qiita.com/west-hiroaki/items/553a9d408cc25541e6aa