Skip to content

Commit d08bf3b

Browse files
committed
implement named tuples
Based on #16580, also much work done by quinnj. `(a=1, ...)` syntax is implemented, and `(; ...)` syntax is implemented but not yet enabled.
1 parent 4f8438b commit d08bf3b

23 files changed

+715
-44
lines changed

NEWS.md

+3
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,9 @@ New language features
1111
a function argument name, the argument is unpacked into local variables `x` and `y`
1212
as in the assignment `(x, y) = arg` ([#6614]).
1313

14+
* Named tuples, with the syntax `(a=1, b=2)`. These behave very similarly to tuples,
15+
except components can also be accessed by name using dot syntax `t.a` ([#22194]).
16+
1417
* Custom infix operators can now be defined by appending Unicode
1518
combining marks, primes, and sub/superscripts to other operators.
1619
For example, `+̂ₐ″` is parsed as an infix operator with the same

base/boot.jl

+1-1
Original file line numberDiff line numberDiff line change
@@ -120,7 +120,7 @@ export
120120
# key types
121121
Any, DataType, Vararg, ANY, NTuple,
122122
Tuple, Type, UnionAll, TypeName, TypeVar, Union, Void,
123-
SimpleVector, AbstractArray, DenseArray,
123+
SimpleVector, AbstractArray, DenseArray, NamedTuple,
124124
# special objects
125125
Function, CodeInfo, Method, MethodTable, TypeMapEntry, TypeMapLevel,
126126
Module, Symbol, Task, Array, WeakRef, VecElement,

base/inference.jl

+16-1
Original file line numberDiff line numberDiff line change
@@ -493,6 +493,8 @@ end
493493
const _Type_name = Type.body.name
494494
isType(@nospecialize t) = isa(t, DataType) && (t::DataType).name === _Type_name
495495

496+
const _NamedTuple_name = NamedTuple.body.body.name
497+
496498
# true if Type is inlineable as constant (is a singleton)
497499
function isconstType(@nospecialize t)
498500
isType(t) || return false
@@ -734,6 +736,10 @@ function isdefined_tfunc(args...)
734736
end
735737
if 1 <= idx <= a1.ninitialized
736738
return Const(true)
739+
elseif a1.name === _NamedTuple_name
740+
if isleaftype(a1)
741+
return Const(false)
742+
end
737743
elseif idx <= 0 || (!isvatuple(a1) && idx > fieldcount(a1))
738744
return Const(false)
739745
elseif !isvatuple(a1) && isbits(fieldtype(a1, idx))
@@ -771,7 +777,9 @@ add_tfunc(nfields, 1, 1,
771777
# TODO: remove with deprecation in builtins.c for nfields(::Type)
772778
isleaftype(x.parameters[1]) && return Const(old_nfields(x.parameters[1]))
773779
elseif isa(x,DataType) && !x.abstract && !(x.name === Tuple.name && isvatuple(x)) && x !== DataType
774-
return Const(length(x.types))
780+
if !(x.name === _NamedTuple_name && !isleaftype(x))
781+
return Const(length(x.types))
782+
end
775783
end
776784
return Int
777785
end, 0)
@@ -1333,6 +1341,10 @@ function getfield_tfunc(@nospecialize(s00), @nospecialize(name))
13331341
end
13341342
return Any
13351343
end
1344+
if s.name === _NamedTuple_name && !isleaftype(s)
1345+
# TODO: better approximate inference
1346+
return Any
1347+
end
13361348
if isempty(s.types)
13371349
return Bottom
13381350
end
@@ -1416,6 +1428,9 @@ function fieldtype_tfunc(@nospecialize(s0), @nospecialize(name))
14161428
if !isa(u,DataType) || u.abstract
14171429
return Type
14181430
end
1431+
if u.name === _NamedTuple_name && !isleaftype(u)
1432+
return Type
1433+
end
14191434
ftypes = u.types
14201435
if isempty(ftypes)
14211436
return Bottom

base/namedtuple.jl

+215
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,215 @@
1+
# This file is a part of Julia. License is MIT: https://julialang.org/license
2+
3+
"""
4+
NamedTuple{names,T}(args::Tuple)
5+
6+
Construct a named tuple with the given `names` (a tuple of Symbols) and field types `T`
7+
(a `Tuple` type) from a tuple of values.
8+
"""
9+
function NamedTuple{names,T}(args::Tuple) where {names, T <: Tuple}
10+
if length(args) == length(names)
11+
if @generated
12+
N = length(names)
13+
types = T.parameters
14+
Expr(:new, :(NamedTuple{names,T}), Any[ :(convert($(types[i]), args[$i])) for i in 1:N ]...)
15+
else
16+
N = length(names)
17+
NT = NamedTuple{names,T}
18+
types = T.parameters
19+
fields = Any[ convert(types[i], args[i]) for i = 1:N ]
20+
ccall(:jl_new_structv, Any, (Any, Ptr{Void}, UInt32), NT, fields, N)::NT
21+
end
22+
else
23+
throw(ArgumentError("Wrong number of arguments to named tuple constructor."))
24+
end
25+
end
26+
27+
"""
28+
NamedTuple{names}(args::Tuple)
29+
30+
Construct a named tuple with the given `names` (a tuple of Symbols) from a tuple of
31+
values.
32+
"""
33+
function NamedTuple{names}(args::Tuple) where {names}
34+
NamedTuple{names,typeof(args)}(args)
35+
end
36+
37+
"""
38+
NamedTuple{names}(nt::NamedTuple)
39+
40+
Construct a named tuple by selecting fields in `names` (a tuple of Symbols) from
41+
another named tuple.
42+
"""
43+
function NamedTuple{names}(nt::NamedTuple) where {names}
44+
if @generated
45+
types = Tuple{(fieldtype(nt, n) for n in names)...}
46+
Expr(:new, :(NamedTuple{names, $types}), Any[ :(getfield(nt, $(QuoteNode(n)))) for n in names ]...)
47+
else
48+
types = Tuple{(fieldtype(typeof(nt), n) for n in names)...}
49+
NamedTuple{names, types}(Tuple(getfield(nt, n) for n in names))
50+
end
51+
end
52+
53+
NamedTuple() = NamedTuple{(),Tuple{}}(())
54+
55+
length(t::NamedTuple) = nfields(t)
56+
start(t::NamedTuple) = 1
57+
done(t::NamedTuple, iter) = iter > nfields(t)
58+
next(t::NamedTuple, iter) = (getfield(t, iter), iter + 1)
59+
endof(t::NamedTuple) = nfields(t)
60+
getindex(t::NamedTuple, i::Int) = getfield(t, i)
61+
getindex(t::NamedTuple, i::Symbol) = getfield(t, i)
62+
indexed_next(t::NamedTuple, i::Int, state) = (getfield(t, i), i+1)
63+
64+
convert(::Type{NamedTuple{names,T}}, nt::NamedTuple{names,T}) where {names,T} = nt
65+
convert(::Type{NamedTuple{names}}, nt::NamedTuple{names}) where {names} = nt
66+
67+
function convert(::Type{NamedTuple{names,T}}, nt::NamedTuple{names}) where {names,T}
68+
NamedTuple{names,T}(T(nt))
69+
end
70+
71+
function show(io::IO, t::NamedTuple)
72+
n = nfields(t)
73+
for i = 1:n
74+
# if field types aren't concrete, show full type
75+
if typeof(getfield(t, i)) !== fieldtype(typeof(t), i)
76+
show(io, typeof(t))
77+
print(io, "(")
78+
show(io, Tuple(t))
79+
print(io, ")")
80+
return
81+
end
82+
end
83+
if n == 0
84+
print(io, "NamedTuple()")
85+
else
86+
print(io, "(")
87+
for i = 1:n
88+
print(io, fieldname(typeof(t),i), " = "); show(io, getfield(t, i))
89+
if n == 1
90+
print(io, ",")
91+
elseif i < n
92+
print(io, ", ")
93+
end
94+
end
95+
print(io, ")")
96+
end
97+
end
98+
99+
eltype(::Type{NamedTuple{names,T}}) where {names,T} = eltype(T)
100+
101+
==(a::NamedTuple{n}, b::NamedTuple{n}) where {n} = Tuple(a) == Tuple(b)
102+
==(a::NamedTuple, b::NamedTuple) = false
103+
104+
isequal(a::NamedTuple{n}, b::NamedTuple{n}) where {n} = isequal(Tuple(a), Tuple(b))
105+
isequal(a::NamedTuple, b::NamedTuple) = false
106+
107+
_nt_names(::NamedTuple{names}) where {names} = names
108+
_nt_names(::Type{T}) where {names,T<:NamedTuple{names}} = names
109+
110+
hash(x::NamedTuple, h::UInt) = xor(object_id(_nt_names(x)), hash(Tuple(x), h))
111+
112+
isless(a::NamedTuple{n}, b::NamedTuple{n}) where {n} = isless(Tuple(a), Tuple(b))
113+
# TODO: case where one argument's names are a prefix of the other's
114+
115+
same_names(::NamedTuple{names}...) where {names} = true
116+
same_names(::NamedTuple...) = false
117+
118+
function map(f, nt::NamedTuple{names}, nts::NamedTuple...) where names
119+
if !same_names(nt, nts...)
120+
throw(ArgumentError("Named tuple names do not match."))
121+
end
122+
# this method makes sure we don't define a map(f) method
123+
NT = NamedTuple{names}
124+
if @generated
125+
N = length(names)
126+
M = length(nts)
127+
args = Expr[:(f($(Expr[:(getfield(nt, $j)), (:(getfield(nts[$i], $j)) for i = 1:M)...]...))) for j = 1:N]
128+
:( NT(($(args...),)) )
129+
else
130+
NT(map(f, map(Tuple, (nt, nts...))...))
131+
end
132+
end
133+
134+
# a version of `in` for the older world these generated functions run in
135+
@pure function sym_in(x::Symbol, itr::Tuple{Vararg{Symbol}})
136+
for y in itr
137+
y === x && return true
138+
end
139+
return false
140+
end
141+
142+
@pure function merge_names(an::Tuple{Vararg{Symbol}}, bn::Tuple{Vararg{Symbol}})
143+
names = Symbol[an...]
144+
for n in bn
145+
if !sym_in(n, an)
146+
push!(names, n)
147+
end
148+
end
149+
(names...,)
150+
end
151+
152+
@pure function merge_types(names::Tuple{Vararg{Symbol}}, a::Type{<:NamedTuple}, b::Type{<:NamedTuple})
153+
bn = _nt_names(b)
154+
Tuple{Any[ fieldtype(sym_in(n, bn) ? b : a, n) for n in names ]...}
155+
end
156+
157+
"""
158+
merge(a::NamedTuple, b::NamedTuple)
159+
160+
Construct a new named tuple by merging two existing ones.
161+
The order of fields in `a` is preserved, but values are taken from matching
162+
fields in `b`. Fields present only in `b` are appended at the end.
163+
164+
```jldoctest
165+
julia> merge((a=1, b=2, c=3), (b=4, d=5))
166+
(a = 1, b = 4, c = 3, d = 5)
167+
```
168+
"""
169+
function merge(a::NamedTuple{an}, b::NamedTuple{bn}) where {an, bn}
170+
if @generated
171+
names = merge_names(an, bn)
172+
types = merge_types(names, a, b)
173+
vals = Any[ :(getfield($(sym_in(n, bn) ? :b : :a), $(QuoteNode(n)))) for n in names ]
174+
:( NamedTuple{$names,$types}(($(vals...),)) )
175+
else
176+
names = merge_names(an, bn)
177+
types = merge_types(names, typeof(a), typeof(b))
178+
NamedTuple{names,types}(map(n->getfield(sym_in(n, bn) ? b : a, n), names))
179+
end
180+
end
181+
182+
merge(a::NamedTuple{()}, b::NamedTuple) = b
183+
184+
"""
185+
merge(a::NamedTuple, iterable)
186+
187+
Interpret an iterable of key-value pairs as a named tuple, and perform a merge.
188+
189+
```jldoctest
190+
julia> merge((a=1, b=2, c=3), [:b=>4, :d=>5])
191+
(a = 1, b = 4, c = 3, d = 5)
192+
```
193+
"""
194+
function merge(a::NamedTuple, itr)
195+
names = Symbol[]
196+
vals = Any[]
197+
inds = ObjectIdDict()
198+
for (k,v) in itr
199+
oldind = get(inds, k, 0)
200+
if oldind > 0
201+
vals[oldind] = v
202+
else
203+
push!(names, k)
204+
push!(vals, v)
205+
inds[k] = length(names)
206+
end
207+
end
208+
merge(a, NamedTuple{(names...,)}((vals...,)))
209+
end
210+
211+
keys(nt::NamedTuple{names}) where {names} = names
212+
values(nt::NamedTuple) = Tuple(nt)
213+
haskey(nt::NamedTuple, key::Union{Integer, Symbol}) = isdefined(nt, key)
214+
get(nt::NamedTuple, key::Union{Integer, Symbol}, default) = haskey(nt, key) ? getfield(nt, key) : default
215+
get(f::Callable, nt::NamedTuple, key::Union{Integer, Symbol}) = haskey(nt, key) ? getfield(nt, key) : f()

base/reflection.jl

+17-3
Original file line numberDiff line numberDiff line change
@@ -127,12 +127,14 @@ julia> fieldname(SparseMatrixCSC, 5)
127127
```
128128
"""
129129
function fieldname(t::DataType, i::Integer)
130-
n_fields = length(t.name.names)
130+
names = isdefined(t, :names) ? t.names : t.name.names
131+
n_fields = length(names)
131132
field_label = n_fields == 1 ? "field" : "fields"
132133
i > n_fields && throw(ArgumentError("Cannot access field $i since type $t only has $n_fields $field_label."))
133134
i < 1 && throw(ArgumentError("Field numbers must be positive integers. $i is invalid."))
134-
return t.name.names[i]::Symbol
135+
return names[i]::Symbol
135136
end
137+
136138
fieldname(t::UnionAll, i::Integer) = fieldname(unwrap_unionall(t), i)
137139
fieldname(t::Type{<:Tuple}, i::Integer) =
138140
i < 1 || i > fieldcount(t) ? throw(BoundsError(t, i)) : Int(i)
@@ -481,7 +483,19 @@ function fieldcount(@nospecialize t)
481483
if !(t isa DataType)
482484
throw(TypeError(:fieldcount, "", Type, t))
483485
end
484-
if t.abstract || (t.name === Tuple.name && isvatuple(t))
486+
if t.name === NamedTuple.body.body.name
487+
names, types = t.parameters
488+
if names isa Tuple
489+
return length(names)
490+
end
491+
if types isa DataType && types <: Tuple
492+
return fieldcount(types)
493+
end
494+
abstr = true
495+
else
496+
abstr = t.abstract || (t.name === Tuple.name && isvatuple(t))
497+
end
498+
if abstr
485499
error("type does not have a definite number of fields")
486500
end
487501
return length(t.types)

base/sysimg.jl

+4-1
Original file line numberDiff line numberDiff line change
@@ -142,6 +142,10 @@ Vector(m::Integer) = Array{Any,1}(Int(m))
142142
Matrix{T}(m::Integer, n::Integer) where {T} = Matrix{T}(Int(m), Int(n))
143143
Matrix(m::Integer, n::Integer) = Matrix{Any}(Int(m), Int(n))
144144

145+
include("associative.jl")
146+
147+
include("namedtuple.jl")
148+
145149
# numeric operations
146150
include("hashing.jl")
147151
include("rounding.jl")
@@ -175,7 +179,6 @@ include("reduce.jl")
175179
include("reshapedarray.jl")
176180
include("bitarray.jl")
177181
include("bitset.jl")
178-
include("associative.jl")
179182

180183
if !isdefined(Core, :Inference)
181184
include("docs/core.jl")

0 commit comments

Comments
 (0)