目的
お世話になっているソシャゲ会社様がelixirに力を入れており、実務で触る機会をいただきました。
今まで、pythonをメインとしてjava、php、c#などでも開発したことがあり、1つの言語が分かっていれば比較的簡単に応用できると思っていましたが、elixirは暗号にしか見えない状態で、持論はあっさり崩れました。
pythonの開発に戻ることになり、キレイサッパリ忘れてしまいそうなので、「pythonとの対比」でメモしておこうと思います。
登場する例はかなり極端にしてあり、実務レベルではないのでご注意ください
pythonとの違いに注意した4点
関数型言語である
pythonは オブジェクト指向型言語
ですが、elixirは 関数型言語
です。
elixirにはクラスはありませんし、もちろんインスタンスもありません。
設計レベルで頭を切り替えることを、常に意識しておくことが重要だなと思いました。
変数のスコープが狭い
配列の全要素値を合計したい場合、シンプルに考えるとこんな感じになると思います。
sum = 0
for x in [1, 2, 3, 4, 5]:
sum += x
print sum
# 15
sum = 0
for x <- [1, 2, 3, 4, 5] do
sum = sum + x
end
IO.inspect sum
# 0
同じ書き方なのに、elixirの結果はまさかのゼロ。
これは、for内が別スコープなので、sumが別変数扱いになっているためです。
elixirは、for内(do内)は関数ということなので、pythonで強引に表現するとこんな感じになるかと思います。
こう見ると当然の結果ではありますね。
def do_sum(x, y):
sum = x + y
sum = 0
for x in [1, 2, 3, 4, 5]:
do_sum(sum, x)
print sum
# 0
実務で、配列をループさせてゴニョゴニョ…はよく行うことなので、どうするべきなのか迷いました。
だがしかし、elixirには「要素を順番に抜き出して処理する」という考えがあり、それに伴う関数が充実しているので問題ありませんでした。
例えば、要素を順番に処理していく Enum.reduce/2
関数。
各要素は、第1引数の変数xに渡されてきます。
また、各要素の戻り値はアキュムレータ(第2引数の変数acc)に保存されて渡されていきます。
sum = Enum.reduce([1, 2, 3, 4, 5], fn(x, acc) -> x+acc end)
# 15
実行イメージは、こう考えると分かりやすいかなと思います。
sum = (5 + (4 + (3 + (2 + 1))))
# 15
まぁ、この例の場合は Enum.sum/1
関数がベストなわけですが!
sum = Enum.sum([1, 2, 3, 4, 5])
# 15
_ (アンダースコア) 1つで始まっている変数の意味
例えば _hoge
という変数が定義してあったとします。
pythonでは「インスタンス外からアクセスされない変数」という意味ですが、elixirは「使用されない変数」という意味になります。
class Test(object):
_hoge = "called hello"
@classmethod
def hello(cls):
print cls._hoge
print Test._hoge # pepにかけると `Access to a protected member _hoge of a class` とのwarning
defmodule Test do
# 引数の変数は使用されていないので、名前を`hoge`に変えた場合、
# コンパイルしようとすると `warning: variable "hoge" is unused` とwarningが表示されます。
def hello(_hoge) do
IO.inspect "called hello"
end
end
右辺を省略した場合のbool比較
pythonは変数値がゼロの場合もFalse
になりますが、elixirはfalseとnil以外は真なのでtrue
になります。
hoge = 0
if hoge:
print True
else:
print False
# False
hoge = 0
if hoge do
:true
else
:false
end
# true
pythonには無い考えで重要な4点
パターンマッチング
これが理解できないとelixirでの開発はできないと言ってもいいレベル。最重要です。
変数の代入もマッチング
例えば、変数への代入もマッチングが行われています。
この例だと、変数 a
に 1
をマッチングして、エラーにならなければ代入(束縛)されます。
a = 1
とは言っても、python同様、elixirの変数には型宣言が無いので、エラーになることはあり得ません。
そのため、最初は全く理解できませんでした。
もうちょっと理解しやすいように、配列を例にします。
この例だと、右辺・左辺共に配列の要素が3つなので、エラーなくマッチして代入されます。
[one, two, three] = [1, 2, 3]
IO.inspect one
# 1
IO.inspect two
# 2
IO.inspect three
# 3
しかし、左辺と右辺の要素数が異なる場合、マッチエラーになります。
[one, two] = [1, 2, 3]
** (MatchError) no match of right hand side value: [1, 2, 3]
また、パイプ記号 |
は特殊で、配列を分割してマッチします。
one
に最初の要素の値、tail
に残りの配列が割り当てられます。
[one | tail] = [1, 2, 3]
IO.inspect one
# 1
IO.inspect tail
# [2, 3]
どう使われる?
ここまでの例を見ると、「代入用の機能?」と思うかもしれませんが、実は「caseの分岐チェック」や「オーバーロードされた関数の引数」で使われる方がメイン・かつ強力です。
defmodule Test do
def hoge(arg) do
case arg do
[a, b, c] -> a + b + c
[a, b] -> a * b
end
end
end
Test.hoge([1, 2, 3]) # [a, b, c]条件にマッチ
# 6
Test.hoge([2, 4]) # [a, b]条件にマッチ
# 8
defmodule Test do
def hoge([a, b, c]) do
a + b + c
end
def hoge([a, b]) do
a * b
end
end
Test.hoge([1, 2, 3]) # [a, b, c]引数のhoge関数にマッチ
# 6
Test.hoge([2, 4]) # [a, b]引数のhoge関数にマッチ
# 8
パイプライン
パイプラインとは?
シェルのパイプと似ています。
関数の戻り値を、パイプラインで指定された関数の第1引数に渡すことができます。
どう使われる?
例えば、「配列値をソートしたうえで要素を逆順にする」場合、まずEnum.sort/1
関数を実行して、その結果をEnum.reverse/1
関数に渡します。
すると、括弧がネストして読みにくい状態になります。
Enum.reverse(Enum.sort([2, 4, 1, 3, 5]))
# [5, 4, 3, 2, 1]
パイプラインを使うとシンプルな記述になり、可読性が上がります。
[2, 4, 1, 3, 5] |> Enum.sort |> Enum.reverse
# [5, 4, 3, 2, 1]
この例だと関数が2つだけなので効果は薄いですが、実務だと5個以上の関数を使用することもあり、劇的に変わります。
自分で関数を作る場合も、第一引数を何にするのかが重要だと思いました
atom(アトム)
atomとは?
主に文字列にコロン記号 :
を前置して定義したものです。
例えば、 :hoge
。これはatomになります。
宣言不要で、記述したら即使用可能になります。
:hoge |> is_atom
# true
:hoge == :"hoge"
# true
i :hoge
# Term
# :hoge
# Data type
# Atom
# Reference modules
# Atom
# Implemented protocols
# IEx.Info, List.Chars, Inspect, String.Chars
どう使われる?
case文の条件(パターンマッチ)として使用されることが多いと思います。
defmodule Test do
def hoge(arg) do
case arg do
{:ok, value} -> "#{value} is hoge!"
{:ng, _} -> "ng!"
_ -> :err
end
end
end
{:ok, 12345} |> Test.hoge # {:ok, value}の条件にマッチする
# "12345 is hoge!"
{:ng, 12345} |> Test.hoge # {:ng, _}の条件にマッチする
# "ng!"
:err |> Test.hoge # _の条件にマッチする
# :err
補足
意識する必要はありませんが、bool値はatomです。
false |> is_atom
# true
false == :false
# true
false == :"false"
# true
:false |> is_boolean
# true
i true
# Term
# true
# Data type
# Atom
# Reference modules
# Atom
# Implemented protocols
# IEx.Info, List.Chars, Inspect, String.Chars
モジュールも、自動的にatomになりますが、これも意識する必要はありません。
defmodule Test do
def hello() do
"called hello!"
end
end
Test |> is_atom
# true
i Test
# Term
# Test
# Data type
# Atom
# Module bytecode
# []
# Source
# iex
# Version
# [59884929836438387182517456501295609178]
# Compile options
# []
# Description
# Call Test.module_info() to access metadata.
# Raw representation
# :"Elixir.Test"
# Reference modules
# Module, Atom
# Implemented protocols
# IEx.Info, List.Chars, Inspect, String.Chars
Test.hello()
# "called hello!"
:"Elixir.Test".hello()
# "called hello!"
アリティ
アリティとは?
アリティは、関数の「引数の数」です。
elixirはオーバーロード可能なので、対象の関数は名前だけでは特定できません。名前+アリティ
で特定されます。
例えば、Test.hello
関数に、引数が1つのものと2つのものがある場合、引数1つは Test.hello/1
、2つは Test.hello/2
と表現されます。
defmodule Test do
def hello(arg1) do
"この関数は Test.hello/1"
end
def hello(arg1, arg2) do
"この関数は Test.hello/2"
end
end
どう使われる?
例えば、関数を変数に代入する場合に登場します。
配列値を加算する Enum.sum/1
を代入させる場合、以下のようにします。
f = &Enum.sum/1
f.([1, 2, 3, 4, 5])
# 15
pythonと似ていて知っていると助かる2点
コマンドラインインターフェース
pythonと同様に、コマンドラインインターフェースが用意されています。
対話式でコードを試せるので、超絶助かります。
$ python
Python 2.7.10 (default, Jul 15 2017, 17:16:57)
[GCC 4.2.1 Compatible Apple LLVM 9.0.0 (clang-900.0.31)] on darwin
Type "help", "copyright", "credits" or "license" for more information.
>>>
$ iex
Erlang/OTP 20 [erts-9.2] [source] [64-bit] [smp:4:4] [ds:4:4:10] [async-threads:10] [hipe] [kernel-poll:false] [dtrace]
Interactive Elixir (1.6.0) - press Ctrl+C to exit (type h() ENTER for help)
iex(1)>
コード規約
pythonの pep8
のように、言語レベルでのコード規約 code formatter
が用意されています。
プロジェクト毎に規約を用意する必要もなく、自動で修正も行ってくれるので便利です。
$ mix format --check-formatted
$ mix format hoge.exs
基本構文の違い
基本構文は言語によって当然違うので、どのみち勉強する必要ありな部分です。
「pythonとの対比」という目的とはちょっとズレますが、備忘録としてメモさせてください。
モジュール・関数
モジュールは defmodule
、関数は def
(外部からアクセスされない関数は defp
)で宣言します。
elixirにはreturn
が無く、関数の戻り値は、関数内で最後に実行された行になります。
モジュールと関数の基本例
defmodule Test do
def hoge() do
:ok
end
end
Test.hoge() |> IO.inspect
# :ok
pythonの以下とほぼ同じです。
class Test(object):
@classmethod
def hoge(cls):
return "ok"
print Test.hoge()
# "ok"
関数の引数にデフォルト値を指定したい場合
引数を省略した場合のデフォルト値を指定する際は、\\
を使用します。
defmodule Test do
def hoge(foo, bar \\ 10) do
foo + bar
end
end
Test.hoge(5) |> IO.inspect
# 15
class Test(object):
@classmethod
def hoge(cls, foo, bar=10):
return foo + bar
print Test.hoge(5)
# 15
bool値を返す関数
elixirでは、bool値を返す関数の場合、名前の後ろに ?
(クエスチョンマーク)を付ける慣習があります。
付けないからといって、エラーになったり code formatter
のチェックに引っかかったりすることはありません。
defmodule Test do
def hoge?() do
false
end
end
Test.hoge?()
# false
エラーを返す関数
同様に、例外が発生する場合、名前の後ろに !
(エクスクラメーションマーク)を付ける慣習があります。
例えば、「連想配列からkey指定で値を取得する」 Map.fetch/2
関数には、!
ありとなしのバージョンが用意されているので例として使わせていただきます。
%{} |> Map.fetch!(:hoge)
# ** (KeyError) key :hoge not found in: %{}
# (stdlib) :maps.get(:hoge, %{})
%{} |> Map.fetch(:hoge)
# :error
for文
固定値ループ
1〜10でループさせる方法です。
out =
for x <- 1..10 do
x
end
out |> IO.inspect
# [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
# アウトプットを合わせるために配列にセットしているのでイコールではないですが...
out = []
for x in range(1, 11):
out.append(x)
print out
# [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
内包表記
pythonと違って実行速度が速いわけではないので、内包表記にするかどうは可読性重視です。
内包表記の基本形
先程の固定値ループの例を内包表記にしています。
out = for x <- 1..10, do: x
out |> IO.inspect
# [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
out = [x for x in range(1, 11)]
print out
# [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
内包表記で絞り込みを行う方法
2の倍数のみで絞り込む例です。
out = for i <- 1..10, rem(i, 2) == 0, do: i
out |> IO.inspect
# [2, 4, 6, 8, 10]
out = [i for i in range(1, 11) if i % 2 == 0]
print out
# [2, 4, 6, 8, 10]
List(配列)
宣言方法
list = [1, 2, 3, 4, 5]
配列要素をループさせる
out =
for x <- [1, 2, 3, 4, 5] do
x * 10
end
out |> IO.inspect
# [10, 20, 30, 40, 50]
out = []
for x in [1, 2, 3, 4, 5]:
out.append(x * 10)
print out
# [10, 20, 30, 40, 50]
ちなみに、同じことを内包表記でやると...
out = for i <- [1, 2, 3, 4, 5], do: i * 10
out |> IO.inspect
# [10, 20, 30, 40, 50]
out = [x * 10 for x in [1, 2, 3, 4, 5]]
print out
# [10, 20, 30, 40, 50]
index値と一緒にループさせる
for {data, index} <- Enum.with_index([1, 2, 3, 4, 5]) do
"#{index} -> #{data}" |> IO.inspect
end
# "0 -> 1"
# "1 -> 2"
# "2 -> 3"
# "3 -> 4"
# "4 -> 5"
for index, data in enumerate([1, 2, 3, 4, 5]):
print "{} -> {}".format(index, data)
# 0 -> 1
# 1 -> 2
# 2 -> 3
# 3 -> 4
# 4 -> 5
要素数を取得
length([1, 2, 3, 4, 5])
# 5
len([1, 2, 3, 4, 5])
# 5
要素の存在チェック
1 in [1, 2, 3, 4, 5]
# true
6 in [1, 2, 3, 4, 5]
# false
配列の結合
elixirの配列はLinkedListなので、大量要素を持つ配列の後ろに結合すると遅くなる点に注意です。
[5, 6, 7, 8, 9, 10] ++ [1, 2, 3, 4, 5]
# [5, 6, 7, 8, 9, 10, 1, 2, 3, 4, 5]
[5, 6, 7, 8, 9, 10] |> Enum.concat([1, 2, 3, 4, 5])
# [5, 6, 7, 8, 9, 10, 1, 2, 3, 4, 5]
[5, 6, 7, 8, 9, 10] + [1, 2, 3, 4, 5]
# [5, 6, 7, 8, 9, 10, 1, 2, 3, 4, 5]
その他の便利関数
既に例で使っていますが、Enumモジュールが非常に便利です。
配列操作に必要なことはほぼ網羅されています。
https://hexdocs.pm/elixir/Enum.html
Map(連想配列)
pythonでいうところのdictです。
宣言と値の取得方法
elixirでは、keyを文字列で設定する場合と、atomで設定する場合で書式が異なります。
ただ、実務上、文字列で設定することはなさそうだったので省略します。
以下の例のkeyの hoge:
と hello:
は、コロンが後ろにありますがatomです。
map = %{
hoge: "hogehoge",
hello: "world"
}
# %{hello: "world", hoge: "hogehoge"}
map.hoge |> IO.inspect
# "hogehoge"
map.hello |> IO.inspect
# "world"
map = {
'hoge': 'hogehoge',
'hello': 'world'
}
# {'hello': 'world', 'hoge': 'hogehoge'}
print map['hoge']
# hogehoge
print map['hello']
# world
要素を追加・更新する
指定したkeyが存在していれば値を更新、存在していなければ要素を追加します。
map = %{
hoge: "hogehoge",
hello: "world"
}
# %{hello: "world", hoge: "hogehoge"}
# 更新
map = Map.put(map, :hoge, "hoge2")
# %{hello: "world", hoge: "hoge2"}
# 追加
map = Map.put(map, :foo, "bar")
# %{foo: "bar", hello: "world", hoge: "hoge2"}
map = {
'hoge': 'hogehoge',
'hello': 'world'
}
# {'hello': 'world', 'hoge': 'hogehoge'}
# 更新
map.update({
'hoge': 'hoge2'
})
# {'hello': 'world', 'hoge': 'hoge2'}
# 追加
map.update({
'foo': 'bar'
})
# {'foo': 'bar', 'hello': 'world', 'hoge': 'hoge2'}
その他の便利関数
配列のEnumモジュール同様、Mapモジュールがあります。
https://hexdocs.pm/elixir/Map.html
tuple(タプル)
pythonと記号が異なるだけで、全く同じです。
{a, b} = {1, 2}
a |> IO.inspect
# 1
b |> IO.inspect
# 2
(a, b) = (1, 2)
print a
# 1
print b
# 2
キーワードリスト
これはpythonには無い考えですが、elixirでは必須なのでメモしておきます。
キーワードリストとは?
配列なのに、Mapと同様、key、value形式になっています。
kwargs = [a: 1, b: 2, c: 3]
# [a: 1, b: 2, c: 3]
ただ、Mapと異なり、keyの重複が可能です。
kwargs = [a: 1, b: 2, c: 3, c: 5]
# [a: 1, b: 2, c: 3, c: 5]
内部的には、{key、value} の2要素のタプルを配列で管理しています。
つまり、 [a: 1, b: 2]
と [{:a, 1}, {:b, 2}]
は同意です。
[a: 1, b: 2, c: 3] === [{:a, 1}, {:b, 2}, {:c, 3}]
# true
宣言と値の取得方法
kwargs = [a: 1, b: 2]
# [a: 1, b: 2]
kwargs[:b]
# 2
要素の追加方法
kwargs = [a: 1, b: 2]
kwargs = [c: 3] ++ kwargs
# [c: 3, a: 1, b: 2]
要素を切り出す方法
要素を抜き出しつつ、抜き出した要素を削除したキーワードリストも返します。
kwargs = [hello: "world", hoge: "hogehoge", foo: "bar"]
# [hello: "world", hoge: "hogehoge", foo: "bar"]
{hoge, kwargs} = Keyword.pop(kwargs, :hoge, [])
hoge |> IO.inspect
# "world"
kwargs |> IO.inspect
# [hello: "world", foo: "bar"]
要素を削除する方法
kwargs = [a: 1, b: 2]
# [a: 1, b: 2]
kwargs = Keyword.delete(kwargs, :b)
# [a: 1]
その他の便利関数
Keywordモジュールを参照してください。
https://hexdocs.pm/elixir/Keyword.html
構造体
pythonには存在しない考えです。
クラスの雰囲気で作れますが、当然ながらクラスではありません。
データ構造を保証する仕組みです。
# 構造体の定義
defmodule Player do
defstruct [name: "default", level: 1]
end
player = %Player{name: "Hoge", level: 10}
# %Player{level: 10, name: "Hoge"}
player = %Player{name: "hello"}
# %Player{level: 1, name: "hello"}
player.__struct__
Player
実はMapですので、Mapを操作する関数は全て使用可能です。
%Player{} |> is_map
# true
処理分岐
cond do文
分岐の条件に複数の変数や複数の条件パターンが絡む場合は、cond do
を使用します。
この分岐はpythonには存在しません。
上の条件から順にマッチングさせ、マッチングした条件の処理が実行されます。
もし、全ての条件にマッチしない場合、CondClauseError
がraiseされますので、それが問題であれば true
を条件に入れておく必要があります。
x = false
y = true
type =
cond do
x == true -> :select_x
y == true -> :select_y
true -> nil # 何もマッチしないと CondClauseError になるので用意
end
type |> IO.inspect
# :select_y
case文
1つの変数に対するパターンマッチで分岐させたい場合は、case
を使用します。
こちらもpythonには存在しません。
cond do
同様、全ての条件にマッチしない場合、CondClauseError
がraiseされます。
result = {:ok, "Hello, World"}
message =
case result do
{:ok, msg} -> msg
{:error, msg} -> "error!: #{msg}"
_ -> :nil # 何もマッチしないと CondClauseError になるので用意
end
message |> IO.inspect
# "Hello, World"
if文
caseでのパターンマッチが強力なため、実務では、あまりif文を使う機会は無い気がします。
elixirには三項演算子が存在しないため、三項演算子の代わりに使用することが多そうです。
age = 20
course = if age >= 20, do: :adult, else: :young
course |> IO.inspect
# :adult
age = 20
course = "adult" if age >= 20 else "young"
print course
# "adult"
unless文
if文の否定型(pythonでいうif not
)を使いたい場合、elixirでは unless
を使用します。
age = 20
course = unless age < 20, do: :adult, else: :young
course |> IO.inspect
# :adult
age = 20
course = "adult" if not age < 20 else "young"
print course
# "adult"
定数
@
を使用します。
defmodule Test do
@name "testだよ!"
def get_name() do
@name
end
end
Test.get_name() |> IO.inspect
# "testだよ!"
class Test(object):
NAME = 'testだよ!'
@classmethod
def get_name(cls):
return cls.NAME
print Test.get_name()
# 'testだよ!'
まとめ
以上で、基礎的な開発が可能になる知識が身に付いた気がします。
ですが、ベース部分を作成するとなると、更に以下のようなノウハウが必要になるかと思います。
- mix
Introduction to Mix - フレームワークPhoenix(フェニックス)
Phoenix Framework - DB操作を行うEcto
elixir - Ecto docs - useやrequireを使用した、macroの知識
elixir - macros
それぞれで1記事が必要なレベル量になってしまうので、需要がありそうだったら別途まとめようかと思います。