コンテンツにスキップ

タプルとハッシュシェイプ

TupleHashShapeは、Rigorが異種配列と既知キーのハッシュに精密な型を与える方法です。外見上はRubyのArrayHashによく似ており(RBS境界を越えるとこれらの名前的型(nominal type、公称型とも)に消去されます)、Rigorの内部では通常のArray[T] / Hash[K, V]が失ってしまう、位置ごと/キーごとの型情報を持ちます。

この章の内容 タプル · map / selectを通じたタプル · タプルの拡幅 · ハッシュシェイプ · メソッド経由のハッシュシェイプ · キーワード引数ハッシュ · スプラット合成 · パターンマッチング · レイアウトが証明できないとき · 新しいシェイプの派生(Pick / Omit / …)

解析器が配列リテラルのレイアウトを証明できるとき、Array[T]ではなくTuple[…]を生成します:

arr = [1, "two", :three]
# Tuple[Constant<1>, Constant<"two">, Constant<:three>]

実際のコードでタプルが現れる最もよくある場面:

# 多重代入の分解は位置ごと
first, second, third = [10, 20, 30]
assert_type("Constant<10>", first)
assert_type("Constant<20>", second)
assert_type("Constant<30>", third)
# divmodは2要素タプルを返す
quotient, remainder = 17.divmod(5)
assert_type("Constant<3>", quotient)
assert_type("Constant<2>", remainder)
# each_with_indexは2要素タプルをyieldする
%w[a b c].each_with_index do |elt, idx|
assert_type("Constant<\"a\"> | Constant<\"b\"> | Constant<\"c\">", elt)
assert_type("non-negative-int", idx)
end

タプルへのインデックスアクセスは位置ごとに保たれます:

arr = [1, "two", :three]
arr[0] # Constant<1>
arr[1] # Constant<"two">
arr[-1] # Constant<:three>
arr[5] # Constant<nil> — 範囲外

[start, length][range]でのスライス(slice)は、一致する要素のタプルを生成します:

arr = [10, 20, 30, 40, 50]
arr[1..3] # Tuple[Constant<20>, Constant<30>, Constant<40>]
arr[2, 2] # Tuple[Constant<30>, Constant<40>]

タプルに対してEnumerableメソッドを呼び出すと、Rigorはブロックを要素ごとの型を代入して各要素について1回評価し、結果をユニオン(union、合併型とも)します:

arr = [1, 2, 3]
doubled = arr.map { |n| n * 2 }
# Tuple[Constant<2>, Constant<4>, Constant<6>]
mixed = [1, "two", :three]
strings = mixed.map { |x| x.to_s }
# Tuple[Constant<"1">, Constant<"two">, Constant<"three">]

selectfilter_mapArray[Element]に広げます。なぜなら結果のサイズが述語に依存し、位置に依存しないからです。findは要素のユニオン(または静的にどの要素も一致しないときnil)を返します。

Tupleは、サイズが設定可能なユニオン予算を超えたとき、未知の形状の配列が連結されたとき、またはRBSでArray[T]として型付けされたパラメータを越えるときにArray[T]に拡幅されます。拡幅は決定論的で、docs/type-specification/inference-budgets.mdに文書化されています。

拡幅は安全です — Array[T]は同じ値のより精度が低いビューです — しかし位置ごとの情報が失われます。[a, b, c]が精密に型チェックされるべきなのにされない状況に遭遇したら、チェーン内のタプルではなくArray[T]を受け取るメソッド、または広い配列に対する+ / concatを探してください。

ハッシュシェイプ — 既知キーのハッシュ

Section titled “ハッシュシェイプ — 既知キーのハッシュ”

ハッシュの類似物はHashShapeです:

user = { name: "Alice", age: 30, admin: false }
# HashShape{name: Constant<"Alice">, age: Constant<30>, admin: Constant<false>}
assert_type("Constant<\"Alice\">", user[:name])
assert_type("Constant<30>", user[:age])
assert_type("Constant<false>", user[:admin])

