Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add NamedTuples #22194

Merged
merged 1 commit into from
Nov 2, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this just be T? Otherwise, it just always returns Any, right?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh wait, nvm, this is correct for iteration protocol.


==(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)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is not preserving of the order of the values. Need to switch an and bn in the implementation:

names = Symbol[]
for n in an
    if !sym_in(n, bn)
        push!(names, n)
    end
end
append!(names, bn)
return names

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think either behavior could be useful but I went with how merge works for ordered dicts and keyword arguments.

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}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This still bothers me that merge of two namedtuples is defined to behave differently from merge of a namedtuple and another type of iterator. I think these should be two different functions (merge and mergepairs, perhaps?)

Additionally, these methods should be written to preserve the input types (such that merge(a, ∅) === a ∀ a)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agree re: element types.

merge could be defined to call pairs on the second argument; then the two would match.

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
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same here

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("bitset.jl")
include("associative.jl")

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