← melang

Erlang

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

型

変数は一度だけ束縛できる単一代入(single assignment)。

Age = 20.
Age = 20,
%% Age = "Alice". %% 既に束縛済みの変数への再代入はマッチエラー

case Age of
    N when is_integer(N) -> N + 10;
    _ -> error
end.

変数は一度束縛すると同じ値にしか再束縛できず、別の値を代入しようとすると`badmatch`エラーになる(Elixirの再束縛とは異なる)。型注釈の構文はなく、型の検査は実行時のパターンマッチとガード節(`is_integer`など)で行う。オプションのtypespec(`-spec`)をDialyzerという静的解析ツールで検査できるが、コンパイラソースコードを解析し、実行可能な形式(機械語や中間表現)に変換するプログラム。静的型付け言語では、実行前にコンパイラが型の不整合などをチェックする役割も担う。自体は検査しない動的型付け言語。

型注釈の位置

型は関数定義の直前に`-spec`属性として別行に書く。Elixirの`@spec`の元になった構文。

-spec greet(string()) -> string().
%% Age = 20. 変数自体に型注釈はない

-spec greet(string()) -> string(). %% 関数定義の直前に別行で型を書く
greet(Name) -> "Hello, " ++ Name.

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

型変換

変換は組み込み関数(BIF)で行い、失敗時はプロセスが異常終了する。

integer_to_list(X)
integer_to_list(42).   %% "42"
list_to_integer("42"). %% 42
list_to_integer("abc"). %% badarg でプロセスが異常終了

try list_to_integer("abc") of
    N -> N
catch
    error:badarg -> 0
end.

`integer_to_list/1`, `list_to_integer/1`のような組み込み関数(BIF)で変換する。失敗すると`badarg`のようなエラーで即座にプロセスが終了する「let it crash」が基本方針で、必要な箇所だけ`try`/`catch`で捕捉する。Elixirの`{:ok, value}`のような値としての失敗表現は、Erlang標準ライブラリの慣習としては薄い。

暗黙の型変換

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

X = 10.
X = 10,
Y = 1.5,
X + Y.   %% 11.5(integerがfloatへ暗黙変換される)
X + "5". %% badarith(数値と文字列の暗黙変換はない)

integerとfloatを混在させる算術演算では暗黙に型が揃えられるが、それ以外の型(文字列・アトムなど)との暗黙変換は行われない。ガード節(`is_integer`など)で型を明示的に確認するスタイルが基本で、Elixirもこの挙動をそのまま受け継いでいる。

Enum / Sum Type

専用のenum型はなく、tagged tupleとpattern matchingで表現する。Elixirの元になった設計。

{success, User} | {failure, Message}
describe({success, User}) -> "ok: " ++ User;
describe({failure, Message}) -> "error: " ++ Message.
%% 網羅しないケースがあると実行時にfunction_clauseになる

専用のenum型はなく、atomとtagged tuple(`{success, User}`のような組)によるpattern matchingでADTAlgebraic Data Type(代数的データ型)の略。複数の型を組み合わせて新しい型を作る手法で、「AまたはB」のように取りうる形が閉じた集合になっているものをSum Type(直和型)と呼ぶ。Rustの`enum`やHaskellの`data`が代表例で、各variant(構成要素)が異なるフィールドを持てる点で、単純な整数の列挙とは異なる。的に表現する。網羅性はコンパイラソースコードを解析し、実行可能な形式(機械語や中間表現)に変換するプログラム。静的型付け言語では、実行前にコンパイラが型の不整合などをチェックする役割も担う。ではなく、実行時のマッチ失敗(`function_clause`エラー)として現れ、プロセスを異常終了させて上位のsupervisorに委ねる「let it crash」の一部として扱われる。Elixirの`{:ok, value}` / `{:error, reason}`という慣習はこの構文をそのまま受け継いでいる。

値の不変性

変数は一度束縛すると同じ値にしか再束縛できない、厳密な単一代入。

X = ... / Map#{key := Value}
User = #{name => "alice", tags => ["a"]},
Other = User,
User2 = User#{name := "bob"},   %% 新しいmapを作り、別の変数へ束縛する
%% User = User2. は badmatch(既に束縛済みの変数は上書きできない)
%% Other は #{name => "alice", ...} のまま変わらない

