cv.cm

コーディング · プログラミング言語

Elixir入門:初心者向けチュートリアル

約11分で読めます

結論

Elixirは、Erlang仮想マシン(BEAM)上で動く動的な関数型言語で、スケーラブルで耐障害性があり並行処理に強いシステムのために設計されています。中心となる考え方は、イミュータブルなデータ、パターンマッチ、パイプ演算子、メッセージで通信する軽量プロセスです。このページでは、環境構築、型、パターンマッチ、制御構文、モジュールと関数、コレクション、プロセス、エラー処理を扱います。

正規表現テストを開く

Elixirとは何か、何に使うのか

Elixirは、José Valimが作り、2012年に最初にリリースされた動的な関数型言語です。Erlangの仮想マシンであるBEAM上で動くため、Erlangの軽量プロセス、耐障害性、分散システムへの対応を引き継ぎつつ、扱いやすい構文、マクロ、現代的なツールを加えています。

Elixirは、PhoenixフレームワークによるWebアプリケーションやAPI、チャットやライブダッシュボードのようなリアルタイム機能(Phoenix LiveView)、メッセージングシステム、データパイプライン、Nervesによる組み込み機器、Nxによる機械学習に使われています。多数の同時接続を処理し、一部が故障しても動き続けなければならないサービスに、多くの企業が採用しています。

Elixirのインストールとhello, worldの実行

Elixirはelixir-lang.orgからインストールします(Erlang/OTPも同時にインストールされるか、必要になります)。スクリプトの拡張子は.exsで、コンパイルされるプロジェクトのファイルは.exです。iexで対話シェルが起動します。

# hello.exs
IO.puts("Hello, world!")
elixir hello.exs
# Hello, world!
iex                      # interactive shell; type h(Enum.map) for help
mix new my_app           # create a new project with Mix, the build tool

コメントは#で始まります。関数の引数を囲むかっこは多くの場面で省略できるので、IO.puts "hi"も動きます。

基本の型とイミュータビリティ

Elixirのデータはすべてイミュータブル(変更不可)です。=で名前を束縛し直すと新しい値を指すだけで、古い値は変わりません。文字列は二重引用符で書くUTF-8のバイナリで、アトムはコロンで始まる名前付き定数です。

name = "Elixir"
year = 2012
IO.puts("#{name} since #{year}")    # Elixir since 2012
IO.puts(10 / 4)                     # 2.5 (always a float)
IO.puts(div(10, 4))                 # 2
IO.puts(rem(10, 4))                 # 2
IO.puts("ab" <> "cd")               # abcd (string concatenation)
IO.puts(String.length("héllo"))     # 5 (counts characters, not bytes)
IO.inspect(:ok)                     # :ok
IO.inspect(is_atom(true))           # true (booleans are atoms)
型例補足
integer42, 1_000_000任意精度
float3.1464ビット倍精度
atom:ok, :error定数。true、false、nilもアトムです
string"hello"UTF-8のバイナリ。#{}で式を埋め込めます
list[1, 2, 3]連結リスト
tuple{:ok, 1}固定サイズで、メモリ上に連続して置かれます
map%{"a" => 1}キーと値のストア

パターンマッチ

Elixirでは、=はマッチ演算子です。左辺がパターンで、右辺がそれに合えば、パターン中の変数が束縛されます。パターンマッチは、代入、case、関数の頭部、エラー処理など、あらゆる場面で使われます。

{:ok, value} = {:ok, 42}
IO.puts(value)                         # 42

[first | rest] = [1, 2, 3]
IO.inspect({first, rest})              # {1, [2, 3]}

%{name: n} = %{name: "Ann", age: 31}
IO.puts(n)                             # Ann

result = {:error, :not_found}
message =
  case result do
    {:ok, v} -> "got #{v}"
    {:error, reason} -> "failed: #{reason}"
  end
IO.puts(message)                       # failed: not_found

x = 1
^x = 1                                 # pin operator: match the current value

パターンが合わないと、ElixirはMatchErrorを発生させます。ピン演算子^は、変数を束縛し直さず「この変数の既存の値を使う」ことを意味します。

制御構文

Elixirにはcase、cond、if/unlessがあり、どれも値を返します。カウンターを書き換えるfor/whileループはなく、代わりに再帰、Enumモジュール、for内包表記を使います。

temp = 22
feel =
  cond do
    temp > 30 -> "hot"
    temp >= 15 -> "mild"
    true -> "cold"
  end
IO.puts(feel)                                   # mild

IO.puts(if(rem(temp, 2) == 0, do: "even", else: "odd"))  # even

squares = for n <- 1..5, rem(n, 2) == 1, do: n * n
IO.inspect(squares)                             # [1, 9, 25]

偽になるのはfalseとnilだけで、0や""を含めそれ以外はすべて真です。

関数・モジュール・パイプ演算子

名前付き関数は、defmoduleで定義するモジュールの中に置きます。関数は複数の節を持て、パターンマッチとガード(when)で選ばれます。無名関数はfn ... endと書き、ドットを付けて呼び出します。パイプ演算子|>は、ある式の結果を、次の呼び出しの第1引数として渡します。

defmodule Math do
  def factorial(0), do: 1
  def factorial(n) when n > 0, do: n * factorial(n - 1)

  def sign(n) when n < 0, do: :negative
  def sign(0), do: :zero
  def sign(_n), do: :positive
end

IO.puts(Math.factorial(5))          # 120
IO.inspect(Math.sign(-3))           # :negative

double = fn x -> x * 2 end
IO.puts(double.(21))                # 42 (note the dot)

