← melang

Elixir

melangの各noteで他言語と比較した、Elixirの設計をまとめたページ。ここに書かれている内容は、各noteの比較表から自動で集めたものです。

型

再代入ではなくパターンマッチによる束縛が基本。

age = 20
age = 20
age = "Alice" # 再代入ではなく新しい束縛(変数自体はイミュータブル)

case age do
  age when is_integer(age) -> age + 10
  _ -> :error
end

`=`は代入ではなくパターンマッチによる束縛で、値そのものはイミュータブル。型注釈はオプションのtypespec(`@spec`)で後付けでき、Dialyzerという静的解析ツールで検査できるが、実行時の型検査自体は動的型付け。

型注釈の位置

型はdefの直前に`@spec`として別行に書く。Haskellの分離型シグネチャに近い発想。

@spec greet(String.t()) :: String.t()
age = 20 # 変数自体に型注釈はない

@spec greet(String.t()) :: String.t() # 関数定義の直前に別行で型を書く
def greet(name), do: "Hello, " <> name

変数には型注釈がないが、関数には`@spec`という属性を`def`の直前に別行で書ける。構文上はHaskellの`name :: Type`という独立したシグネチャ行に近い発想だが、コンパイラソースコードを解析し、実行可能な形式(機械語や中間表現)に変換するプログラム。静的型付け言語では、実行前にコンパイラが型の不整合などをチェックする役割も担う。自体は検査せずDialyzerという外部の静的解析ツールが検査を担う点が異なる。

型変換

変換を関数で書き、パースは結果をタプルか:errorで返す。

Integer.to_string(x)
Integer.to_string(42)   # "42"
trunc(3.9)               # 3

Integer.parse("42")      # {42, ""}
Integer.parse("abc")     # :error

String.to_integer("abc") # ArgumentError

`Integer.parse/1`は成功時に`{値, 残りの文字列}`、失敗時に`:error`を返し、パターンマッチで扱える。一方`String.to_integer/1`は失敗時に例外を投げる。Elixirでは`{:ok, value}`のようなタプルで結果を表す慣習があり、melangの`Result`に近い考え方。

暗黙の型変換

integer/float間は暗黙変換されるが、他の型とは変換されない。

x = 10
x = 10
y = 1.5
x + y   # 11.5(integerがfloatへ暗黙変換される)
x + "5" # ArithmeticError(数値と文字列の暗黙変換はない)

integerとfloatを混在させる算術演算では暗黙に型が揃えられるが、それ以外の型(文字列など)との暗黙変換は行われない。パターンマッチやガード節で型を明示的に扱うスタイルが基本。

Enum / Sum Type

専用のenum型はなく、tagged tupleとpattern matchingで表現する。

{:success, user} | {:failure, message}
def describe({:success, user}), do: "ok: #{user}"
def describe({:failure, message}), do: "error: #{message}"
# 網羅しないケースがあると実行時にFunctionClauseErrorになる

専用のenum型はなく、atomとtagged tupleによるpattern matchingでADTAlgebraic Data Type(代数的データ型)の略。複数の型を組み合わせて新しい型を作る手法で、「AまたはB」のように取りうる形が閉じた集合になっているものをSum Type(直和型)と呼ぶ。Rustの`enum`やHaskellの`data`が代表例で、各variant(構成要素)が異なるフィールドを持てる点で、単純な整数の列挙とは異なる。的に表現する。網羅性はコンパイラソースコードを解析し、実行可能な形式(機械語や中間表現)に変換するプログラム。静的型付け言語では、実行前にコンパイラが型の不整合などをチェックする役割も担う。ではなく、実行時のマッチ失敗(`FunctionClauseError`)として現れる。

値の不変性

変数は同じ名前へ束縛し直せるが、データは変更されない。

x = ... / %{map | key: value}
user = %{name: "alice", tags: ["a"]}
other = user
user = %{user | name: "bob"}   # 新しいmapを作り、名前をrebindingする
# other は %{name: "alice", ...} のまま変わらない

tags = user.tags ++ ["admin"]  # 新しいlist

# 状態は、再帰で新しい状態へ遷移して持つ
def loop(count) do
  receive do
    :inc -> loop(count + 1)
  end
end

`x = 1`のあとに`x = 2`と書けるが、これは同じ名前を別の値へ束縛し直すだけで、元のデータは変わらない。データはすべて不変で、更新は新しい値を作って表す。プロセスの状態は、再帰で新しい状態に遷移させる。melangは、rebindingも再代入として禁止する点が異なる。

名前付き引数

言語機能としての名前付き引数はなく、最後の引数をキーワードリストとして受け取る慣習で代替する。

def foo(opts \\ [])
def foo(opts \\ []) do
  width = Keyword.get(opts, :width, 0)
  height = Keyword.get(opts, :height, 0)
end

foo(width: 10, height: 20) # 最後の引数のキーワードリストとして渡る

`width: 10, height: 20`という書き方はキーワードリストの糖衣構文であり、関数側で個別の引数名にマッピングされるわけではない。呼び出し側の見た目は名前付き引数に近いが、実体は「最後の引数として1つのリストを受け取る」設計であり、`width`・`height`という名前をコンパイラソースコードを解析し、実行可能な形式(機械語や中間表現)に変換するプログラム。静的型付け言語では、実行前にコンパイラが型の不整合などをチェックする役割も担う。が検証するわけではない。

関数の引数

値がすべて不変。共有かコピーかは処理系が決める。

def f(list)
def f(list), do: [1 | list]  # 元のlistは変わらない

xs = [2, 3]
ys = f(xs)
# xs == [2, 3]
# ys == [1, 2, 3]

値がすべて不変で、変数の再束縛も呼び出し側には影響しない。同一プロセス内では値が共有され、プロセス間では値がコピーされる、といった扱いは処理系が決める。Haskellと同じく、melangの「変更可能なデータが存在しない」前提に近い。

if 文/式

`if`は式(マクロ)。`else`を省略すると`nil`を返す。分岐の主役は`case` / `cond` / 関数節。

if cond do a else b end
x = if cond, do: 1, else: 2

# elseを省略するとnilになる
y = if cond, do: 1   # cond が偽なら nil

`if`は式で、`else`を省略すると`nil`が返る。`else`忘れが`nil`という値として現れるため、気づきにくい場合がある。多分岐にはパターンマッチを使う`case`や`cond`、関数の節が主に使われる。

I/O の明示

動的型付けで、I/Oを示す仕組みもない。

def find_user(id)
def find_user(id) do
  Repo.get(User, id)
end

def get_user(id) do
  find_user(id) # 呼べる
end

`@spec`で型を後付けしDialyzerで検査できるが、effectを表す語彙は用意されていない。並行処理はプロセス間メッセージパッシングで行うが、これも通常の関数呼び出しと同じ構文で書ける。

言語名

「不老不死の霊薬」を意味する一般名詞。他分野・他言語との衝突は少ない。

化学・錬金術用語である「万能薬」を意味する語から取られている。一般名詞ではあるが日常会話での使用頻度が低いため、検索性はGoやSwiftほど悪化していない。Erlang VM上で動く後発言語として、印象的で覚えやすい名前を優先した例といえる。