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 25, 2017
1 parent 80a2c2f commit 43d2099
Show file tree
Hide file tree
Showing 22 changed files with 624 additions and 42 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
178 changes: 178 additions & 0 deletions base/namedtuple.jl
Original file line number Diff line number Diff line change
@@ -0,0 +1,178 @@
# This file is a part of Julia. License is MIT: https://julialang.org/license

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}
ccall(:jl_new_structv, Any, (Any, Ptr{Void}, UInt32), NT, collect(Any, T(args)), N)::NT
end
else
throw(ArgumentError("Wrong number of arguments to named tuple constructor."))
end
end

function NamedTuple{names}(args::Tuple) where {names}
NamedTuple{names,typeof(args)}(args)
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), $(Expr(:quote, 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[]
for (k,v) in itr
push!(names, k)
push!(vals, v)
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()
6 changes: 4 additions & 2 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
2 changes: 2 additions & 0 deletions base/sysimg.jl
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,8 @@ 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("namedtuple.jl")

# numeric operations
include("hashing.jl")
include("rounding.jl")
Expand Down
50 changes: 46 additions & 4 deletions doc/src/manual/functions.md
Original file line number Diff line number Diff line change
Expand Up @@ -221,6 +221,48 @@ A zero-argument anonymous function is written as `()->3`. The idea of a function
may seem strange, but is useful for "delaying" a computation. In this usage, a block of code is
wrapped in a zero-argument function, which is later invoked by calling it as `f`.

## Tuples

Julia has a built-in data structure called a *tuple* that is closely related to function
arguments and return values.
A tuple is a fixed-length container that can hold any values, but cannot be modified
(it is *immutable*).
Tuples are constructed with commas and parentheses, and can be accessed via indexing:

```jldoctest
julia> (1, 1+1)
(1, 2)
julia> (1,)
(1,)
julia> x = (0.0, "hello", 6*7)
(0.0, "hello", 42)
julia> x[2]
"hello"
```

Notice that a length-1 tuple must be written with a comma, `(1,)`, since `(1)` would just
be a parenthesized value.
`()` represents the empty (length-0) tuple.

## Named Tuples

The components of tuples can optionally be named, in which case a *named tuple* is
constructed:

```jldoctest
julia> x = (a=1, b=1+1)
(a = 1, b = 2)
julia> x.a
1
```

Named tuples are very similar to tuples, except that fields can additionally be accessed by name
using dot syntax (`x.a`).

## Multiple Return Values

In Julia, one returns a tuple of values to simulate returning multiple values. However, tuples
Expand Down Expand Up @@ -320,7 +362,7 @@ In all these cases, `x` is bound to a tuple of the trailing values passed to `ba
It is possible to constrain the number of values passed as a variable argument; this will be discussed
later in [Parametrically-constrained Varargs methods](@ref).

On the flip side, it is often handy to "splice" the values contained in an iterable collection
On the flip side, it is often handy to "splat" the values contained in an iterable collection
into a function call as individual arguments. To do this, one also uses `...` but in the function
call instead:

Expand Down Expand Up @@ -349,7 +391,7 @@ julia> bar(x...)
(1, 2, (3, 4))
```

Furthermore, the iterable object spliced into a function call need not be a tuple:
Furthermore, the iterable object splatted into a function call need not be a tuple:

```jldoctest barfunc
julia> x = [3,4]
Expand All @@ -371,7 +413,7 @@ julia> bar(x...)
(1, 2, (3, 4))
```

Also, the function that arguments are spliced into need not be a varargs function (although it
Also, the function that arguments are splatted into need not be a varargs function (although it
often is):

```jldoctest
Expand All @@ -397,7 +439,7 @@ Closest candidates are:
baz(::Any, ::Any) at none:1
```

As you can see, if the wrong number of elements are in the spliced container, then the function
As you can see, if the wrong number of elements are in the splatted container, then the function
call will fail, just as it would if too many arguments were given explicitly.

## Optional Arguments
Expand Down
25 changes: 25 additions & 0 deletions doc/src/manual/types.md
Original file line number Diff line number Diff line change
Expand Up @@ -905,6 +905,31 @@ used to represent the arguments accepted by varargs methods (see [Varargs Functi
The type `Vararg{T,N}` corresponds to exactly `N` elements of type `T`. `NTuple{N,T}` is a convenient
alias for `Tuple{Vararg{T,N}}`, i.e. a tuple type containing exactly `N` elements of type `T`.

### Named Tuple Types

Named tuples are instances of the `NamedTuple` type, which has two parameters: a tuple of
symbols giving the field names, and a tuple type giving the field types.

```jldoctest
julia> typeof((a=1,b="hello"))
NamedTuple{(:a, :b),Tuple{Int64,String}}
```

A `NamedTuple` type can be used as a constructor, accepting a single tuple argument.
The constructed `NamedTuple` type can be either a concrete type, with both parameters specified,
or a type that specifies only field names:

```jldoctest
julia> NamedTuple{(:a, :b),Tuple{Float32, String}}((1,""))
(a = 1.0f0, b = "")
julia> NamedTuple{(:a, :b)}((1,""))
(a = 1, b = "")
```

If field types are specified, the arguments are converted. Otherwise the types of the arguments
are used directly.

#### [Singleton Types](@id man-singleton-types)

There is a special kind of abstract parametric type that must be mentioned here: singleton types.
Expand Down
Loading

0 comments on commit 43d2099

Please sign in to comment.