「配列」っぽい型に続いて「っぽい型」シリーズ第二弾(おそらく打ち止め)です。
上の記事ではArrayっぽい型をいくつかご紹介しました。Arrayほどではないにしろ,Arrayと並ぶコレクション界の雄「Hash」にも1つだけ「っぽい型」が存在します。
NamedTuple(**T)
NamedTupleは,NamedTuple.from
にHashを与えて生成できるほか,{key: value, ...}
というリテラル構文を持っています。後者の構文は,RubyではシンボルをキーとしたHashのリテラル構文ですが,Crystalでは{:key => value}
と{key: value}
が異なる型(前者はHash,後者はNamedTuple)のオブジェクトになるので注意が必要です。
# 両方とも同じ結果
p named_tuple = NamedTuple(name: String, age: Int32).from({:name => "John", :age => 32})
p named_tuple = {name: "John", age: 32}
#=> {name: "John", age: 32}
NamedTupleは「キーにSymbol型しか使用できず,一旦生成されたら一切の変更がきかない不変なHash」のように見えます。また,やはりジェネリック型の一種ですが,型引数をキーと一緒に値の数だけすべて指定する(100個値があれば,キーと型引数の指定が100組必要)という少し変わった特徴があります。
Tupleが各要素の型を内部的に保持していたように,NamedTupleもあるキーに対応する値の型を内部的に保持しています。そのため,#[]
で取得した値に対して,その型に固有のメソッドをそのまま使用できます。
name = named_tuple[:name]
p name.upcase
#=> "JOHN"
Hashの場合は,複数の型の値を含んでいると#[]
で取得した値の型がArrayの要素と同様のユニオン型になってしまいます。そのため,値の型に固有なメソッドを使用するためには,if
文などによる型の特定が必要です。
hash = {:name => "John", :age => 32}
name = hash[:name]
p name.upcase
#=> Error in line 4: undefined method 'upcase' for Int32 (compile-time type is (Int32 | String))
if name.is_a?(String)
# ここでは name は必ずString型
p name.upcase
#=> "JOHN"
end
NamedTupleもTupleと同様にメソッドの引数周りでよく目にします。Crystalではダブルスプラット(**
)を使ってメソッドの引数を定義すると,可変長の名前付き引数を指定することができるようになります。この際,メソッド側で引数を受け取るのがNamedTupleオブジェクトです。
def foo(**args)
p typeof(args)
end
foo(a: 1, b: "2")
#=> NamedTuple(a: Int32, b: String)
また,NamedTupleを使ってメソッドに対して複数の引数を1度に指定することもできます。
def bar(a: Int32, b: String)
p a
p b
end
args = {a: 1, b: "2"}
bar(**args)
#=> 1
# "2"
【おまけ】ハッシュっぽい型のリテラルっぽい生成法
配列っぽい型ではリテラルっぽいオブジェクトの生成が可能であったように,Hashのように何らかのキーに対して値を設定できる型も,リテラルっぽい特殊な構文でオブジェクトを生成することができます。
この時必要になるのは,引数を取らないコンストラクタと#[]=
メソッドです。
たとえば,内部にHash(String, Int32)型のインスタンス変数をもち#total
で値の合計を求められるラッパクラスAdderHashを考えてみましょう。
class AdderHash
@hash = {} of String => Int32
def total
@hash.values.reduce { |t, i| t + i }
end
# 未定義のメソッドは全て @hash に移譲
forward_missing_to @hash
end
実装から想定される基本的な使い方はこんな感じです。
add_hash = AdderHash.new
add_hash["a"] = 1
add_hash["b"] = 2
add_hash["c"] = 3
p add_hash.total
#=> 6
この型も引数を取らないコンストラクタと#[]=
メソッドが使用できる1ので,そのままで以下のようなリテラルっぽい構文を利用できます。
add_hash = AdderHash{"a" => 1, "b" => 2, "c" => 3}
p add_hash.total
#=> 6
-
AdderHash自体には
#[]=
が定義されていませんが,未定義のメソッドをforward_missing_to
マクロで全てインスタンス変数@hash
に移譲しているので,そちらの#[]=
が使用されます ↩