Weird Tuple types that seem like they should be disallowed?

I noticed that some Tuple types that are parameterized by non-types exist, but seem useless. I wonder if they:

  1. May have instances at all?

  2. Should be disallowed?

Example:

               _
   _       _ _(_)_     |  Documentation: https://docs.julialang.org
  (_)     | (_) (_)    |
   _ _   _| |_  __ _   |  Type "?" for help, "]?" for Pkg help.
  | | | | | | |/ _` |  |
  | | |_| | | | (_| |  |  Version 1.5.4 (2021-03-11)
 _/ |\__'_|_|_|\__'_|  |
|__/                   |

julia> Tuple{1243} <: Tuple{Any}
true

julia> Tuple{(124310, 983724)} <: Tuple{Any}
true

It’s relevant that similar constructions are not permissible in function type signatures:

julia> function f(x::423)
       5
       end
ERROR: ArgumentError: invalid type for argument x in method definition for f at REPL[2]:1
Stacktrace:
 [1] top-level scope at REPL[2]:1
 [2] run_repl(::REPL.AbstractREPL, ::Any) at /build/julia/src/julia-1.5.4/usr/share/julia/stdlib/v1.5/REPL/src/REPL.jl:288

Good observation! It actually can be really useful to put data up in the type-domain. The simplest example is probably multidimensional arrays; in Julia, they are represented by the AbstractArray{T, N} type, where N is an integer representing the number of dimensions, i.e. Matrix{Float64} === Array{Float64, 2}. Note that it’s not the type Integer, but the literal number 2.

As a more complicated example, I wrote a blog post about encoding a permutation as part of a type to permute vectors very quickly (at the cost of recompiling the function whenever you change the permutation).

Having values as part of types is mentioned in the docs too,

  • Both abstract and concrete types can be parameterized by other types. They can also be parameterized by symbols, by values of any type for which isbits returns true (essentially, things like numbers and bools that are stored like C types or struct s with no pointers to other objects), and also by tuples thereof. Type parameters may be omitted when they do not need to be referenced or restricted.

To answer your question,

May have instances at all?

I think the answer is no. But that doesn’t mean they can’t be used, e.g.

julia> struct MyType{T} end

julia> f(::Type{MyType{T}}) where T = T[1] + 1
f (generic function with 1 method)

julia> X = MyType{(5,6,7)}
MyType{(5, 6, 7)}

julia> f(X)
6

By the way, the result here is actually computed at compile-time, e.g. in

julia> @code_warntype f(X)
Variables
  #self#::Core.Const(f)
  #unused#::Core.Const(MyType{(5, 6, 7)})

Body::Int64
1 ─ %1 = Base.getindex($(Expr(:static_parameter, 1)), 1)::Core.Const(5)
│   %2 = (%1 + 1)::Core.Const(6)
└──      return %2

we can see the Core.Const(6) result is known to the compiler to be a constant.

Lastly, just to comment on

Should be disallowed?

Generally Julia tries to not impose restrictions on users unless they are needed for performance or correctness, so I think even if they weren’t so fundamental as to be used in Array, they should not be disallowed.

4 Likes

Hmm, you seem to have misunderstood me somewhat: I do know about parametric types and singleton types (and, yes, singleton types can have instances, just do, e.g., MyType{(5, 6, 7)}(), notice the parentheses at the end).
But I was asking specifically about the Tuple types that are parameterized by non-types :smiley:

Hm, I guess I don’t really see the distinction, it just looks like another way to put data in the type domain. E.g.

julia> g(::Type{Tuple{T}}) where {T} = T - 1
g (generic function with 1 method)

julia> g(Tuple{123})
122
1 Like

Aren’t StaticArrays implemented like that?

2 Likes

Eric, you’re technically correct, but the usage like yours is not really “blessed” by the Julia devs, it seems: the Julia Manual section on Types says this:

For consistency across Julia, the call site should always pass a Val instance rather than using a type, i.e., use foo(Val(:bar)) rather than foo(Val{:bar}).

I.e., the blessed way is to pass instances of singleton types to functions instead of the singleton types themselves.

Yep! They put the size as a tuple in the type: https://github.com/JuliaArrays/StaticArrays.jl/blob/59f92e0ca7ac391a850a6e7a2ce1eb53aa237fc4/src/StaticArrays.jl#L73-L77. Great example.

3 Likes

True, although I think that’s more of a style issue than anything else, but I could be mistaken. I guess the StaticArrays example gives a better-motivated reason than singleton types.

2 Likes

I definitely agree that the StaticArrays way is less verbose, but I guess the developers had some good reason for writing the quoted paragraph in the manual, so probably that code should be somewhat adjusted in StaticArrays?

I think the StaticArrays example is a different usage than what they’re discussing at that part of the manual, since the StaticArrays subtypes generally store data in fields in addition to keeping the size as a tuple in the type. For example, here: StaticArrays.jl/SArray.jl at 59f92e0ca7ac391a850a6e7a2ce1eb53aa237fc4 · JuliaArrays/StaticArrays.jl · GitHub is where the SArray type is defined (SArray is analogous to Array, while StaticArray is analogous to AbstractArray).

1 Like

I found the commit that introduced that paragraph to the manual: Use Val(x) and f(::Val{x}) (#22475) · JuliaLang/julia@259996c · GitHub

The commit message presents the following advantage, but I’m not sure if it applies here:

This form also has the advantage that multiple singleton instances can be put in a tuple and inference will work (similarly with multiple-return functions).

1 Like

It is a hack, and this is known. Cf

Perhaps you are confusing Julia with some kind of religion (it isn’t — it is a programming language).

The manual is suggesting a way to do something above, but as pointed out above by @ericphanson, these remain valid type parameters. And no, not all types need to have instances. That’s perfectly fine.

2 Likes