pythonエンジニアの自分がelixirを(ちょっとだけ)開発できるようになった時の勉強メモ

目的

お世話になっているソシャゲ会社様がelixirに力を入れており、実務で触る機会をいただきました。

今まで、pythonをメインとしてjava、php、c#などでも開発したことがあり、1つの言語が分かっていれば比較的簡単に応用できると思っていましたが、elixirは暗号にしか見えない状態で、持論はあっさり崩れました。

pythonの開発に戻ることになり、キレイサッパリ忘れてしまいそうなので、「pythonとの対比」でメモしておこうと思います。

登場する例はかなり極端にしてあり、実務レベルではないのでご注意ください:sob:

pythonとの違いに注意した4点

関数型言語である

pythonは オブジェクト指向型言語 ですが、elixirは 関数型言語 です。
elixirにはクラスはありませんし、もちろんインスタンスもありません。

設計レベルで頭を切り替えることを、常に意識しておくことが重要だなと思いました。

変数のスコープが狭い

配列の全要素値を合計したい場合、シンプルに考えるとこんな感じになると思います。

pythonの場合
sum = 0

for x in [1, 2, 3, 4, 5]:
    sum += x

print sum
# 15
elixirの場合
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で強引に表現するとこんな感じになるかと思います。
こう見ると当然の結果ではありますね。

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)に保存されて渡されていきます。

Enum.reduce/1を使った例
sum = Enum.reduce([1, 2, 3, 4, 5], fn(x, acc) -> x+acc end)
# 15

実行イメージは、こう考えると分かりやすいかなと思います。

Enum.reduceの実行イメージ
sum = (5 + (4 + (3 + (2 + 1))))
# 15

まぁ、この例の場合は Enum.sum/1 関数がベストなわけですが!

Enum.sum/1を使った例
sum = Enum.sum([1, 2, 3, 4, 5])
# 15

_ (アンダースコア) 1つで始まっている変数の意味

例えば _hoge という変数が定義してあったとします。
pythonでは「インスタンス外からアクセスされない変数」という意味ですが、elixirは「使用されない変数」という意味になります。

pythonの場合
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
elixirの場合
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になります。

pythonの場合
hoge = 0

if hoge:
    print True
else:
    print False

# False
elixirの場合
hoge = 0

if hoge do
  :true
else
  :false
end

# true

pythonには無い考えで重要な4点

パターンマッチング

これが理解できないとelixirでの開発はできないと言ってもいいレベル。最重要です。

変数の代入もマッチング

例えば、変数への代入もマッチングが行われています。
この例だと、変数 a1 をマッチングして、エラーにならなければ代入(束縛)されます。

変数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の分岐チェック」や「オーバーロードされた関数の引数」で使われる方がメイン・かつ強力です。

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になります。
宣言不要で、記述したら即使用可能になります。

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文の条件(パターンマッチ)として使用されることが多いと思います。

atomを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です。

boolean値が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になりますが、これも意識する必要はありません。

モジュールが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
関数をatomで実行
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

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.
>>>
elixirのコマンドライン
$ 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
コード規約に沿ってhoge.exsを自動修正
$ mix format hoge.exs

基本構文の違い

基本構文は言語によって当然違うので、どのみち勉強する必要ありな部分です。
「pythonとの対比」という目的とはちょっとズレますが、備忘録としてメモさせてください。

モジュール・関数

モジュールは defmodule、関数は def(外部からアクセスされない関数は defp)で宣言します。

elixirにはreturnが無く、関数の戻り値は、関数内で最後に実行された行になります。

モジュールと関数の基本例

elixir例
defmodule Test do
  def hoge() do
    :ok
  end
end

Test.hoge() |> IO.inspect
# :ok

pythonの以下とほぼ同じです。

python例
class Test(object):
    @classmethod
    def hoge(cls):
        return "ok"

print Test.hoge()
# "ok"

関数の引数にデフォルト値を指定したい場合

引数を省略した場合のデフォルト値を指定する際は、\\ を使用します。

elixir例
defmodule Test do
  def hoge(foo, bar \\ 10) do
    foo + bar
  end
end

Test.hoge(5) |> IO.inspect
# 15
python例
class Test(object):
    @classmethod
    def hoge(cls, foo, bar=10):
        return foo + bar

print Test.hoge(5)
# 15

bool値を返す関数

elixirでは、bool値を返す関数の場合、名前の後ろに ?(クエスチョンマーク)を付ける慣習があります。
付けないからといって、エラーになったり code formatter のチェックに引っかかったりすることはありません。

elixirでbool値を返す関数例
defmodule Test do
  def hoge?() do
    false
  end
end

Test.hoge?()
# false

エラーを返す関数

同様に、例外が発生する場合、名前の後ろに !(エクスクラメーションマーク)を付ける慣習があります。
例えば、「連想配列からkey指定で値を取得する」 Map.fetch/2 関数には、! ありとなしのバージョンが用意されているので例として使わせていただきます。

!ありバージョン。keyが存在しない場合はKeyErrorがraiseされる
%{} |> Map.fetch!(:hoge)
# ** (KeyError) key :hoge not found in: %{}
#     (stdlib) :maps.get(:hoge, %{})
!なしバージョン。keyが存在しない場合はatomを返す
%{} |> Map.fetch(:hoge)
# :error

