julia icon indicating copy to clipboard operation
julia copied to clipboard

The behavior of type parameter constraint inheritance is not documented

Open brainandforce opened this issue 1 year ago • 1 comments

If you have an abstract type with a constrained type parameter, subtypes whose parameters inherit from the type's parameters do not inherit the constraints.

abstract type AbstractFoo{T<:Real}
end

struct Foo{T} <: AbstractFoo{T} end
julia> supertype(Foo)
AbstractFoo{T} where T

julia> supertype(Foo{<:Real}}
AbstractFoo

julia> Foo <: AbstractFoo    # This one is very much unexpected!
false

julia> Foo{<:Real} <: AbstractFoo
true

This is true even though you can't construct a type which violates the constraints on its parent's parameters:

julia> Foo{String}
ERROR: TypeError: in AbstractFoo, in T, expected T<:Real, got Type{String}
Stacktrace:
 [1] top-level scope
   @ REPL[5]:1

This is a subtle point that is not included in the documentation on abstract parametric types, but has caused me some headaches when some subtype relations broke unexpectedly.

The above examples were tested on Julia 1.10 (version info below) running on Arch Linux in WSL2, installed with the AUR provided julia-bin package that provides the official release:

julia> versioninfo()
Julia Version 1.10.0
Commit 3120989f39b (2023-12-25 18:01 UTC)
Build Info:
  Official https://julialang.org/ release
Platform Info:
  OS: Linux (x86_64-linux-gnu)
  CPU: 20 × 13th Gen Intel(R) Core(TM) i5-13600K
  WORD_SIZE: 64
  LIBM: libopenlibm
  LLVM: libLLVM-15.0.7 (ORCJIT, goldmont)
  Threads: 1 on 20 virtual cores

brainandforce avatar Feb 18 '24 22:02 brainandforce

It seems that maybe the simplest and most robust solution to "simply" change type behavior to clone parameter constraints from supertypes? The actual parameter of a subtype would be restricted by the typeintersect of the subtype's and parent type's constraints in the relevant parameters.

Since types can only be declared once and never modified or moved within the hierarchy, this seems possible? It also seems nonbreaking, except for the useless(?) case where previously one could define a type even though one could never instantiate it.

mikmoore avatar Feb 19 '24 16:02 mikmoore

@vtjnash I know that changing the behavior described here would be a breaking change, but I opened this issue solely for documentation purposes, since this behavior is not detailed in the manual.

brainandforce avatar Jun 18 '24 21:06 brainandforce

Documenting the buggy behavior may prevent future fixes.

nsajko avatar Jun 18 '24 22:06 nsajko

Okay, I added the doc tag as well, to reflect that we may want to document the current situation better, and indicate that while changes could be considered, they are not necessary for this issue

vtjnash avatar Jun 19 '24 02:06 vtjnash

I want to add a relevant example simplified from another discourse post, which I think also behaves like a bug. The type alias RealFoo{T}'s constraint on T is not inherited by RFoo{T} even though they are connected by <:.

julia> abstract type AbstractFoo{T} end

julia> const RealFoo{T<:Real} = AbstractFoo{T}
RealFoo (alias for AbstractFoo{T} where T<:Real)

julia> struct RFoo{T} <: RealFoo{T} end

julia> RFoo{String}
RFoo{String}

frankwswang avatar Jul 19 '24 19:07 frankwswang

The actual parameter of a subtype would be restricted by the typeintersect of the subtype's and parent type's constraints in the relevant parameters.

I don't think this, as stated, is an acceptable change, given that typeintersect isn't exact in general. However, in some cases typeintersect is known upfront to be exact, so perhaps that could be exploited for a partial fix to this issue. Specifically, typeintersect is known to be exact when one of its two arguments subtypes the other argument. So I think a change like proposed by @mikmoore could be good, assuming it were only applied when one of the two upper bounds subtypes the other, to ensure exactness of the type intersection. Of course, someone would need to do the PR, and it could still be blocked by a PkgEval of the General registry.

In particular, this would fix the case when one of the constraints is <:Any, as in the OP.

nsajko avatar Aug 02 '24 17:08 nsajko