JuliaLang / julia

The Julia Programming Language
https://julialang.org/
MIT License
45.13k stars 5.43k forks source link

inconsistent truncating behavior of converting `<:Tuple` constructors #52657

Open nsajko opened 7 months ago

nsajko commented 7 months ago
julia> Tuple{Int}([3, 4])
(3,)

julia> Tuple{Int}((3, 4))
ERROR: MethodError: Cannot `convert` an object of type 
  Tuple{Int64{},Int64} to an object of type 
  Tuple{Int64{}}

  ...

Stacktrace:
 [1] _tuple_error(T::Type, x::Tuple{Int64, Int64})
   @ Base ./essentials.jl:409
 [2] convert(#unused#::Type{Tuple{Int64}}, x::Tuple{Int64, Int64})
   @ Base ./essentials.jl:416
 [3] Tuple{Int64}(x::Tuple{Int64, Int64})
   @ Base ./tuple.jl:364
 [4] top-level scope
   @ REPL[1]:1

julia> versioninfo()
Julia Version 1.9.4
Commit 8e5136fa297 (2023-11-14 08:46 UTC)
Build Info:
  Official https://julialang.org/ release
Platform Info:
  OS: Linux (x86_64-linux-gnu)
  CPU: 8 × AMD Ryzen 3 5300U with Radeon Graphics
  WORD_SIZE: 64
  LIBM: libopenlibm
  LLVM: libLLVM-14.0.6 (ORCJIT, znver2)
  Threads: 1 on 8 virtual cores
Environment:
  JULIA_NUM_PRECOMPILE_TASKS = 3
  JULIA_PKG_PRECOMPILE_AUTO = 0

It doesn't seem right for Tuple{Int}([3, 4]) to succeed (giving a truncated output, however), while Tuple{Int}((3, 4)) throws. IMO ideally both calls would throw, but I'm not sure if making that change would violate backwards compatibility.

The problem with Tuple{Int}([3, 4]) == (3,) is that (3,) == [3, 4] doesn't hold, in case it's not clear.

This is similarly silly: Tuple{}(1:10) == ().

What does any relevant documentation on constructors say? EDIT: there's no relevant documentation as far as I can tell, so I guess it wouldn't be breaking to make Tuple{Int}([3, 4]) and Tuple{}(1:10) throw.

AmanDoesntCode commented 7 months ago

Hey there, I found a probable solution to your problem. The purpose of use behind the syntax is incorrect. Using the block delimeters "{}" after the "Tuple" keyword is to define the datatype of each element one by one in the tuple rather than defining the datatype of the whole tuple.

when you use :

julia> Tuple{Int}([3, 4])
output : (3,)  

This happens because an array is a mutable type container and therefore it somewhat ignores the element that doesn't have any datatype defined for it in the core.tuple function.

when you use:

julia> Tuple{Int}((3, 4))
ERROR: MethodError: Cannot `convert` an object of type Tuple{Int64,Int64} to an object of type Tuple{Int64} 

This happens because a Tuple is a non-mutable type container and therefore doesn't ignore the element that doesn't have any datatype defined resulting it into throwing a MethodError for a faulty conversion.

Use the syntax like this instead:

julia> Tuple{Int, Int}((3, 4))
(3, 4)

julia> Tuple{Int, Int}([3, 4])
(3, 4)

julia> Tuple{Int, String}([3, "Aman"])
(3, "Aman")

this feature is present so that, if you used any other datatype in the tuple you could define it. It is said in the documentation "A tuple is a fixed-length container that can hold any values of different types, but cannot be modified"

Hope this helps :) . Documentation source here

DilumAluthge commented 7 months ago

Yeah, it would make sense to me that both should throw.

jakobnissen commented 6 months ago

Related: https://github.com/JuliaLang/julia/issues/40495 Not quite a duplicate, because a resolution to this issue could be to allow the latter case.

tpapp commented 6 months ago

I ran into this with NTuples (#52963, marked as a duplicate, thanks!) and I also agree that it should throw. Specifically, if the argument supports length (iterator, ::AbstractVector), length should match the number of elements in the Tuple.

But what to do about infinite iterators (IsInfinite())? Should they just silently take the first N elements, which is what currently happens? I think it would be cleaner to ask the user to Iterators.take first, but that may be breaking.

nsajko commented 6 months ago

IMO, an infinite iterator is longer than any tuple, so trying to construct any type of tuple from an infinite iterator would ideally throw.

nsajko commented 6 months ago

Should the exception be ArgumentError, DimensionMismatch, InexactError or a custom exception type?

tpapp commented 6 months ago

AFAIK DimensionMismatch is inter-argument mismatch. I would go with ArgumentError and an informative message.

I agree about infinite objects throwing.

nsajko commented 1 month ago

Analogous issue for NamedTuple:

julia> (@NamedTuple{})(1:3)
NamedTuple()

julia> (@NamedTuple{})((10, 20))
ERROR: ArgumentError: Wrong number of arguments to named tuple constructor.
Stacktrace:
 [1] @NamedTuple{}(args::Tuple{Int64, Int64})
   @ Base ./namedtuple.jl:117
 [2] top-level scope
   @ REPL[2]:1