for文

固定値ループ

1〜10でループさせる方法です。

elixir例
out =
  for x <- 1..10 do
    x
  end

out |> IO.inspect
# [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
python例
# アウトプットを合わせるために配列にセットしているのでイコールではないですが...

out = []
for x in range(1, 11):
    out.append(x)

print out
# [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]

内包表記

pythonと違って実行速度が速いわけではないので、内包表記にするかどうは可読性重視です。

内包表記の基本形

先程の固定値ループの例を内包表記にしています。

elixirで固定値ループを内包表記
out = for x <- 1..10, do: x

out |> IO.inspect
# [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
python例
out = [x for x in range(1, 11)]

print out
# [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]

内包表記で絞り込みを行う方法

2の倍数のみで絞り込む例です。

elixir例
out = for i <- 1..10, rem(i, 2) == 0, do: i

out |> IO.inspect
# [2, 4, 6, 8, 10]
python例
out = [i for i in range(1, 11) if i % 2 == 0]

print out
# [2, 4, 6, 8, 10]

List(配列)

宣言方法

elixir、python共同じ
list = [1, 2, 3, 4, 5]

配列要素をループさせる

elixir例
out =
  for x <- [1, 2, 3, 4, 5] do
    x * 10
  end

out |> IO.inspect
# [10, 20, 30, 40, 50]
python例
out = []

for x in [1, 2, 3, 4, 5]:
    out.append(x * 10)

print out
# [10, 20, 30, 40, 50]

ちなみに、同じことを内包表記でやると...

elixir例
out = for i <- [1, 2, 3, 4, 5], do: i * 10

out |> IO.inspect
# [10, 20, 30, 40, 50]
python例
out = [x * 10 for x in [1, 2, 3, 4, 5]]

print out
# [10, 20, 30, 40, 50]

index値と一緒にループさせる

elixir例
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"
python例
for index, data in enumerate([1, 2, 3, 4, 5]):
    print "{} -> {}".format(index, data)

# 0 -> 1
# 1 -> 2
# 2 -> 3
# 3 -> 4
# 4 -> 5

要素数を取得

elixir例
length([1, 2, 3, 4, 5])
# 5
python例
len([1, 2, 3, 4, 5])
# 5

要素の存在チェック

elixir、python共同じ
1 in [1, 2, 3, 4, 5]
# true

6 in [1, 2, 3, 4, 5]
# false

配列の結合

elixirの配列はLinkedListなので、大量要素を持つ配列の後ろに結合すると遅くなる点に注意です。

elixir例
[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]
python例
[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です。

elixir例
map = %{
  hoge: "hogehoge",
  hello: "world"
}
# %{hello: "world", hoge: "hogehoge"}

map.hoge |> IO.inspect
# "hogehoge"

map.hello |> IO.inspect
# "world"
python例
map = {
    'hoge': 'hogehoge',
    'hello': 'world'
}
# {'hello': 'world', 'hoge': 'hogehoge'}

print map['hoge']
# hogehoge

print map['hello']
# world

要素を追加・更新する

指定したkeyが存在していれば値を更新、存在していなければ要素を追加します。

elixir例
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"}
python例
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と記号が異なるだけで、全く同じです。

elixir例
{a, b} = {1, 2}

a |> IO.inspect
# 1

b |> IO.inspect
# 2
python例
(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を操作する関数は全て使用可能です。

構造体がMapであることを確認
%Player{} |> is_map
# true

処理分岐

cond do文

分岐の条件に複数の変数複数の条件パターンが絡む場合は、cond do を使用します。
この分岐はpythonには存在しません。

上の条件から順にマッチングさせ、マッチングした条件の処理が実行されます。
もし、全ての条件にマッチしない場合、CondClauseErrorがraiseされますので、それが問題であれば true を条件に入れておく必要があります。

elixirのcond_do例
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されます。

elixirのcase例
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には三項演算子が存在しないため、三項演算子の代わりに使用することが多そうです。

elixir例
age = 20

course = if age >= 20, do: :adult, else: :young

course |> IO.inspect
# :adult
python例
age = 20

course = "adult" if age >= 20 else "young"

print course
# "adult"

unless文

if文の否定型(pythonでいうif not)を使いたい場合、elixirでは unless を使用します。

elixir例
age = 20

course = unless age < 20, do: :adult, else: :young

course |> IO.inspect
# :adult
python例
age = 20

course = "adult" if not age < 20 else "young"

print course
# "adult"

定数

@ を使用します。

elixir例
defmodule Test do
  @name "testだよ!"

  def get_name() do
    @name
  end
end

Test.get_name() |> IO.inspect
# "testだよ!"
python例(厳密に定数なのではなく慣例ですが!)
class Test(object):
    NAME = 'testだよ!'

    @classmethod
    def get_name(cls):
        return cls.NAME

print Test.get_name()
# 'testだよ!'

まとめ

以上で、基礎的な開発が可能になる知識が身に付いた気がします。
ですが、ベース部分を作成するとなると、更に以下のようなノウハウが必要になるかと思います。

それぞれで1記事が必要なレベル量になってしまうので、需要がありそうだったら別途まとめようかと思います。

参考サイト

Elixir School 日本語訳

Sign up for free and join this conversation.
Sign Up
If you already have a Qiita account log in.