Georgery
Georgery

Reputation: 8117

Create a primitive Type that behaves like another Type

I would like to create a primitive type that behaves like let's say Int64, but named Foo. Is this possible?

I learned here that I can do something like this

# Declare the new type.
primitive type MyInt8 <: Signed 8 end

# A constructor to create values of the type MyInt8.
MyInt8(x :: Int8) = reinterpret(MyInt8, x)

# A constructor to convert back.
Int8(x :: MyInt8) = reinterpret(Int8, x)

# This allows the REPL to show values of type MyInt8.
Base.show(io :: IO, x :: MyInt8) = print(io, Int8(x))

But I do not understand why, even if MyInt8 is a subtype of Signed (MyInt8 <: Signed) there are no methods for this type:

a = MyInt8(Int8(3))
b = MyInt8(Int8(4))

a + b
ERROR: promotion of types MyInt8 and MyInt8 failed to change any arguments

I thought that as a subtype the primitive type would automatically "get" all the methods of the supertype as well.

Where am I wrong here?

Upvotes: 3

Views: 202

Answers (1)

As a subtype of Signed, MyInt8 does automatically "get" all methods defined for numbers. And there is actually quite a lot of them:

julia> methodswith(MyInt8, supertypes=true) |> length
1218

julia> methodswith(MyInt8, supertypes=true)[1:10]
[1] poll_fd(s::RawFD, timeout_s::Real; readable, writable) in FileWatching at /home/francois/.local/julia-1.5.2/share/julia/stdlib/v1.5/FileWatching/src/FileWatching.jl:649
[2] poll_file(s::AbstractString, interval_seconds::Real) in FileWatching at /home/francois/.local/julia-1.5.2/share/julia/stdlib/v1.5/FileWatching/src/FileWatching.jl:784
[3] poll_file(s::AbstractString, interval_seconds::Real, timeout_s::Real) in FileWatching at /home/francois/.local/julia-1.5.2/share/julia/stdlib/v1.5/FileWatching/src/FileWatching.jl:784
[4] watch_file(s::AbstractString, timeout_s::Real) in FileWatching at /home/francois/.local/julia-1.5.2/share/julia/stdlib/v1.5/FileWatching/src/FileWatching.jl:687
[5] watch_folder(s::String, timeout_s::Real) in FileWatching at /home/francois/.local/julia-1.5.2/share/julia/stdlib/v1.5/FileWatching/src/FileWatching.jl:716
[6] watch_folder(s::AbstractString, timeout_s::Real) in FileWatching at /home/francois/.local/julia-1.5.2/share/julia/stdlib/v1.5/FileWatching/src/FileWatching.jl:715
[7] randcycle(r::Random.AbstractRNG, n::T) where T<:Integer in Random at /home/francois/.local/julia-1.5.2/share/julia/stdlib/v1.5/Random/src/misc.jl:348
[8] randcycle(n::Integer) in Random at /home/francois/.local/julia-1.5.2/share/julia/stdlib/v1.5/Random/src/misc.jl:349
[9] randexp(rng::Random.AbstractRNG, ::Type{T}, dim1::Integer, dims::Integer...) where T in Random at /home/francois/.local/julia-1.5.2/share/julia/stdlib/v1.5/Random/src/normal.jl:204
[10] randperm(r::Random.AbstractRNG, n::T) where T<:Integer in Random at /home/francois/.local/julia-1.5.2/share/julia/stdlib/v1.5/Random/src/misc.jl:282

Some of them don't require that MyInt8 conform to any specific interface, and will work "out of the box":

julia> a = MyInt8(Int8(3))
3

julia> conj(a)
3

But some will provide an implementation that depends on MyInt8 conforming to an interface:

# Here `imag` does not know how to build a zero of the MyInt8 type
julia> imag(a)
ERROR: MethodError: no method matching MyInt8(::Int64)
Closest candidates are:
  MyInt8(::T) where T<:Number at boot.jl:716
  MyInt8(::Int8) at REPL[7]:2
  MyInt8(::Float16) where T<:Integer at float.jl:71
  ...
Stacktrace:
 [1] convert(::Type{MyInt8}, ::Int64) at ./number.jl:7
 [2] oftype(::MyInt8, ::Int64) at ./essentials.jl:367
 [3] zero(::MyInt8) at ./number.jl:241
 [4] imag(::MyInt8) at ./complex.jl:78
 [5] top-level scope at REPL[37]:1

The addition falls into that last category:

# There is indeed a generic implementation that MyInt8 inherits
julia> which(+, (MyInt8, MyInt8))
+(a::Integer, b::Integer) in Base at int.jl:918

# But it relies on promotion features, which are not implemented for MyInt8
julia> a + a
ERROR: promotion of types MyInt8 and MyInt8 failed to change any arguments
Stacktrace:
 [1] error(::String, ::String, ::String) at ./error.jl:42
 [2] sametype_error(::Tuple{MyInt8,MyInt8}) at ./promotion.jl:306
 [3] not_sametype(::Tuple{MyInt8,MyInt8}, ::Tuple{MyInt8,MyInt8}) at ./promotion.jl:300
 [4] +(::MyInt8, ::MyInt8) at ./int.jl:921
 [5] top-level scope at REPL[41]:1

In that particular case, the inherited implementation of + is only there to handle the addition of two operands of different types; I guess you'll have to implement + for MyInt8 yourself, in much the same way as Int8 implements its own addition:

julia> which(+, (Int8, Int8))
+(x::T, y::T) where T<:Union{Int128, Int16, Int32, Int64, Int8, UInt128, UInt16, UInt32, UInt64, UInt8} in Base at int.jl:86

I'd probably go for an implementation like the following:

julia> Base.:+(a::MyInt8, b::MyInt8) = MyInt8(Int8(a)+Int8(b))

julia> a + a
6

EDIT:

This should reach the same performances as the native Int8 type:

# two Int8 vectors
julia> a1 = rand(Int8, 1000); b1 = rand(Int8, 1000);

# same vectors, as MyInt8 values
julia> a2 = MyInt8.(a1);      b2 = MyInt8.(b1);

# check that both ways of doing the calculation produce the same results
julia> @assert a1 .+ b1 == Int8.(a2 .+ b2)

# Benchmark
julia> using BenchmarkTools
julia> @benchmark $a1 .+ $b1
BenchmarkTools.Trial: 
  memory estimate:  1.06 KiB
  allocs estimate:  1
  --------------
  minimum time:     145.274 ns (0.00% GC)
  median time:      176.402 ns (0.00% GC)
  mean time:        200.996 ns (4.20% GC)
  maximum time:     1.349 μs (79.34% GC)
  --------------
  samples:          10000
  evals/sample:     759

julia> @benchmark $a2 .+ $b2
BenchmarkTools.Trial: 
  memory estimate:  1.06 KiB
  allocs estimate:  1
  --------------
  minimum time:     140.316 ns (0.00% GC)
  median time:      172.119 ns (0.00% GC)
  mean time:        195.947 ns (4.54% GC)
  maximum time:     1.148 μs (73.34% GC)
  --------------
  samples:          10000
  evals/sample:     750

Upvotes: 3

Related Questions