Tags = maps:get(tags, User) ++ ["admin"],  %% 新しいlist

%% 状態は、再帰で新しい状態へ遷移して持つ
loop(Count) ->
    receive
        inc -> loop(Count + 1)
    end.

Elixirの`x = 1`に続く`x = 2`のようなrebindingすら許さず、同じ変数名への再束縛は既に束縛済みの値と一致する場合しか成立しない厳密な単一代入(single assignment)。データはすべて不変で、更新は新しい値を作って別の変数に束縛する。プロセスの状態は再帰で新しい状態に遷移させ、melangが目指す「値は不変・再代入は禁止」という設計に、束縛の面ではElixirより近い。

名前付き引数

言語機能としての名前付き引数はなく、キーと値の組のリストやmapを最後の引数として渡す。

foo(Opts)
foo(Opts) ->
    Width = proplists:get_value(width, Opts, 0),
    Height = proplists:get_value(height, Opts, 0),
    Width + Height.

foo([{width, 10}, {height, 20}]).
foo(#{width => 10, height => 20}).

proplist(`{key, value}`のタプルのリスト)やmapを1つの引数として渡し、関数側で`proplists:get_value/3`やパターンマッチで値を取り出す慣習がある。`width`・`height`という名前を関数シグネチャがコンパイラソースコードを解析し、実行可能な形式(機械語や中間表現)に変換するプログラム。静的型付け言語では、実行前にコンパイラが型の不整合などをチェックする役割も担う。で検証するわけではなく、キーワードリストを言語機能として持つElixirより一段低レイヤーな表現。

関数の引数

値がすべて不変。プロセス境界を越えるときだけメッセージとしてコピーされる。

f(List)
f(List) -> [1 | List].  %% 元のListは変わらない

Xs = [2, 3],
Ys = f(Xs),
%% Xs =:= [2, 3]
%% Ys =:= [1, 2, 3]

%% プロセス間で送るときは値がコピーされる
Pid ! {self(), Xs}.

値はすべて不変で、通常の関数呼び出しは同一プロセス内で値を共有して渡すため、渡し方を選ぶ構文はない。一方、プロセス間でメッセージ(`!`)として送るときは、共有メモリを持たないプロセスモデルのため値がコピーされる。「関数呼び出しの引数」と「プロセス間メッセージ」で共有かコピーかが変わる点が、単一プロセスのHaskell・Elixirとの違い。

if 文/式

`if`は式だが`else`はなく、ガード式の節で分岐する。どの節にも一致しないと実行時エラーになる。

if Guard1 -> A; true -> B end
X = if
        Cond -> 1;
        true -> 2
    end.

% どの節にも一致しないとif_clauseエラーになる

`if`は式で、`else`という構文はない。節の末尾に`true ->`を置いてelse相当を表す。条件にはガード式しか書けないため、実際には`case`やパターンマッチによる関数節の分岐が主に使われる。

I/O の明示

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

find_user(Id)
find_user(Id) ->
    db:get(user, Id).

get_user(Id) ->
    find_user(Id). %% 呼べる

`-spec`で型を後付けしDialyzerで検査できるが、effectを表す語彙は用意されていない。並行処理はプロセス間メッセージパッシング(`!`と`receive`)で行うが、これも通常の関数呼び出しとは別構文である一方、I/Oの有無自体を型シグネチャから読み取る手段ではない。

言語名

開発元エリクソン社と数学者の名を掛けた人名・社名由来の固有名詞。

開発元Ericsson(エリクソン)社の略称と、数学者Agner Krarup Erlang(待ち行列理論の創始者で、通信分野の単位「アーラン」にも名を残す)の名を掛けた命名とされる。固有名詞のため一般語との衝突は少ないが、開発したEricsson社内での呼称という側面が強く、対外的な知名度はElixirの後発ブームに助けられて広がった面がある。処理系(VM)を指す`BEAM`という別名も持ち、言語名・VM名・実行コマンド(`erl`)が三者三様である点はRustに似た構造を持つ。