
2026/03/12 6:13
**型システムはリークする抽象化です:`Map.take!/2` のケース** - 関数 `Map.take!/2` は、第二引数で指定されたキーだけを含むマップを返すように設計されています。 - 入力マップに存在しないキーが要求されると、`Map.take!/2` はエラーを発生させます(欠落したキーを黙って無視するのではなく)。 - この挙動は実装詳細を漏らすものであり、呼び出し側は例外が起こり得ることを知り、それに対処する必要があります。 - 一方でノンバング版 `Map.take/2` は利用可能なキーだけを持つ新しいマップを返すため、多くのシナリオで安全に使えるようになっています。 **含意** - `Map.take!/2` をパターンマッチや型推論に頼る型システムは、呼び出し側が例外を考慮していない場合、安全性を保証できなくなる恐れがあります。 - 開発者は、バング版のエラー投げ動作が明示的に必要でない限り、`Map.take/2` の使用を優先すべきです。
RSS: https://news.ycombinator.com/rss
要約▶
Japanese Translation:
José Valim は 2026 年 3 月 3 日に Elixir に集合論的型システムを追加することについて書きました。彼は
Map.take!/2 を例として示し、これはマップから要求されたキーが欠けている場合にエラーを発生させる関数です。
def take!(map, keys) when is_map(map) and is_list(keys) do Map.new(keys, fn key -> {key, Map.fetch!(map, key)} end) end
汎用型シグネチャ
map(), [term()] -> map() は、結果が入力キーの部分集合を含むことを表現できません。TypeScript のような静的型付き言語では、この関係はジェネリクスと keyof でキャプチャされます。
function take<T extends Record<string, any>, K extends keyof T>( obj: T, keys: readonly K[] ): Pick<T, K> { … }
キーのリストが実行時に決定される場合(例:
Math.random() を通じて)、Elixir は string[] のような型を推論し、安全性保証を破ります。as const を使用するとリテラル型を保持できますが、 "name" | "email" | "age" などの不整合なユニオンを生成する可能性があります。分配的条件付き型パターン(TakeResult<T, Keys>)は特定の場合で安全性を改善しますが、複雑さも増します。
Valim は二つの実用的な道筋を提案しています:
- 高度なジェネリクス – Elixir の推論に TypeScript スタイルの条件付き型を拡張し、部分集合関係を保持する。
- マクロベースの検証 –
のようにキーリストがコンパイル時リテラルであることを保証。マクロは明示的なマップ式に展開し、複雑な推論を回避します。Map.take!(user, [:name, :age])
記事ではトレードオフを強調しています:より豊富な型システムは表現力を高めますが、学習曲線とツールのオーバーヘッドも増加します。マクロソリューションはその独自の長所と短所を持つ、簡易的な回避策として提供されています。
本文
ジョゼ・ヴァリム – 2026年3月3日
動的言語に型システムを追加する
動的言語へ型システムを導入することは、型が表現力をどのように制限するかを示す優れた演習です。
この記事では、Elixir 標準ライブラリに潜在的に存在しうる
Map.take!/2 関数を例に取り上げて、その課題と解決策を探ります。
Dashbit ではスタートアップや企業が Elixir チームを採用・育成するのを支援しています。Elixir、Nx、Livebook の創設者として、集合論的型システムを Elixir に導入する取り組みをリードしています。限られたクライアントとの経験からエコシステムを改善し続けています。私たちのチームがどのように Elixir の活用を次のレベルへ引き上げるか、ぜひお問い合わせください。
Map.take/2
を探る
Map.take/2Elixir ではマップが主要なキー–バリュー構造です。キーがアトムである場合、フィールドは事前にすべて分かっている「レコード」のように振る舞います。
Elixir にはすでに
Map.take/2 が存在します:
iex> user = %{id: 1, name: "Alice", email: "alice@example.com", age: 30} iex> Map.take(user, [:name, :email]) %{name: "Alice", email: "alice@example.com"}
キーが存在しない場合は無視されます。マップをレコードとして扱う際には、キーが必ず存在することを保証したいケースが多く、そこで
Map.take!/2 の提案が生まれました。
Map.take!/2
の提案
Map.take!/2最近、Wojtek Mach が
Map.take!/2 を追加することを提案しました。Elixir では感嘆符付き関数は「入力が有効でも例外を投げる可能性がある」ことを示します。例えば:
iex> Map.take!(user, [:name, :email, :missing]) ** (KeyError) unknown key :missing
単純な実装は次のようになります:
def take!(map, keys) when is_map(map) and is_list(keys) do Map.new(keys, fn key -> {key, Map.fetch!(map, key)} end) end
型システムを導入すると、戻り値の正確な形状を反映した署名が必要になります。
現在(広すぎる)型署名
現段階では最も適切とできるものは次のようになります:
$ map(), [term()] -> map() def take!(map, keys) when is_map(map) and is_list(keys)
これは戻り値が
map() であることだけを示し、キーについては情報を持ちません。そのため結果を安全に利用することができません。
静的型付き言語の難点
多くの静的型付け言語では、キーごとの組み合わせごとにボイラープレートを書く必要があります:
def take_name_and_email(user) do %{name: user.name, email: user.email} end def take_name_and_age(user) do %{name: user.name, age: user.age} end
Map.take!/2 の目的はこのようなボイラープレートを回避することです。
TypeScript の keyof
keyofTypeScript における類似例が課題を示します:
function take<T extends Record<string, any>, K extends keyof T>( obj: T, keys: readonly K[] ): Pick<T, K> { /* ... */ }
キーリストが静的に既知であれば機能します。しかし、ランタイムで変化する(例えば
Math.random() によって選択される)場合は型推論が string[] へと崩れ、静的チェックが失われます。
as const を使った精度強制やキーをマッピングすると、不整合が生じる可能性があります。推論された型が実際に存在しないフィールドを許容してしまう恐れがあります。
安全性への道
Ken AKAFrosty は分配的条件型アプローチを提案しました:
type TakeResult<T, Keys extends readonly (keyof T)[]> = Keys extends any ? Pick<T, Keys[number]> : never; function take< T extends Record<string, any>, K extends keyof T, const Keys extends readonly K[] >(obj: T, keys: Keys): TakeResult<T, Keys> { /* ... */ }
キーリストが文字列リテラルのユニオンである場合、これは安全性を保ちます。ただし、キー配列上でマッピングするような複雑な変換では破綻する可能性があります。
マクロによる逃げ道
一つの解決策は
Map.take!/2 をマクロにすることです。コンパイル時に展開すると:
Map.take!(user, [:name, :age]) # => %{name: user.name, age: user.age}
マクロはキーリストがリテラルであることを保証でき、複雑な署名を必要とせずに正確な結果型を推論できます。
マクロは他言語(Racket、Elixir の
printf 例など)でも成功裏に利用されています。静的型だけでは保証できないコンパイル時制約を課すことが可能です。
要点
動的コードは表現力豊かで正しくても、静的型システムの検証が難しい場合があります。ロジックをリファクタリングしたりカプセル化したりすると、意図せず型安全性が損なわれることがあります。そのためには:
- より多くのボイラープレートを許容する
- 高度な型抽象(コストが高い)を追加する
- あるいはマクロでコンパイル時保証を実装する
という選択肢があります。既存の動的言語に型システムを設計・導入する際には、これらのトレードオフを理解しておくことが不可欠です。