"  Elixir is fun  "
|> String.trim()
|> String.upcase()
|> String.split()
|> IO.inspect()                     # ["ELIXIR", "IS", "FUN"]

関数は名前とアリティ(引数の数)で識別され、ドキュメントではString.split/1やMath.factorial/1のように書かれます。

コレクション:リスト・マップ・キーワードリスト・Enum

Enumモジュールは、あらゆる列挙可能なコレクションを扱い、map、filter、reduce、sort_by、group_byなどを提供します。マップは主要なキーと値の構造で、[size: 10, color: :red]のようなキーワードリストは、オプションの指定によく使われます。

nums = [5, 3, 8, 1]
IO.inspect(Enum.sort(nums))                      # [1, 3, 5, 8]
IO.inspect(Enum.map(nums, &(&1 * 10)))           # [50, 30, 80, 10]
IO.inspect(Enum.filter(nums, fn n -> n > 2 end)) # [5, 3, 8]
IO.puts(Enum.reduce(nums, 0, &+/2))              # 17

user = %{name: "Ann", age: 31}
older = %{user | age: 32}                        # update an existing key
IO.puts(user.age)                                # 31 (unchanged)
IO.puts(older.age)                               # 32
with_city = Map.put(user, :city, "Oslo")
IO.puts(with_city.city)                          # Oslo
IO.inspect(Map.get(user, :email, "none"))        # "none"

キャプチャ演算子&で、短い無名関数を作れます。&(&1 * 10)はfn x -> x * 10 endと同じ意味で、&+/2は引数2つの+関数を指します。

プロセスと並行処理

Elixirを代表する機能がBEAMプロセスです。独自のメモリを持つ、非常に軽量で分離された実行単位です。プロセス同士はメッセージの送信だけで通信し、1台のマシンで数十万のプロセスを動かせます。スーパーバイザーはクラッシュしたプロセスを再起動します。これがElixirの耐障害性の考え方「let it crash」の土台です。

parent = self()

spawn(fn ->
  send(parent, {:result, 6 * 7})
end)

receive do
  {:result, value} -> IO.puts("Got #{value}")   # Got 42
after
  1_000 -> IO.puts("timeout")
end

tasks = for n <- 1..3, do: Task.async(fn -> n * n end)
IO.inspect(Task.await_many(tasks))               # [1, 4, 9]

実際のアプリケーションでspawnを直接呼ぶことはまれです。Task、GenServer、Agent、SupervisorといったOTPの抽象化を使います。

エラー処理

Elixirらしい書き方では、想定内の失敗に{:ok, value}や{:error, reason}のようなタグ付きタプルを返し、caseやwithで処理します。File.read!/1のように名前が!で終わる関数は、代わりに例外を発生させます。例外はtry/rescueで捕捉できますが、あまり使われません。

case File.read("missing.txt") do
  {:ok, text} -> IO.puts(text)
  {:error, reason} -> IO.puts("Cannot read: #{reason}")   # Cannot read: enoent
end

with {a, ""} <- Integer.parse("40"),
     {b, ""} <- Integer.parse("2") do
  IO.puts(a + b)                                         # 42
else
  _ -> IO.puts("not a whole number")
end

try do
  String.to_integer("abc")
rescue
  e in ArgumentError -> IO.puts("Error: #{Exception.message(e)}")
end

withは複数のマッチをつなげ、最初にマッチしなかった時点で止めて、その値を返します。深くネストしたcase式を避けられます。

小さな完成プログラム:単語の出現頻度

このスクリプトは、テキスト中の単語を数え、よく出る上位3語を表示します。StringとEnumの関数で組み立てた、典型的なElixirのパイプラインです。

defmodule WordFreq do
  def top(text, n) do
    text
    |> String.downcase()
    |> String.split(~r/[^a-z']+/, trim: true)
    |> Enum.frequencies()
    |> Enum.sort_by(fn {word, count} -> {-count, word} end)
    |> Enum.take(n)
  end
end

"The cat and the dog. The dog sat, and the cat ran."
|> WordFreq.top(3)
|> Enum.each(fn {word, count} -> IO.puts("#{word}: #{count}") end)
# the: 4
# and: 2
# cat: 2

次に学ぶこと

  • hexdocs.pmとelixir-lang.orgにある公式のGetting Startedガイド。
  • HexDocs。Elixirと、すべてのHexパッケージのドキュメントが公開されています。
  • Web開発のためのPhoenixフレームワークのガイド。
  • コミュニティによる無料のカリキュラムElixir Schoolと、練習用のExercismのElixirトラック。

よくある質問

ElixirとErlangの違いは何ですか?

どちらも同じBEAM仮想マシン上で動き、互いのコードを呼び出せます。Elixirは、Rubyに着想を得た構文、マクロ、パイプ演算子、MixやHexといったツールを加えたもので、ErlangはOTPが作られた、より古い言語です。

ElixirはWeb開発に向いていますか?

はい。Phoenixフレームワークは高速で多数の同時接続を処理でき、LiveViewを使うと、サーバー側でレンダリングしたHTMLと少ないJavaScriptで、インタラクティブなページを作れます。

Elixirは静的型付けですか?

Elixirは動的型付けです。最近のバージョンでは、集合論に基づく段階的型システムがコンパイラに加えられつつあり、typespecとDialyzerで型エラーを見つけるチームも多くあります。

Elixirの|>は何を意味しますか?

|>はパイプ演算子です。x |> f(y)はf(x, y)と同じで、変換の連なりを上から下へ読めます。

.exと.exsファイルとは何ですか?

.exファイルはプロジェクトの一部としてコンパイルされ、.exsファイルは実行時に解釈されるスクリプトで、テストや設定によく使われます。

コーディング講座の一覧