diff --git a/NEWS.md b/NEWS.md index 1da9e0e11c5494..c17d36b1f70213 100644 --- a/NEWS.md +++ b/NEWS.md @@ -61,6 +61,7 @@ New library functions New library features -------------------- * Function composition now works also on one argument `∘(f) = f` (#34251) +* `@NamedTuple{key1::Type1, ...}` macro for convenient `NamedTuple` declarations ([#34548]). * `isapprox` (or `≈`) now has a one-argument "curried" method `isapprox(x)` which returns a function, like `isequal` (or `==`)` ([#32305]). * `Ref{NTuple{N,T}}` can be passed to `Ptr{T}`/`Ref{T}` `ccall` signatures ([#34199]) diff --git a/base/exports.jl b/base/exports.jl index 2b428dae046f7c..7f2a8ab26b43d5 100644 --- a/base/exports.jl +++ b/base/exports.jl @@ -944,6 +944,7 @@ export @s_str, # regex substitution string @v_str, # version number @raw_str, # raw string with no interpolation/unescaping + @NamedTuple, # documentation @text_str, diff --git a/base/namedtuple.jl b/base/namedtuple.jl index 965466c71489ee..edbab586ea10f2 100644 --- a/base/namedtuple.jl +++ b/base/namedtuple.jl @@ -17,6 +17,8 @@ using [`values`](@ref). Iteration over `NamedTuple`s produces the *values* without the names. (See example below.) To iterate over the name-value pairs, use the [`pairs`](@ref) function. +The [`@NamedTuple`](@ref) macro can be used for conveniently declaring `NamedTuple` types. + # Examples ```jldoctest julia> x = (a=1, b=2) @@ -324,3 +326,38 @@ julia> Base.setindex(nt, "a", :a) function setindex(nt::NamedTuple, v, idx::Symbol) merge(nt, (; idx => v)) end + +""" + @NamedTuple{key1::Type1, key2::Type2, ...} + @NamedTuple begin key1::Type1; key2::Type2; ...; end + +This macro gives a more convenient syntax for declaring `NamedTuple` types. It returns a `NamedTuple` +type with the given keys and types, equivalent to `NamedTuple{(:key1, :key2, ...), Tuple{Type1,Type2,...}}`. +If the `::Type` declaration is omitted, it is taken to be `Any`. The `begin ... end` form allows the +declarations to be split across multiple lines (similar to a `struct` declaration), but is otherwise +equivalent. + +For example, the tuple `(a=3.1, b="hello")` has a type `NamedTuple{(:a, :b),Tuple{Float64,String}}`, which +can also be declared via `@NamedTuple` as: + +```jldoctest +julia> @NamedTuple{a::Float64, b::String} +NamedTuple{(:a, :b),Tuple{Float64,String}} + +julia> @NamedTuple begin + a::Float64 + b::String + end +NamedTuple{(:a, :b),Tuple{Float64,String}} +``` +""" +macro NamedTuple(ex) + Meta.isexpr(ex, :braces) || Meta.isexpr(ex, :block) || + throw(ArgumentError("@NamedTuple expects {...} or begin...end")) + decls = filter(e -> !(e isa LineNumberNode), ex.args) + all(e -> e isa Symbol || Meta.isexpr(e, :(::)), decls) || + throw(ArgumentError("@NamedTuple must contain a sequence of name or name::type expressions")) + vars = [QuoteNode(e isa Symbol ? e : e.args[1]) for e in decls] + types = [esc(e isa Symbol ? :Any : e.args[2]) for e in decls] + return :(NamedTuple{($(vars...),), Tuple{$(types...)}}) +end diff --git a/doc/src/base/base.md b/doc/src/base/base.md index fab114d5749c2c..1035ba519c5114 100644 --- a/doc/src/base/base.md +++ b/doc/src/base/base.md @@ -208,6 +208,7 @@ Union{} Core.UnionAll Core.Tuple Core.NamedTuple +Base.@NamedTuple Base.Val Core.Vararg Core.Nothing diff --git a/doc/src/manual/types.md b/doc/src/manual/types.md index c89bad59f6588a..af514aded09695 100644 --- a/doc/src/manual/types.md +++ b/doc/src/manual/types.md @@ -923,12 +923,26 @@ julia> typeof((a=1,b="hello")) NamedTuple{(:a, :b),Tuple{Int64,String}} ``` +The [`@NamedTuple`](@ref) macro provides a more convenient `struct`-like syntax for declaring +`NamedTuple` types via `key::Type` declarations, where an omitted `::Type` corresponds to `::Any`. + +```jldoctest +julia> @NamedTuple{a::Int, b::String} +NamedTuple{(:a, :b),Tuple{Int64,String}} + +julia> @NamedTuple begin + a::Int + b::String + end +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,"")) +julia> @NamedTuple{a::Float32,b::String}((1,"")) (a = 1.0f0, b = "") julia> NamedTuple{(:a, :b)}((1,"")) diff --git a/test/namedtuple.jl b/test/namedtuple.jl index 66bb823e6230a4..aaf8a6d2379978 100644 --- a/test/namedtuple.jl +++ b/test/namedtuple.jl @@ -276,3 +276,15 @@ let nt0 = NamedTuple(), nt1 = (a=33,), nt2 = (a=0, b=:v) @test Base.setindex(nt1, "value", :a) == (a="value",) @test Base.setindex(nt1, "value", :a) isa NamedTuple{(:a,),<:Tuple{AbstractString}} end + +# @NamedTuple +@testset "@NamedTuple" begin + @test @NamedTuple{a::Int, b::String} === NamedTuple{(:a, :b),Tuple{Int,String}} === + @NamedTuple begin + a::Int + b::String + end + @test @NamedTuple{a::Int, b} === NamedTuple{(:a, :b),Tuple{Int,Any}} + @test_throws LoadError include_string(Main, "@NamedTuple{a::Int, b, 3}") + @test_throws LoadError include_string(Main, "@NamedTuple(a::Int, b)") +end