# A named tuple is a fixed-size, immutable, stack-allocated mapping # of a fixed set of keys to values. # # You can think of a `NamedTuple` as an immutable `Hash` whose keys (which # are of type `Symbol`), and the types for each key, are known at compile time. # # A named tuple can be created with a named tuple literal: # # ``` # language = {name: "Crystal", year: 2011} # NamedTuple(name: String, year: Int32) # # language[:name] # => "Crystal" # language[:year] # => 2011 # language[:other] # compile time error # ``` # # See [`NamedTuple` literals](https://crystal-lang.org/reference/syntax_and_semantics/literals/named_tuple.html) in the language reference. # # The compiler knows what types are in each key, so when indexing a named tuple # with a symbol or string literal the compiler will return the value for that # key and with the expected type, like in the above snippet. Indexing with a # symbol or string literal for which there's no key will give a compile-time # error. # # Indexing with a symbol or string that is only known at runtime will return # a value whose type is the union of all the types in the named tuple, # and might raise `KeyError`. # # Indexing with `#[]?` does not make the return value nilable if the key is # known to exist: # # ``` # language = {name: "Crystal", year: 2011} # language[:name]? # => "Crystal" # typeof(language[:name]?) # => String # ``` # # `NamedTuple`'s own instance classes may also be indexed in a similar manner, # returning their value types instead: # # ``` # tuple = NamedTuple(name: String, year: Int32) # tuple[:name] # => String # tuple["year"] # => Int32 # tuple[:other]? # => nil # ``` struct NamedTuple # Creates a named tuple that will contain the given arguments. # # With a named tuple literal you cannot create an empty named tuple. # This method doesn't have this limitation, which makes it especially # useful in macros and generic code. # # ``` # NamedTuple.new(name: "Crystal", year: 2011) #=> {name: "Crystal", year: 2011} # NamedTuple.new # => {} # {} # syntax error # ``` def self.new(**options : **T) {% if @type.name(generic_args: false) == "NamedTuple" %} # deduced type vars options {% elsif @type.name(generic_args: false) == "NamedTuple()" %} # special case: empty named tuple # TODO: check against `NamedTuple()` directly after 1.5.0 options {% else %} # explicitly provided type vars # following `typeof` is needed to access private types {% begin %} { {% for key in T %} {{ key.stringify }}: options[{{ key.symbolize }}].as(typeof(element_type({{ key.symbolize }}))), {% end %} } {% end %} {% end %} end # Creates a named tuple from the given hash, with elements casted to the given types. # Here the Int32 | String union is cast to Int32. # # ``` # num_or_str = 42.as(Int32 | String) # NamedTuple(name: String, val: Int32).from({"name" => "number", "val" => num_or_str}) # => {name: "number", val: 42} # # num_or_str = "a string".as(Int32 | String) # NamedTuple(name: String, val: Int32).from({"name" => "number", "val" => num_or_str}) # raises TypeCastError (Cast from String to Int32 failed) # ``` # See also: `#from`. def self.from(hash : Hash) : self {% begin %} NamedTuple.new(**{{T}}).from(hash) {% end %} end # Expects to be called on a named tuple whose values are types, creates a tuple from the given hash, # with types casted appropriately. The hash keys must be either symbols or strings. # # This allows you to easily pass a hash as individual named arguments to a method. # # ``` # require "json" # # def speak_about(thing : String, n : Int64) # "I see #{n} #{thing}s" # end # # hash = JSON.parse(%({"thing": "world", "n": 2})).as_h # hash : Hash(String, JSON::Any) # hash = hash.transform_values(&.raw) # hash : Hash(String, JSON::Any::Type) # # speak_about(**{thing: String, n: Int64}.from(hash)) # => "I see 2 worlds" # ``` def from(hash : Hash) if size != hash.size raise ArgumentError.new("Expected a hash with #{size} keys but one with #{hash.size} keys was given.") end {% begin %} NamedTuple.new( {% for key, value in T %} {{key.stringify}}: self[{{key.symbolize}}].cast(hash.fetch({{key.symbolize}}) { hash[{{key.stringify}}] }), {% end %} ) {% end %} end # Returns the value for the given *key* if there is such a key, otherwise # raises `KeyError`. # Read the type docs to understand the difference between indexing with a # literal or a variable. # # ``` # tuple = {name: "Crystal", year: 2011} # # tuple[:name] # => "Crystal" # typeof(tuple[:name]) # => String # tuple["year"] # => 2011 # typeof(tuple["year"]) # => Int32 # tuple[:other] # Error: missing key 'other' for named tuple NamedTuple(name: String, year: Int32) # # key = :name # tuple[key] # => "Crystal" # typeof(tuple[key]) # => (Int32 | String) # # key = "year" # tuple[key] # => 2011 # # key = :other # tuple[key] # raises KeyError # ``` def [](key : Symbol | String) fetch(key) { raise KeyError.new "Missing named tuple key: #{key.inspect}" } end # Returns the value type for the given *key* if there is such a key, otherwise # raises `KeyError`. # Read the type docs to understand the difference between indexing with a # literal or a variable. # # ``` # alias Foo = NamedTuple(name: String, year: Int32) # # Foo[:name] # => String # Foo["year"] # => Int32 # Foo["year"].zero # => 0 # Foo[:other] # => Error: missing key 'other' for named tuple NamedTuple(name: String, year: Int32).class # # key = :year # Foo[key] # => Int32 # Foo[key].zero # Error: undefined method 'zero' for String.class (compile-time type is (Int32.class | String.class)) # # key = "other" # Foo[key] # raises KeyError # ``` def self.[](key : Symbol | String) self[key]? || raise KeyError.new "Missing named tuple key: #{key.inspect}" end # Returns the value for the given *key* if there is such a key, otherwise # returns `nil`. # Read the type docs to understand the difference between indexing with a # literal or a variable. # # ``` # tuple = {name: "Crystal", year: 2011} # # tuple[:name]? # => "Crystal" # typeof(tuple[:name]?) # => String # tuple["year"]? # => 2011 # typeof(tuple["year"]?) # => Int32 # tuple[:other]? # => nil # typeof(tuple[:other]?) # => Nil # # key = :name # tuple[key]? # => "Crystal" # typeof(tuple[key]?) # => (Int32 | String | Nil) # # key = "year" # tuple[key]? # => 2011 # # key = :other # tuple[key]? # => nil # ``` def []?(key : Symbol | String) fetch(key, nil) end # Returns the value type for the given *key* if there is such a key, otherwise # returns `nil`. # Read the type docs to understand the difference between indexing with a # literal or a variable. # # ``` # alias Foo = NamedTuple(name: String, year: Int32) # # Foo[:name]? # => String # Foo["year"]? # => Int32 # Foo["year"]?.zero # => 0 # Foo[:other]? # => nil # typeof(Foo[:other]?) # => Nil # # key = :year # Foo[key]? # => Int32 # Foo[key]?.zero # Error: undefined method 'zero' for String.class (compile-time type is (Int32.class | String.class | Nil)) # # key = "other" # Foo[key]? # => nil # ``` def self.[]?(key : Symbol | String) # following `typeof` is needed to access private types {% begin %} case key {% for key in T %} when {{ key.symbolize }}, {{ key.stringify }} typeof(element_type({{ key.symbolize }})) {% end %} end {% end %} end # Traverses the depth of a structure and returns the value. # Returns `nil` if not found. # # ``` # h = {a: {b: {c: [10, 20]}}, x: {a: "b"}} # h.dig? :a, :b, :c # => [10, 20] # h.dig? "a", "x" # => nil # ``` def dig?(key : Symbol | String, *subkeys) if (value = self[key]?) && value.responds_to?(:dig?) value.dig?(*subkeys) end end # :nodoc: def dig?(key : Symbol | String) self[key]? end # Traverses the depth of a structure and returns the value, otherwise # raises `KeyError`. # # ``` # h = {a: {b: {c: [10, 20]}}, x: {a: "b"}} # h.dig :a, :b, :c # => [10, 20] # h.dig "a", "x" # raises KeyError # ``` def dig(key : Symbol | String, *subkeys) if (value = self[key]) && value.responds_to?(:dig) return value.dig(*subkeys) end raise KeyError.new "NamedTuple value not diggable for key: #{key.inspect}" end # :nodoc: def dig(key : Symbol | String) self[key] end # Returns the value for the given *key*, if there's such key, otherwise returns *default_value*. # # ``` # tuple = {name: "Crystal", year: 2011} # tuple.fetch(:name, "Unknown") # => "Crystal" # tuple.fetch("year", 0) # => 2011 # tuple.fetch(:other, 0) # => 0 # ``` def fetch(key : Symbol | String, default_value) fetch(key) { default_value } end # Returns the value for the given *key*, if there's such key, otherwise the value returned by the block. # # ``` # tuple = {name: "Crystal", year: 2011} # tuple.fetch(:name) { "Unknown" } # => "Crystal" # tuple.fetch(:other) { 0 } # => 0 # ``` def fetch(key : Symbol, &block) {% for key in T %} return self[{{key.symbolize}}] if {{key.symbolize}} == key {% end %} yield end # Returns the value for the given *key*, if there's such key, otherwise the value returned by the block. # # ``` # tuple = {name: "Crystal", year: 2011} # tuple.fetch("name") { "Unknown" } # => "Crystal" # tuple.fetch("other") { 0 } # => 0 # ``` def fetch(key : String, &block) {% for key in T %} return self[{{key.symbolize}}] if {{key.stringify}} == key {% end %} yield end # Merges two named tuples into one, returning a new named tuple. # If a key is defined in both tuples, the value and its type is used from *other*. # # ``` # a = {foo: "Hello", bar: "Old"} # b = {bar: "New", baz: "Bye"} # a.merge(b) # => {foo: "Hello", bar: "New", baz: "Bye"} # ``` def merge(other : NamedTuple) merge(**other) end # :ditto: def merge(**other : **U) forall U {% begin %} NamedTuple.new( {% for k in T %} {% unless U.keys.includes?(k) %} {{k.stringify}}: self[{{k.symbolize}}],{% end %} {% end %} {% for k in U %} {{k.stringify}}: other[{{k.symbolize}}], {% end %} ) {% end %} end # Merges two named tuples into one, returning a new named tuple. # If a key is defined in both tuples, the value and its type are used from **self** (original tuple) # # ``` # a = {foo: "Hello", bar: "Old"} # b = {bar: "New", baz: "Bye"} # a.reverse_merge(b) # => {foo: "Hello", bar: "Old", baz: "Bye"} # ``` def reverse_merge(other : NamedTuple) other.merge(**self) end # :ditto: def reverse_merge(**other) reverse_merge(other) end # Returns a hash value based on this name tuple's size, keys and values. # # See also: `Object#hash`. # See `Object#hash(hasher)` def hash(hasher) {% for key in T.keys.sort %} hasher = {{key.symbolize}}.hash(hasher) hasher = self[{{key.symbolize}}].hash(hasher) {% end %} hasher end # Returns the types of this named tuple type. # # ``` # tuple = {a: 1, b: "hello", c: 'x'} # tuple.class.types # => {a: Int32, b: String, c: Char} # ``` def self.types NamedTuple.new(**{{T}}) end # Same as `to_s`. def inspect : String to_s end # Returns a `Tuple` of symbols with the keys in this named tuple. # # ``` # tuple = {name: "Crystal", year: 2011} # tuple.keys # => {:name, :year} # ``` def keys {% begin %} Tuple.new( {% for key in T %} {{key.symbolize}}, {% end %} ) {% end %} end # Returns a `Tuple` of symbols with the keys in this named tuple, sorted by name. # # ``` # tuple = {foo: 1, bar: 2, baz: 3} # tuple.sorted_keys # => {:bar, :baz, :foo} # ``` def sorted_keys {% begin %} Tuple.new( {% for key in T.keys.sort %} {{key.symbolize}}, {% end %} ) {% end %} end # Returns a `Tuple` with the values in this named tuple. # # ``` # tuple = {name: "Crystal", year: 2011} # tuple.values # => {"Crystal", 2011} # ``` def values {% begin %} Tuple.new( {% for key in T %} self[{{key.symbolize}}], {% end %} ) {% end %} end # Returns `true` if this named tuple has the given *key*, `false` otherwise. # # ``` # tuple = {name: "Crystal", year: 2011} # tuple.has_key?(:name) # => true # tuple.has_key?(:other) # => false # ``` def has_key?(key : Symbol) : Bool {% for key in T %} return true if {{key.symbolize}} == key {% end %} false end # :ditto: def has_key?(key : String) : Bool {% for key in T %} return true if {{key.stringify}} == key {% end %} false end # Appends a string representation of this named tuple to the given `IO`. # # ``` # tuple = {name: "Crystal", year: 2011} # tuple.to_s # => %({name: "Crystal", year: 2011}) # ``` def to_s(io : IO) : Nil io << '{' {% for key, value, i in T %} {% if i > 0 %} io << ", " {% end %} Symbol.quote_for_named_argument io, {{key.stringify}} io << ": " self[{{key.symbolize}}].inspect(io) {% end %} io << '}' end def pretty_print(pp) pp.surround("{", "}", left_break: nil, right_break: nil) do {% for key, value, i in T %} {% if i > 0 %} pp.comma {% end %} pp.group do pp.text Symbol.quote_for_named_argument({{key.stringify}}) pp.text ": " pp.nest do pp.breakable "" self[{{key.symbolize}}].pretty_print(pp) end end {% end %} end end # Yields each key and value in this named tuple. # # ``` # tuple = {name: "Crystal", year: 2011} # tuple.each do |key, value| # puts "#{key} = #{value}" # end # ``` # # Output: # # ```text # name = Crystal # year = 2011 # ``` def each(&) : Nil {% for key in T %} yield {{key.symbolize}}, self[{{key.symbolize}}] {% end %} end # Yields each key in this named tuple. # # ``` # tuple = {name: "Crystal", year: 2011} # tuple.each_key do |key| # puts key # end # ``` # # Output: # # ```text # name # year # ``` def each_key(&) : Nil {% for key in T %} yield {{key.symbolize}} {% end %} end # Yields each value in this named tuple. # # ``` # tuple = {name: "Crystal", year: 2011} # tuple.each_value do |value| # puts value # end # ``` # # Output: # # ```text # Crystal # 2011 # ``` def each_value(&) : Nil {% for key in T %} yield self[{{key.symbolize}}] {% end %} end # Yields each key and value, together with an index starting at *offset*, in this named tuple. # # ``` # tuple = {name: "Crystal", year: 2011} # tuple.each_with_index do |key, value, i| # puts "#{i + 1}) #{key} = #{value}" # end # ``` # # Output: # # ```text # 1) name = Crystal # 2) year = 2011 # ``` def each_with_index(offset = 0, &) i = offset each do |key, value| yield key, value, i i += 1 end end # Returns an `Array` populated with the results of each iteration in the given block, # which is given each key and value in this named tuple. # # ``` # tuple = {name: "Crystal", year: 2011} # tuple.map { |k, v| "#{k}: #{v}" } # => ["name: Crystal", "year: 2011"] # ``` def map(&) {% if T.size == 0 %} [] of NoReturn {% else %} [ {% for key in T %} (yield {{ key.symbolize }}, self[{{ key.symbolize }}]), {% end %} ] {% end %} end # Returns a new `Array` of tuples populated with each key-value pair. # # ``` # tuple = {name: "Crystal", year: 2011} # tuple.to_a # => [{:name, "Crystal"}, {:year, 2011}] # ``` # # NOTE: `to_a` on an empty named tuple produces an `Array(Tuple(Symbol, NoReturn))` def to_a to_a(&.itself) end # Returns an `Array` with the results of running *block* against tuples with key and values belonging # to this `NamedTuple`. # # ``` # tuple = {first_name: "foo", last_name: "bar"} # tuple.to_a(&.last.capitalize) # => ["Foo", "Bar"] # ``` # # NOTE: `to_a` on an empty named tuple produces an `Array(Tuple(Symbol, NoReturn))` def to_a(&) {% if T.size == 0 %} [] of {Symbol, NoReturn} {% else %} [ {% for key in T %} yield({ {{key.symbolize}}, self[{{key.symbolize}}] }), {% end %} ] {% end %} end # Returns a `Hash` with the keys and values in this named tuple. # # ``` # tuple = {name: "Crystal", year: 2011} # tuple.to_h # => {:name => "Crystal", :year => 2011} # ``` # # NOTE: `to_h` on an empty named tuple produces a `Hash(Symbol, NoReturn)` def to_h {% if T.size == 0 %} {} of Symbol => NoReturn {% else %} { {% for key in T %} {{key.symbolize}} => self[{{key.symbolize}}], {% end %} } {% end %} end # Returns the number of elements in this named tuple. # # ``` # tuple = {name: "Crystal", year: 2011} # tuple.size # => 2 # ``` def size {{T.size}} end # Returns `true` if this named tuple is empty. # # ``` # tuple = {name: "Crystal", year: 2011} # tuple.empty? # => false # ``` def empty? size == 0 end # Returns `true` if this tuple has the same keys as *other*, and values # for each key are the same in `self` and *other*. # # ``` # tuple1 = {name: "Crystal", year: 2011} # tuple2 = {year: 2011, name: "Crystal"} # tuple3 = {name: "Crystal", year: 2012} # tuple4 = {name: "Crystal", year: 2011.0} # # tuple1 == tuple2 # => true # tuple1 == tuple3 # => false # tuple1 == tuple4 # => true # ``` def ==(other : self) {% for key in T %} return false unless self[{{key.symbolize}}] == other[{{key.symbolize}}] {% end %} true end # :ditto: def ==(other : NamedTuple) return false unless sorted_keys == other.sorted_keys {% for key in T %} return false unless self[{{key.symbolize}}] == other[{{key.symbolize}}]? {% end %} true end # Returns a named tuple with the same keys but with cloned values, using the `clone` method. def clone {% begin %} NamedTuple.new( {% for key in T %} {{key.stringify}}: self[{{key.symbolize}}].clone, {% end %} ) {% end %} end private def first_key_internal i = 0 keys[i] end private def first_value_internal i = 0 values[i] end # Returns a value with the same type as the value for the given *key* of an # instance of `self`. *key* must be a symbol or string literal known at # compile-time. # # The most common usage of this macro is to extract the appropriate element # type in `NamedTuple`'s class methods. This macro works even if the # corresponding element type is private. # # NOTE: there should never be a need to call this method outside the standard library. private macro element_type(key) x = uninitialized self x[{{ key.id.symbolize }}] end end