Skip to content

Commit

Permalink
implement named tuples
Browse files Browse the repository at this point in the history
Based on #16580, also much work done by quinnj.

`(a=1, ...)` syntax is implemented, and `(; ...)` syntax is
implemented but not yet enabled.
  • Loading branch information
JeffBezanson committed Oct 26, 2017
1 parent cc87d82 commit c71ba5a
Show file tree
Hide file tree
Showing 23 changed files with 715 additions and 44 deletions.
3 changes: 3 additions & 0 deletions NEWS.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,9 @@ New language features
a function argument name, the argument is unpacked into local variables `x` and `y`
as in the assignment `(x, y) = arg` ([#6614]).

* Named tuples, with the syntax `(a=1, b=2)`. These behave very similarly to tuples,
except components can also be accessed by name using dot syntax `t.a` ([#22194]).

* Custom infix operators can now be defined by appending Unicode
combining marks, primes, and sub/superscripts to other operators.
For example, `+̂ₐ″` is parsed as an infix operator with the same
Expand Down
2 changes: 1 addition & 1 deletion base/boot.jl
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,7 @@ export
# key types
Any, DataType, Vararg, ANY, NTuple,
Tuple, Type, UnionAll, TypeName, TypeVar, Union, Void,
SimpleVector, AbstractArray, DenseArray,
SimpleVector, AbstractArray, DenseArray, NamedTuple,
# special objects
Function, CodeInfo, Method, MethodTable, TypeMapEntry, TypeMapLevel,
Module, Symbol, Task, Array, WeakRef, VecElement,
Expand Down
17 changes: 16 additions & 1 deletion base/inference.jl
Original file line number Diff line number Diff line change
Expand Up @@ -493,6 +493,8 @@ end
const _Type_name = Type.body.name
isType(@nospecialize t) = isa(t, DataType) && (t::DataType).name === _Type_name

const _NamedTuple_name = NamedTuple.body.body.name

# true if Type is inlineable as constant (is a singleton)
function isconstType(@nospecialize t)
isType(t) || return false
Expand Down Expand Up @@ -734,6 +736,10 @@ function isdefined_tfunc(args...)
end
if 1 <= idx <= a1.ninitialized
return Const(true)
elseif a1.name === _NamedTuple_name
if isleaftype(a1)
return Const(false)
end
elseif idx <= 0 || (!isvatuple(a1) && idx > fieldcount(a1))
return Const(false)
elseif !isvatuple(a1) && isbits(fieldtype(a1, idx))
Expand Down Expand Up @@ -771,7 +777,9 @@ add_tfunc(nfields, 1, 1,
# TODO: remove with deprecation in builtins.c for nfields(::Type)
isleaftype(x.parameters[1]) && return Const(old_nfields(x.parameters[1]))
elseif isa(x,DataType) && !x.abstract && !(x.name === Tuple.name && isvatuple(x)) && x !== DataType
return Const(length(x.types))
if !(x.name === _NamedTuple_name && !isleaftype(x))
return Const(length(x.types))
end
end
return Int
end, 0)
Expand Down Expand Up @@ -1333,6 +1341,10 @@ function getfield_tfunc(@nospecialize(s00), @nospecialize(name))
end
return Any
end
if s.name === _NamedTuple_name && !isleaftype(s)
# TODO: better approximate inference
return Any
end
if isempty(s.types)
return Bottom
end
Expand Down Expand Up @@ -1416,6 +1428,9 @@ function fieldtype_tfunc(@nospecialize(s0), @nospecialize(name))
if !isa(u,DataType) || u.abstract
return Type
end
if u.name === _NamedTuple_name && !isleaftype(u)
return Type
end
ftypes = u.types
if isempty(ftypes)
return Bottom
Expand Down
215 changes: 215 additions & 0 deletions base/namedtuple.jl
Original file line number Diff line number Diff line change
@@ -0,0 +1,215 @@
# This file is a part of Julia. License is MIT: https://julialang.org/license

"""
NamedTuple{names,T}(args::Tuple)
Construct a named tuple with the given `names` (a tuple of Symbols) and field types `T`
(a `Tuple` type) from a tuple of values.
"""
function NamedTuple{names,T}(args::Tuple) where {names, T <: Tuple}
if length(args) == length(names)
if @generated
N = length(names)
types = T.parameters
Expr(:new, :(NamedTuple{names,T}), Any[ :(convert($(types[i]), args[$i])) for i in 1:N ]...)
else
N = length(names)
NT = NamedTuple{names,T}
types = T.parameters
fields = Any[ convert(types[i], args[i]) for i = 1:N ]
ccall(:jl_new_structv, Any, (Any, Ptr{Void}, UInt32), NT, fields, N)::NT
end
else
throw(ArgumentError("Wrong number of arguments to named tuple constructor."))
end
end

"""
NamedTuple{names}(args::Tuple)
Construct a named tuple with the given `names` (a tuple of Symbols) from a tuple of
values.
"""
function NamedTuple{names}(args::Tuple) where {names}
NamedTuple{names,typeof(args)}(args)
end

"""
NamedTuple{names}(nt::NamedTuple)
Construct a named tuple by selecting fields in `names` (a tuple of Symbols) from
another named tuple.
"""
function NamedTuple{names}(nt::NamedTuple) where {names}
if @generated
types = Tuple{(fieldtype(nt, n) for n in names)...}
Expr(:new, :(NamedTuple{names, $types}), Any[ :(getfield(nt, $(QuoteNode(n)))) for n in names ]...)
else
types = Tuple{(fieldtype(typeof(nt), n) for n in names)...}
NamedTuple{names, types}(Tuple(getfield(nt, n) for n in names))
end
end

NamedTuple() = NamedTuple{(),Tuple{}}(())

length(t::NamedTuple) = nfields(t)
start(t::NamedTuple) = 1
done(t::NamedTuple, iter) = iter > nfields(t)
next(t::NamedTuple, iter) = (getfield(t, iter), iter + 1)
endof(t::NamedTuple) = nfields(t)
getindex(t::NamedTuple, i::Int) = getfield(t, i)
getindex(t::NamedTuple, i::Symbol) = getfield(t, i)
indexed_next(t::NamedTuple, i::Int, state) = (getfield(t, i), i+1)

convert(::Type{NamedTuple{names,T}}, nt::NamedTuple{names,T}) where {names,T} = nt
convert(::Type{NamedTuple{names}}, nt::NamedTuple{names}) where {names} = nt

function convert(::Type{NamedTuple{names,T}}, nt::NamedTuple{names}) where {names,T}
NamedTuple{names,T}(T(nt))
end

function show(io::IO, t::NamedTuple)
n = nfields(t)
for i = 1:n
# if field types aren't concrete, show full type
if typeof(getfield(t, i)) !== fieldtype(typeof(t), i)
show(io, typeof(t))
print(io, "(")
show(io, Tuple(t))
print(io, ")")
return
end
end
if n == 0
print(io, "NamedTuple()")
else
print(io, "(")
for i = 1:n
print(io, fieldname(typeof(t),i), " = "); show(io, getfield(t, i))
if n == 1
print(io, ",")
elseif i < n
print(io, ", ")
end
end
print(io, ")")
end
end

eltype(::Type{NamedTuple{names,T}}) where {names,T} = eltype(T)

==(a::NamedTuple{n}, b::NamedTuple{n}) where {n} = Tuple(a) == Tuple(b)
==(a::NamedTuple, b::NamedTuple) = false

isequal(a::NamedTuple{n}, b::NamedTuple{n}) where {n} = isequal(Tuple(a), Tuple(b))
isequal(a::NamedTuple, b::NamedTuple) = false

_nt_names(::NamedTuple{names}) where {names} = names
_nt_names(::Type{T}) where {names,T<:NamedTuple{names}} = names

hash(x::NamedTuple, h::UInt) = xor(object_id(_nt_names(x)), hash(Tuple(x), h))

isless(a::NamedTuple{n}, b::NamedTuple{n}) where {n} = isless(Tuple(a), Tuple(b))
# TODO: case where one argument's names are a prefix of the other's

same_names(::NamedTuple{names}...) where {names} = true
same_names(::NamedTuple...) = false

function map(f, nt::NamedTuple{names}, nts::NamedTuple...) where names
if !same_names(nt, nts...)
throw(ArgumentError("Named tuple names do not match."))
end
# this method makes sure we don't define a map(f) method
NT = NamedTuple{names}
if @generated
N = length(names)
M = length(nts)
args = Expr[:(f($(Expr[:(getfield(nt, $j)), (:(getfield(nts[$i], $j)) for i = 1:M)...]...))) for j = 1:N]
:( NT(($(args...),)) )
else
NT(map(f, map(Tuple, (nt, nts...))...))
end
end

# a version of `in` for the older world these generated functions run in
@pure function sym_in(x::Symbol, itr::Tuple{Vararg{Symbol}})
for y in itr
y === x && return true
end
return false
end

@pure function merge_names(an::Tuple{Vararg{Symbol}}, bn::Tuple{Vararg{Symbol}})
names = Symbol[an...]
for n in bn
if !sym_in(n, an)
push!(names, n)
end
end
(names...,)
end

@pure function merge_types(names::Tuple{Vararg{Symbol}}, a::Type{<:NamedTuple}, b::Type{<:NamedTuple})
bn = _nt_names(b)
Tuple{Any[ fieldtype(sym_in(n, bn) ? b : a, n) for n in names ]...}
end

"""
merge(a::NamedTuple, b::NamedTuple)
Construct a new named tuple by merging two existing ones.
The order of fields in `a` is preserved, but values are taken from matching
fields in `b`. Fields present only in `b` are appended at the end.
```jldoctest
julia> merge((a=1, b=2, c=3), (b=4, d=5))
(a = 1, b = 4, c = 3, d = 5)
```
"""
function merge(a::NamedTuple{an}, b::NamedTuple{bn}) where {an, bn}
if @generated
names = merge_names(an, bn)
types = merge_types(names, a, b)
vals = Any[ :(getfield($(sym_in(n, bn) ? :b : :a), $(QuoteNode(n)))) for n in names ]
:( NamedTuple{$names,$types}(($(vals...),)) )
else
names = merge_names(an, bn)
types = merge_types(names, typeof(a), typeof(b))
NamedTuple{names,types}(map(n->getfield(sym_in(n, bn) ? b : a, n), names))
end
end

merge(a::NamedTuple{()}, b::NamedTuple) = b

"""
merge(a::NamedTuple, iterable)
Interpret an iterable of key-value pairs as a named tuple, and perform a merge.
```jldoctest
julia> merge((a=1, b=2, c=3), [:b=>4, :d=>5])
(a = 1, b = 4, c = 3, d = 5)
```
"""
function merge(a::NamedTuple, itr)
names = Symbol[]
vals = Any[]
inds = ObjectIdDict()
for (k,v) in itr
oldind = get(inds, k, 0)
if oldind > 0
vals[oldind] = v
else
push!(names, k)
push!(vals, v)
inds[k] = length(names)
end
end
merge(a, NamedTuple{(names...,)}((vals...,)))
end

keys(nt::NamedTuple{names}) where {names} = names
values(nt::NamedTuple) = Tuple(nt)
haskey(nt::NamedTuple, key::Union{Integer, Symbol}) = isdefined(nt, key)
get(nt::NamedTuple, key::Union{Integer, Symbol}, default) = haskey(nt, key) ? getfield(nt, key) : default
get(f::Callable, nt::NamedTuple, key::Union{Integer, Symbol}) = haskey(nt, key) ? getfield(nt, key) : f()
20 changes: 17 additions & 3 deletions base/reflection.jl
Original file line number Diff line number Diff line change
Expand Up @@ -127,12 +127,14 @@ julia> fieldname(SparseMatrixCSC, 5)
```
"""
function fieldname(t::DataType, i::Integer)
n_fields = length(t.name.names)
names = isdefined(t, :names) ? t.names : t.name.names
n_fields = length(names)
field_label = n_fields == 1 ? "field" : "fields"
i > n_fields && throw(ArgumentError("Cannot access field $i since type $t only has $n_fields $field_label."))
i < 1 && throw(ArgumentError("Field numbers must be positive integers. $i is invalid."))
return t.name.names[i]::Symbol
return names[i]::Symbol
end

fieldname(t::UnionAll, i::Integer) = fieldname(unwrap_unionall(t), i)
fieldname(t::Type{<:Tuple}, i::Integer) =
i < 1 || i > fieldcount(t) ? throw(BoundsError(t, i)) : Int(i)
Expand Down Expand Up @@ -481,7 +483,19 @@ function fieldcount(@nospecialize t)
if !(t isa DataType)
throw(TypeError(:fieldcount, "", Type, t))
end
if t.abstract || (t.name === Tuple.name && isvatuple(t))
if t.name === NamedTuple.body.body.name
names, types = t.parameters
if names isa Tuple
return length(names)
end
if types isa DataType && types <: Tuple
return fieldcount(types)
end
abstr = true
else
abstr = t.abstract || (t.name === Tuple.name && isvatuple(t))
end
if abstr
error("type does not have a definite number of fields")
end
return length(t.types)
Expand Down
5 changes: 4 additions & 1 deletion base/sysimg.jl
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,10 @@ Vector(m::Integer) = Array{Any,1}(Int(m))
Matrix{T}(m::Integer, n::Integer) where {T} = Matrix{T}(Int(m), Int(n))
Matrix(m::Integer, n::Integer) = Matrix{Any}(Int(m), Int(n))

include("associative.jl")

include("namedtuple.jl")

# numeric operations
include("hashing.jl")
include("rounding.jl")
Expand Down Expand Up @@ -175,7 +179,6 @@ include("reduce.jl")
include("reshapedarray.jl")
include("bitarray.jl")
include("intset.jl")
include("associative.jl")

if !isdefined(Core, :Inference)
include("docs/core.jl")
Expand Down
Loading

0 comments on commit c71ba5a

Please sign in to comment.