9
6

More than 5 years have passed since last update.

【初心者向け】普段Pythonを書いている人がElixir基礎の中の基礎知識をまとめる

Last updated at Posted at 2018-08-06

はじめに

はじめてElixirを(3ヵ月だけ)触った時の勉強メモを書いています。初心者向けの内容です。
普段は主にPythonを使っているのでPythonと比較したりします。

Elixirとは

Elixirは関数型言語に分類されるプログラミング言語で、Erlangの仮想マシン(BEAM)上で動作します。
Wikipedia

基本データ型

Elixirでは値が不変(immutable)ですが、変数は不変ではありません。

真理値

真理値として小文字で始まるtruefalseがあります。
falsenil以外は真とみなされるので、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(それ以降の要素)に分ける
    • 順番にアクセスするため、添字での参照やサイズの取得は遅い
    • 先頭に要素を追加するのは速い
  • 用途
    • 汎用的なデータ型

リストの関数

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: 2IO.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/0Hello.greet/1は別物です。
※プライベート関数はdefpで定義します。

無名関数

文字列やアトムなどと同様に「値」の一種です。次のように無名関数を定義して変数fにセットします。

無名関数
f = fn (a, b) -> a + b end
f.(2, 3)
#=> 5

パイプライン演算子(|>)

左辺の関数の戻り値を右辺の関数の第一引数に渡すことができます。
例えば、Enum.sort/1Enum.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

if
iex> if String.valid?("Hello"), do: "Valid string!", else: "Invalid string."
"Valid string!"

Elixirではfalsenilだけは偽とみなされます。
unlessは条件が否定される時だけ作用します。

unless
iex> unless is_integer("hello"), do: "Not an Int"
"Not an Int"

case

パターンマッチングの利用を参照

cond

値ではなく、条件をマッチさせたい場合は、condを使うことができます(Pythonでいうとelifのようなもの)。

cond
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]

フィルターはnilfalse以外の全ての値を通します。

内包表記の返り値

内包表記は結果としてリストを返します。:intoオプションを使うことで、リスト以外のデータ構造を生成することができます。
:intoCollectableプロトコルを実装している構造体を指定できます。

オプションの使用
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

9
6
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
9
6