ハッシュシェイプ(shape)にはタプルよりいくつか追加の次元があります:

  • 必須キーと省略可能キー。 キーがリテラルに無条件に書かれたか、条件付きでマージされたか?
  • オープンとクローズ。 列挙されたキー以外の追加キーを持てるか?
  • 読み取り専用エントリー。Rigorがそのキーへの書き込みを見たか、読み取りだけか?

Rigorは3つすべてを追跡しますが、ほとんどはナローイング(narrowing)ルールを通じて公開します — ほとんどのユーザーはこれらを直接考える必要はありません。

メソッド呼び出しを通じたハッシュシェイプ

Section titled “メソッド呼び出しを通じたハッシュシェイプ”
config = { host: "example.com", port: 8080 }
# HashShape{host: Constant<"example.com">, port: Constant<8080>}
config.fetch(:host) # Constant<"example.com">
config.fetch(:host, "x") # Constant<"example.com"> (デフォルト未使用)
config[:port] # Constant<8080>
config.key?(:host) # Constant<true> — 証明済み
config.empty? # Constant<false> — 証明済み
config.size # Constant<2>

キーワード引数でメソッドを呼び出すとき、暗黙のハッシュシェイプがRigorの型チェックの対象です:

def connect(host:, port: 80)
# ...
end
connect(host: "example.com") # OK (portはデフォルト)
connect(host: "example.com", port: 80) # OK
connect(host: "example.com", port: "8080") # warning:
# port: Integer
# が必須のとき

ハッシュシェイプは**スプラットとダブルスプラット操作を通じて流れるので、optsが既知のシェイプのときconnect(**opts)は正しくナローイングされます。

1つのタプルを別のタプルにスプラットすると、スプラットが固定位置にある場合に位置ごとの情報が保持されます:

head = [1, 2]
tail = [3, 4]
arr = [*head, *tail]
# Tuple[Constant<1>, Constant<2>, Constant<3>, Constant<4>]
with_middle = [*head, "X", *tail]
# Tuple[Constant<1>, Constant<2>, Constant<"X">,
# Constant<3>, Constant<4>]

ハッシュシェイプへのダブルスプラットも同様:

defaults = { port: 80, ssl: false }
overrides = { port: 443, ssl: true }
final = { **defaults, **overrides }
# HashShape{port: Constant<443>, ssl: Constant<true>}
# (Rubyのセマンティクスに従いオーバーライドが勝つ)

case x in [a, b, c]は多重代入とまったく同じくa / b / cを位置ごとにナローイングします:

case [10, 20, 30]
in [first, _, third]
assert_type("Constant<10>", first)
assert_type("Constant<30>", third)
end

ハッシュパターンも同様:

case { name: "Alice", age: 30 }
in { name:, age: }
assert_type("Constant<\"Alice\">", name)
assert_type("Constant<30>", age)
end

選言パターン(Integer | String => x)はキャプチャされたローカルに対してユニオンを生成します — 基礎となるナローイングルールについては第3章を参照してください。

レイアウトが証明できないとき

Section titled “レイアウトが証明できないとき”

配列リテラルの要素の1つでもConstantでも、タプル形状でもない型を持つとき、RigorはArray[T]にフォールバックします。ここでTは要素型のユニオンです — まだ有用ですが、位置ごとではありません:

arr = [1, ARGV.first]
# Array[Constant<1> | String?]

キーが証明可能にシンボル/文字列リテラルでないハッシュも同様 — RigorはHashShapeではなくHash[K, V]を生成します。

新しいシェイプを派生させる — pick_of / omit_of / partial_of / required_of / readonly_of

Section titled “新しいシェイプを派生させる — pick_of / omit_of / partial_of / required_of / readonly_of”

