Let typeof(::Type{T}) where T = Type{T}

dispatching on function-like things can be quite tedious, as typeof as of now forgets information.
While typeof(::Function) gives the function singleton type, typeof(::Type{T}) where T, i.e. typeof a Type-constructor, will return DataType or a respective other higher type, and as such looses the information about the type.

I want to discuss whether it makes sense to change this in general such that

typeof(::Type{T}) where T = Type{T}
supertype(::Type{Type{T}}) where T = ... # the same as now ``typeof(::Type{T}) where T``, i.e. DataType, UnionAll, etc.

For an example let’s choose T = Some, then I would suggest to have the following

typeof(Some)  # Type{Some}
supertype(typeof(Some))  # UnionAll

I am curious about other perspectives.

It is generally not recommended to operate in type space in Julia (this is not spelled out very explicitly, but cf this faq). This can easily lead to overly specialized code in cases where it may not be needed.

Types ending up as DataType is kind of an escape valve so that the compiler gets a break. If necessary, you can sharpen things up to get a Type, like the method you define. But generally it is an anti-pattern in Julia.

It would be interesting to see a use case though if you have an MWE.

1 Like

My usecase is better type-inference :wink: and I truly hope that to achieve typeinference it is not an anti-pattern to use Type{T}.

I build a package called IsDef.jl which provides some type-inference helpers which are very handy for me. I was just in the process of fixing the apply thing (there was another issue for this where you already helped me considering function-type vs function-value. It works now, but was more difficult than expected)

Yes, I think that

dispatching on whether certain methods are implemented or not

is an anti-pattern in Julia, and so is relying too much on inferred return types (if I understand correctly, these are the two things your package does).

Again, an MWE / use case would be interesting and may result in suggestions for better solutions.

5 Likes

Understood. Thanks for the generalized phrasing :wink: helped.

I am soon going to publish a package I call TypeClasses which makes use of IsDef and leads at least for me very clean code. I am currently in the process of adding CI, Docs, Codecov and all this, so should soon be ready hopefully. As you for sure know, TypeClasses themselves are more like a Tool, but at least there are many commonly known examples for the abstraction power of TypeClasses. Actually I myself build a package ExtensibleEffects which builds upon TypeClasses and makes use of a couple of them. Also this is in the same process and soon going to be officially registered hopefully.


The faq does not explain why something like IsDef would be discouraged. The faq rather recommends a style, but does not discourage the use of inference in general.
To add, my experiments so far, as well as my understanding of what is going on, is that something like IsDef.isdef is indeed stable enough to be used and recommended.

Now I actually thought of a concrete simple example.

I from time to time use a simple Type I call ContextManager (in analog to python’s contextmanagers). You can find one implementation in my package DataTypesBasic.jl (that package will definitely be released this upcoming week ;-)).

Here the type, it’s super simple

struct ContextManager{F}
  func::F
end

and an example would be

ContextManager(function (cont)
    println("do some initialization to get 42")
    returnme = cont(42)
    println("do some cleanup for 42")
    return returnme
end)

I hope that also clarifies the idea of this construct. It is like a container with initilization and destruction.

Now the usecase: How to define Base.eltype for this container? You can do it with type-inference, which may fail in certain complex cases, but fail in being broader, if I understood it correctly, i.e. returning Any. Which is totally fine here.

So we can define

apply(f, args...) = f(args...)
function Base.eltype(::Type{<:ContextManager{F}}) where F
    Core.Compiler.return_type(apply, Tuple{F, typeof(identity)})
end

or using IsDef (which is what I actually do currently)

using IsDef
function Base.eltype(::Type{<:ContextManager{F}}) where F
  Out(apply, F, typeof(identity))
end

I still lack context for the use case (your library is about how to do something you consider useful, but I don’t understand why one would want to do this), but generally I would just try to avoid defining eltype when the information is not readily available. Eg I am not sure why a “container” is the best abstraction for something that will be calculated on demand.

Eg if you are collecting resuls from calling that closure, Julia’s type inference may figure out the type just fine if everything is type stable. There is a mechanism for iterables not knowing their eltype and it can still compile to efficient code, you don’t have to go out of your way to do the job of the compiler (that’s the anti-pattern here).

2 Likes

You can use Core.Typeof in limited cases, but I agree with Tamas — this isn’t something you should really need to do often.

3 Likes

Why you would like to define something like ContextManager: You want to bind an initialization together with a destruction into a unified abstraction (a Monad, or a “Container”, or how you would want to call it).
The issue however is more general and applies to any “container” which is build upon functions. It is similar to empty containers as such, and hence would allow for a use of Base.promote_op/Core.Compiler.return_type

The question seems to be whether you would rather leave eltype(...) = Any or provide it with the custom-type-inference fallback. I understood that you better don’t use eltype at all, but look at the concrete elements and take typeof. But sometimes you may still want to use it and then it is better to have something possibly better than Any.

@mbauman I never heard about Core.Typeof and also couldn’t find any documentation on a quick search. Can you link me to further details?

Not really — it’s an internal function that shouldn’t be needed in user code. It’s just Typeof(x) = isa(x,Type) ? Type{x} : typeof(x).

3 Likes

I have a feeling it’s another X Y problem… for starter, eltype means elements type and your struct is not a collection at all, you might as well just give it another field.

3 Likes

A monad is how you want to do something in certain languages (eg Haskell). It is almost certainly not what you want to do (ie the actual problem you want to solve, eg parse something). Monads fit perfectly into Haskell, but not Julia’s type system, which has no first class representation for monads (and arrows in general).

I agree with @jling that this may be an XY problem. I think that this discussion is going in circles because of this.

I disagree :wink: Monads very well fit into Julia from my perspective. I will soon release a couple of packages which I hope can convince others too.


Anyhow, this discussion indeed went far off. I originally just wanted to discuss the precise proposal I made about typeof and supertype, but then found the type-inference discussions too interesting. Thanks for all your inputs!

Core.Typeof seems to be half of an answer to my original question, but missing on the supertype relationship I suggested (obviously, as this hardly can be redefined in existing julia without breaking anything). Hence I mark @Tamas_Papp very first general answer as the answer. Thanks again