From 82bdb7a2ed683fb366cde06c56338d31db394e2c Mon Sep 17 00:00:00 2001 From: Andy Ferris Date: Thu, 14 Dec 2017 22:07:15 +1000 Subject: [PATCH 1/6] Add `only` function The function `only(x)` returns the one-and-only element of a collection `x`, or else throws an error. --- NEWS.md | 3 ++- base/Base.jl | 2 +- base/exports.jl | 1 + base/iterators.jl | 29 ++++++++++++++++++++++++++++- doc/src/base/iterators.md | 1 + test/iterators.jl | 22 ++++++++++++++++++++++ 6 files changed, 55 insertions(+), 3 deletions(-) diff --git a/NEWS.md b/NEWS.md index 2ff018ce96ce4..90f35fae80a6e 100644 --- a/NEWS.md +++ b/NEWS.md @@ -32,8 +32,9 @@ Standard library changes * The methods of `mktemp` and `mktempdir` which take a function body to pass temporary paths to no longer throw errors if the path is already deleted when the function body returns ([#33091]). -#### Libdl +* New function `only(x)` returns the one-and-only element of a collection `x`, and throws an error if `x` contains zero or multiple elements. +#### Libdl #### LinearAlgebra diff --git a/base/Base.jl b/base/Base.jl index f64c061f9e9fb..9a19e02672a17 100644 --- a/base/Base.jl +++ b/base/Base.jl @@ -138,7 +138,7 @@ include("ntuple.jl") include("abstractdict.jl") include("iterators.jl") -using .Iterators: zip, enumerate +using .Iterators: zip, enumerate, only using .Iterators: Flatten, Filter, product # for generators include("namedtuple.jl") diff --git a/base/exports.jl b/base/exports.jl index ebd746392558a..49063720c14c4 100644 --- a/base/exports.jl +++ b/base/exports.jl @@ -628,6 +628,7 @@ export enumerate, # re-exported from Iterators zip, + only, # object identity and equality copy, diff --git a/base/iterators.jl b/base/iterators.jl index bebe8ac1701f1..764df4e175a22 100644 --- a/base/iterators.jl +++ b/base/iterators.jl @@ -929,7 +929,6 @@ julia> collect(Iterators.partition([1,2,3,4,5], 2)) """ partition(c::T, n::Integer) where {T} = PartitionIterator{T}(c, Int(n)) - struct PartitionIterator{T} c::T n::Int @@ -1095,4 +1094,32 @@ eltype(::Type{Stateful{T, VS}} where VS) where {T} = eltype(T) IteratorEltype(::Type{Stateful{T,VS}}) where {T,VS} = IteratorEltype(T) length(s::Stateful) = length(s.itr) - s.taken +""" + only(x) + +Returns the one and only element of collection `x`, and throws an error if the collection +has zero or multiple elements. +""" +Base.@propagate_inbounds function only(x) + i = start(x) + @boundscheck if done(x, i) + error("Collection is empty, must contain exactly 1 element") + end + (ret, i) = next(x, i) + @boundscheck if !done(x, i) + error("Collection has multiple elements, must contain exactly 1 element") + end + return ret +end + +# Collections of known size +only(x::Tuple{}) = error("Tuple is empty, must contain exactly 1 element") +only(x::Tuple{Any}) = x[1] +only(x::Tuple) = error("Tuple contains $(length(x)) elements, must contain exactly 1 element") + +only(a::AbstractArray{<:Any, 0}) = @inbounds return a[] + +only(x::NamedTuple{<:Any, <:Tuple{Any}}) = first(x) +only(x::NamedTuple) = error("NamedTuple contains $(length(x)) elements, must contain exactly 1 element") + end diff --git a/doc/src/base/iterators.md b/doc/src/base/iterators.md index 7f89b792117cc..0b63561ebdc32 100644 --- a/doc/src/base/iterators.md +++ b/doc/src/base/iterators.md @@ -15,4 +15,5 @@ Base.Iterators.flatten Base.Iterators.partition Base.Iterators.filter Base.Iterators.reverse +Base.Iterators.only ``` diff --git a/test/iterators.jl b/test/iterators.jl index 352a0ec8404d6..d98be58286898 100644 --- a/test/iterators.jl +++ b/test/iterators.jl @@ -647,3 +647,25 @@ end @test length(collect(d)) == 2 @test length(collect(d)) == 0 end + +@testset "only" begin + @test only([3]) === 3 + @test_throws ErrorException only([]) + @test_throws ErrorException only([3, 2]) + + @test @inferred(only((3,))) === 3 + @test_throws ErrorException only(()) + @test_throws ErrorException only((3, 2)) + + @test only(Dict(1=>3)) === (1=>3) + @test_throws ErrorException only(Dict{Int,Int}()) + @test_throws ErrorException only(Dict(1=>3, 2=>2)) + + @test only(Set([3])) === 3 + @test_throws ErrorException only(Set(Int[])) + @test_throws ErrorException only(Set([3,2])) + + @test @inferred(only((;a=1))) === 1 + @test_throws ErrorException only(NamedTuple()) + @test_throws ErrorException only((a=3, b=2.0)) +end From 3218d9d4aa6bfa569488570918351b5d7d8e0aa2 Mon Sep 17 00:00:00 2001 From: Nick Robinson Date: Sat, 31 Aug 2019 11:22:24 +0100 Subject: [PATCH 2/6] Update to new iteration interface --- base/iterators.jl | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/base/iterators.jl b/base/iterators.jl index 764df4e175a22..0bc81024d6598 100644 --- a/base/iterators.jl +++ b/base/iterators.jl @@ -1101,12 +1101,12 @@ Returns the one and only element of collection `x`, and throws an error if the c has zero or multiple elements. """ Base.@propagate_inbounds function only(x) - i = start(x) - @boundscheck if done(x, i) + i = iterate(x) + @boundscheck if i === nothing error("Collection is empty, must contain exactly 1 element") end - (ret, i) = next(x, i) - @boundscheck if !done(x, i) + (ret, state) = i + @boundscheck if iterate(x, state) !== nothing error("Collection has multiple elements, must contain exactly 1 element") end return ret From 14ed6ddda8d4bbcf4f4bbb74ee879246a6614d61 Mon Sep 17 00:00:00 2001 From: Nick Robinson Date: Sat, 31 Aug 2019 11:45:47 +0100 Subject: [PATCH 3/6] Throw ArgumentError not plain ErrorException --- base/iterators.jl | 20 +++++++++++--------- test/iterators.jl | 22 ++++++++++------------ 2 files changed, 21 insertions(+), 21 deletions(-) diff --git a/base/iterators.jl b/base/iterators.jl index 0bc81024d6598..f3c366597dd59 100644 --- a/base/iterators.jl +++ b/base/iterators.jl @@ -1097,29 +1097,31 @@ length(s::Stateful) = length(s.itr) - s.taken """ only(x) -Returns the one and only element of collection `x`, and throws an error if the collection -has zero or multiple elements. +Returns the one and only element of collection `x`, and throws an `ArgumentError` if the +collection has zero or multiple elements. """ Base.@propagate_inbounds function only(x) i = iterate(x) @boundscheck if i === nothing - error("Collection is empty, must contain exactly 1 element") + throw(ArgumentError("Collection is empty, must contain exactly 1 element")) end (ret, state) = i @boundscheck if iterate(x, state) !== nothing - error("Collection has multiple elements, must contain exactly 1 element") + throw(ArgumentError("Collection has multiple elements, must contain exactly 1 element")) end return ret end # Collections of known size -only(x::Tuple{}) = error("Tuple is empty, must contain exactly 1 element") +only(x::Tuple{}) = throw(ArgumentError("Tuple is empty, must contain exactly 1 element")) only(x::Tuple{Any}) = x[1] -only(x::Tuple) = error("Tuple contains $(length(x)) elements, must contain exactly 1 element") - +only(x::Tuple) = throw( + ArgumentError("Tuple contains $(length(x)) elements, must contain exactly 1 element") +) only(a::AbstractArray{<:Any, 0}) = @inbounds return a[] - only(x::NamedTuple{<:Any, <:Tuple{Any}}) = first(x) -only(x::NamedTuple) = error("NamedTuple contains $(length(x)) elements, must contain exactly 1 element") +only(x::NamedTuple) = throw( + ArgumentError("NamedTuple contains $(length(x)) elements, must contain exactly 1 element") +) end diff --git a/test/iterators.jl b/test/iterators.jl index d98be58286898..0c0f5925c43b5 100644 --- a/test/iterators.jl +++ b/test/iterators.jl @@ -183,7 +183,6 @@ end @test Base.IteratorEltype(repeated(0, 5)) == Base.HasEltype() @test Base.IteratorSize(zip(repeated(0), repeated(0))) == Base.IsInfinite() - # product # ------- @@ -411,7 +410,6 @@ for n in [5,6] [(1,1),(2,2),(3,3),(4,4),(5,5)] end - @test join(map(x->string(x...), partition("Hello World!", 5)), "|") == "Hello| Worl|d!" @@ -650,22 +648,22 @@ end @testset "only" begin @test only([3]) === 3 - @test_throws ErrorException only([]) - @test_throws ErrorException only([3, 2]) + @test_throws ArgumentError only([]) + @test_throws ArgumentError only([3, 2]) @test @inferred(only((3,))) === 3 - @test_throws ErrorException only(()) - @test_throws ErrorException only((3, 2)) + @test_throws ArgumentError only(()) + @test_throws ArgumentError only((3, 2)) @test only(Dict(1=>3)) === (1=>3) - @test_throws ErrorException only(Dict{Int,Int}()) - @test_throws ErrorException only(Dict(1=>3, 2=>2)) + @test_throws ArgumentError only(Dict{Int,Int}()) + @test_throws ArgumentError only(Dict(1=>3, 2=>2)) @test only(Set([3])) === 3 - @test_throws ErrorException only(Set(Int[])) - @test_throws ErrorException only(Set([3,2])) + @test_throws ArgumentError only(Set(Int[])) + @test_throws ArgumentError only(Set([3,2])) @test @inferred(only((;a=1))) === 1 - @test_throws ErrorException only(NamedTuple()) - @test_throws ErrorException only((a=3, b=2.0)) + @test_throws ArgumentError only(NamedTuple()) + @test_throws ArgumentError only((a=3, b=2.0)) end From 694604d8303278af9a0987a70aceb26ba22af2fc Mon Sep 17 00:00:00 2001 From: Nick Robinson Date: Sat, 31 Aug 2019 14:20:55 +0100 Subject: [PATCH 4/6] Add more tests for `only` --- test/iterators.jl | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/test/iterators.jl b/test/iterators.jl index 0c0f5925c43b5..73f891a718565 100644 --- a/test/iterators.jl +++ b/test/iterators.jl @@ -666,4 +666,10 @@ end @test @inferred(only((;a=1))) === 1 @test_throws ArgumentError only(NamedTuple()) @test_throws ArgumentError only((a=3, b=2.0)) + + @test only(1 for ii in 1:1) === 1 + @test only(1 for ii in 1:10 if ii < 2) === 1 + @test_throws ArgumentError only(1 for ii in 1:10) + @test_throws ArgumentError only(1 for ii in 1:10 if ii > 2) + @test_throws ArgumentError only(1 for ii in 1:10 if ii > 200) end From e2d543a0b01e3e044d0ecbb0d57a5b392a647cd1 Mon Sep 17 00:00:00 2001 From: Nick Robinson Date: Sat, 31 Aug 2019 14:24:21 +0100 Subject: [PATCH 5/6] Make `only` constant fold more inputs --- base/iterators.jl | 3 +++ test/iterators.jl | 4 ++++ 2 files changed, 7 insertions(+) diff --git a/base/iterators.jl b/base/iterators.jl index f3c366597dd59..4baa5ac0d7006 100644 --- a/base/iterators.jl +++ b/base/iterators.jl @@ -1113,6 +1113,9 @@ Base.@propagate_inbounds function only(x) end # Collections of known size +only(x::Ref) = x[] +only(x::Number) = x +only(x::Char) = x only(x::Tuple{}) = throw(ArgumentError("Tuple is empty, must contain exactly 1 element")) only(x::Tuple{Any}) = x[1] only(x::Tuple) = throw( diff --git a/test/iterators.jl b/test/iterators.jl index 73f891a718565..25c7c50483d04 100644 --- a/test/iterators.jl +++ b/test/iterators.jl @@ -667,6 +667,10 @@ end @test_throws ArgumentError only(NamedTuple()) @test_throws ArgumentError only((a=3, b=2.0)) + @test @inferred(only(1)) === 1 + @test @inferred(only('a')) === 'a' + @test @inferred(only(Ref([1, 2]))) == [1, 2] + @test only(1 for ii in 1:1) === 1 @test only(1 for ii in 1:10 if ii < 2) === 1 @test_throws ArgumentError only(1 for ii in 1:10) From 9235f4431c50b4b94749f05a82d0240e7a96a0fe Mon Sep 17 00:00:00 2001 From: Nick Robinson Date: Sat, 31 Aug 2019 16:28:31 +0100 Subject: [PATCH 6/6] Import `@boundscheck` and `@inbounds` from Base --- base/iterators.jl | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/base/iterators.jl b/base/iterators.jl index 4baa5ac0d7006..6f87120965957 100644 --- a/base/iterators.jl +++ b/base/iterators.jl @@ -12,7 +12,7 @@ using .Base: @inline, Pair, AbstractDict, IndexLinear, IndexCartesian, IndexStyle, AbstractVector, Vector, tail, tuple_type_head, tuple_type_tail, tuple_type_cons, SizeUnknown, HasLength, HasShape, IsInfinite, EltypeUnknown, HasEltype, OneTo, @propagate_inbounds, Generator, AbstractRange, - LinearIndices, (:), |, +, -, !==, !, <=, <, missing, map, any + LinearIndices, (:), |, +, -, !==, !, <=, <, missing, map, any, @boundscheck, @inbounds import .Base: first, last, @@ -1100,7 +1100,7 @@ length(s::Stateful) = length(s.itr) - s.taken Returns the one and only element of collection `x`, and throws an `ArgumentError` if the collection has zero or multiple elements. """ -Base.@propagate_inbounds function only(x) +@propagate_inbounds function only(x) i = iterate(x) @boundscheck if i === nothing throw(ArgumentError("Collection is empty, must contain exactly 1 element"))