Elixir
melangの各noteで他言語と比較した、Elixirの設計をまとめたページ。ここに書かれている内容は、各noteの比較表から自動で集めたものです。
型
再代入ではなくパターンマッチによる束縛が基本。
age = 20age = 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 = 10x = 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 endx = 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上で動く後発言語として、印象的で覚えやすい名前を優先した例といえる。