Understanding supertypes

I’m a little confused about how supertypes work and the inheritance of methods. Say for example, I define my own type with integer as the supertype. How can I use the integer methods with my type?

primitive type MyInt <: Integer 64 end

function MyInt(x::Int)::MyInt
    return reinterpret(MyInt, x)
end

MyInt(8) + MyInt(8) 
ERROR: promotion of types MyInt and MyInt failed to change any arguments
Stacktrace:
 [1] error(::String, ::String, ::String) at ./error.jl:42
 [2] sametype_error(::Tuple{MyInt,MyInt}) at ./promotion.jl:308
 [3] not_sametype(::Tuple{MyInt,MyInt}, ::Tuple{MyInt,MyInt}) at ./promotion.jl:302
 [4] +(::MyInt, ::MyInt) at ./int.jl:799
 [5] top-level scope at none:0

Can someone help explain this?

You would need to define Base.+ for MyInt. And then for more generic code, some promotion rules.

Various packages extend eg Float64, you should look at them to see how it is done in the wild. Base.Rational is a good example (the manual mentions this), and so is eg

and similar ones.

1 Like

What’s the point of the supertype then? I guess basically I want to create a type that acts exactly like an integer exact for one method I want to define to act my way. Is that not possible with supertypes?

The cryptic error message you’re seeing is because julia defines some fallback methods for basic arithmetic operations:

+(x::Number, y::Number) = +(promote(x,y)...)
-(x::Number, y::Number) = -(promote(x,y)...)
*(x::Number, y::Number) = *(promote(x,y)...)
/(x::Number, y::Number) = /(promote(x,y)...)

So what happened was since there were no methods defined for +(x::MyInt, y::MyInt) it fell back on +(x::Number, y::Number) = +(promote(x,y)...) since MyInt <: Number. These definitions are useful for things like adding real numbers to complex numbers. Ie. when you add a real number to a complex number, you want to make the real number a complex number and then you just need to add two complex numbers. ie.

julia> promote(1.0, 1 + im)
(1.0 + 0.0im, 1.0 + 1.0im)

now we have two complex floats that can be added together.

But we see that

julia> promote(MyInt(1), MyInt(2))
(MyInt(0x0000000000000001), MyInt(0x0000000000000002))

julia> ans == (MyInt(1), MyInt(2))
true

so promote doesn’t change the types which led to you receiving a (kinda unhelpful) error message. You can read more about promote and it’s friends here: https://docs.julialang.org/en/v1/manual/conversion-and-promotion/index.html

What you probably want to do is something like

Base.Int(x::MyInt) = reinterpret(Int, x)
Base.:(+)(x::MyInt, y::MyInt) = MyInt(Int(x) + Int(y))

and then

julia> MyInt(1) + MyInt(2) == MyInt(3)
true

As a general comment though, I suspect you don’t actually want to deal with a primitive type here. You might be better served by something like

struct MyInt <: Integer
    val::Int 
end 

ie. just wrapping an integer in a struct and then when you need to do operations on MyInt you just do them with myint.val instead of using reinterpret and bit twiddling. But this is going to be quite dependant on your actual usecase.

1 Like

In part just what you’re implying it is, but it is not necessarily guaranteed that every method is defined for newly defined types. This would be impossible without requiring types to have standard fields. In the primitives case here it would be downright dangerous to make too many assumptions.

If a supertype describes an “interface” (an orthogonal but related concept) then there is a minimal set of methods that must be defined to ensure there are no method errors. Julia has a number of such interfaces in Base, but as far as I know none are documented for basic types such as Integer. Considering that there are packages that extend Number types, it would indeed be nice if these were better documented, so that it would be more clear how to do what you’re attempting here.

4 Likes

This note is about the use of a Type as a supertype when defining a struct.


Let’s select Integer to be our exemplar supertype.

Here is very simple struct that is using Integer as its supertype.

struct BehavesLikeAnInteger <: Integer
  value::Int
end

intlike = BehavesLikeAnInteger(5)
# BehavesLikeAnInteger(5)

Here is almost the same thing – now gone horribly wrong.

struct BehavesLikeAnInteger <: Integer
    value::String
end

notatall_intlike = BehavesLikeAnInteger("The marmalade is talking.")

# BehavesLikeAnInteger("The marmalade is talking.")

The Type that is used as the supertype for a struct is how we say:

“This struct of mine embodies the intent that Type evinces/evokes.
Moreover, the information that I keep each time that this struct
is constructed, provides the wherewithall to participate in operations
that expect some kind of [are designed to accept an] Integer.”


So the choice of a supertype is not bringing to bear the operational
functionality of the type, rather, it is bringing your struct into
the operational fold of that supertype. It is incumbent upon the
designer of a struct with supertype to provide the functionality
necessary for the struct “to play well with” extant methods.

Often, this is accomplished by forwarding the method through e.g.
the value field of a struct. Here is an example.

struct BehavesLikeAnInteger <: Integer
    value::Int
end

# iszero(x) returns a Bool, no re-construction is needed
Base.iszero(x::BehavesLikeAnInteger) =
    iszero(x.value)

# abs(x) returns a typeof(x), re-construction is needed
Base.abs(x::BehavesLikeAnInteger)  =
    BehavesLikeAnInteger(abs(x.value))

This gets tedious very quickly. I wrote TypedDelegation.jl
for just this reason. That package makes type respectful
delegation through the field[s] of a struct easy to express.
There are examples of how it is used in the README.md file.


Another reason to choose a supertype for your struct is that
the supertype is available for use in guiding multidispatch.

abstract type VideoGame end
abstract type SinglePlayerGame <: VideoGame end
abstract type MultiPlayerGame <: VideoGame end

struct Pacman <: SinglePlayerGame
    <...>
    player::GamePlayer
end

struct RedDeadRedemption2 <: MultiplayerGame
    <...>
    players::Vector{GamePlayer}
end

function gameplayer(game::SinglePlayerGame, player::GamePlayer) ... end

function gameplayers(game::MultiPlayerGame, players::Vector{GamePlayer}) ... end

Register all your players, in one [actually two] fell swoop,
independent of the specific game being played, dispatched by
the virtual trait isita_singleplayergame implemented through
inheritance and dispatch.

2 Likes