# 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:
 error(::String, ::String, ::String) at ./error.jl:42
 sametype_error(::Tuple{MyInt,MyInt}) at ./promotion.jl:308
 not_sametype(::Tuple{MyInt,MyInt}, ::Tuple{MyInt,MyInt}) at ./promotion.jl:302
 +(::MyInt, ::MyInt) at ./int.jl:799
 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.")

``````

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

<...>
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