HashShape(またはTuple)があって、フィールドの一部だけを残したい、特定のものを落としたい、必須/任意を反転させたい — そんなときのために、RigorはType::Combinator上に5つのシェイプ射影型関数を公開しています。これらはTypeScriptのPick/Omit/Partial/Required/Readonlyユーティリティ型に対応しますが、TSの後付けではなく第一級のRigor操作です。各関数は、残したエントリーに対してソースの既存の分類(必須/任意/読み取り専用/追加キーのポリシー)を保ちます。

射影動作TypeScriptでの対応
pick_of[T, K]リテラルキーユニオンKにキーが含まれるエントリーだけを残す。Tupleの場合、Kは整数インデックスのユニオン。Pick<T, K>
omit_of[T, K]Kにキーが含まれるエントリーを落とし、残りを保つ。Omit<T, K>
partial_of[T]必須エントリーすべてを任意に反転させる。値型をnil広げない — Rigorは「キーが存在しない」と「キーは存在し値がnil」を区別する。Partial<T>
required_of[T]partial_ofの逆。任意エントリーすべてを必須に反転させる。Required<T>
readonly_of[T]現在のビューで各エントリーを読み取り専用としてマークする。基底オブジェクトが凍結されていることを証明するものではない — あくまでビュー層のマーカー。Readonly<T>

これらは2つの表面に現れます:

RBS::Extendedディレクティブのペイロードとして

Section titled “RBS::Extendedディレクティブのペイロードとして”

射影名はディレクティブグラマーの一部であり、パーサは型引数位置でSymbol/Stringリテラルと|ユニオンを受け付けるため、キー集合をその場で書けます:

class UserView
# ランタイムはユーザーハッシュ全体を返すが、ビューは呼び出し元へ
# :nameと:emailだけを公開する。ディレクティブは戻り値側の
# HashShapeをその2エントリーに絞り込む。
%a{rigor:v1:return: pick_of[UserHash, :name | :email]}
def public_attrs: () -> ::Hash[::Symbol, ::String]
end

解析対象のファイル内では、呼び出しサイトの結果型は、基底のRBSシグが宣伝する素のHash[Symbol, String]ではなく、射影されたHashShapeになります。

オプトインのTypeScriptユーティリティ型プラグイン経由で

Section titled “オプトインのTypeScriptユーティリティ型プラグイン経由で”

ディレクティブでTSの綴り(Pick<T, K>など)を使いたい場合は、rigor-typescript-utility-typesプラグインをオプトインしてください。このプラグインはPlugin::TypeNodeResolverを登録し、各TS名を正準射影に変換します:

.rigor.yml
plugins:
- gem: rigor-typescript-utility-types
%a{rigor:v1:return: Pick[UserHash, "name" | "email"]}

プラグインチェインは解析器が見る前にPick[…]pick_of[…]に解決します — 推論結果は直接pick_ofと書いたときと同じです。プラグインは純粋に命名上の利便性のためのものです。

射影は、シェイプ情報を保つキャリア(carrier)(HashShape、およびpick_of/omit_ofの場合のTuple)にのみ発火します。素のHash[K, V]やその他の非シェイプ入力に適用するのは損失のある(lossy)処理です — 射影は静かに入力型へ縮退し、Rigorはdynamic.shape.lossy-projection:info診断を記録するので、呼び出しサイトを監査できます。

class C
# `User`はHashShapeではなく`Nominal[User]`なので、射影は何も
# 絞り込めない。ディレクティブは受理されるが、`:info`が損失のある
# 縮退を記録する。
%a{rigor:v1:return: pick_of[User, :name]}
def render: () -> ::User
end

修正は通常、HashShapeキャリアを書く(あるいはData.define/Structを使う)ことで、素のNominalを使わないことです。

第5章は関数の側を扱います: Rigorがメソッドのパラメータと戻り値型をどう型付けするか、Enumerable反復を通じてブロックパラメータがどうバインドされるか、アリティ(arity)/パラメータ型のミスマッチがcall.*診断としてどう現れるか。

© 2026 TypedDuck. Licensed under CC BY-SA 4.0.