Immutable or bitstype?


#1

I want to define a subtype (say MyInt) of Integer and be able to convert between MyInt and other Integers.

I suppose I could use an immutable or a bitstype:

immutable MyInt <: supertype(Int)
    x::Int
end

Base.convert(::Type{MyInt}, a::MyInt) = a
Base.convert(::Type{MyInt}, a::Integer) = MyInt(convert(Int, a))
Base.convert{T<:Integer}(::Type{T}, a::MyInt) = convert(T, a.x)
Base.show(io::IO, a::MyInt) = show(a.x)

or

bitstype sizeof(Int)*8 MyInt <: supertype(Int)

Base.convert(::Type{MyInt}, a::MyInt) = a
Base.convert(::Type{MyInt}, a::Integer) = reinterpret(MyInt, convert(Int, a))
Base.convert{T<:Integer}(::Type{T}, a::MyInt) = convert(T, reinterpret(Int, a))
Base.show(io::IO, a::MyInt) = show(reinterpret(Int, a))

Is one approach faster/better than the other?
Are there any other considerations?

I noticed in Base.Enums the use of box (v0.5) and bitcast (v0.6).
How do these compare with reinterpret?

Enums in v0.5 uses Core.Intrinsics.box:

Base.convert{T<:Integer}(::Type{T}, x::Enum) = convert(T, box(Int32, x))

Enums in v0.6 uses Core.Intrinsics.bitcast:

Base.convert{T<:Integer}(::Type{Integer}, x::Enum{T}) = bitcast(T, x)

#2

It would help to know something about what purpose MyInt is to serve. If you want the behaviour of a subtype and prefer to avoid redefining everything related to your type, use immutable not bitstype.
You may find this helpful.

import Base: convert, show,
    ==, <, <=, +, -, *, /, abs, sign

immutable MyInt <: Signed
    val::Int
end

convert(::Type{Int}, x::MyInt) = x.val
convert(::Type{MyInt}, x::Int) = MyInt(x)

show(io::IO, x::MyInt) = show(io, convert(Int, x))

# delegate comparison ops and arithmetic ops 
for op in (:-, :abs, :sign)
    @eval $op(x::MyInt) = MyInt( $op(x.val) )
end
for op in (:(==), :(<), :(<=))
    @eval $op(x::MyInt, y::MyInt) = $op(x.val, y.val)
    @eval $op(x::MyInt, y::Int) = $op(x.val, y)
    @eval $op(x::Int, y::MyInt) = $op(x, y.val)
end
for op in (:+, :-, :*)
    @eval $op(x::MyInt, y::MyInt) = MyInt($op(x.val, y.val))
    @eval $op(x::MyInt, y::Int) = MyInt($op(x.val, y))
    @eval $op(x::Int, y::MyInt) = MyInt($op(x, y.val))
end
# and division
(/)(x::MyInt, y::MyInt) = Float64(x.val) / Float64(y.val)
(/)(x::Int, y::MyInt) = x / Float64(y.val)
(/)(x::MyInt, y:Int) = Float64(x.val) / y

#3

Thanks for your reply.

Yes, I should be more specific.
I am really asking about the difference between Julia’s representation of a single-field immutable vs bitstype, and if there are any performance benefits, or if there are any other considerations.

In trying to answer my own question I looked at llvm (which I don’t really understand).
I get this for immutable:

julia> @code_llvm convert(MyInt, 5)

; Function Attrs: uwtable
define %MyInt @julia_convert_71649(%jl_value_t*, i64) #0 {
top:
  %2 = insertvalue %MyInt undef, i64 %1, 0
  ret %MyInt %2
}

and this for bitstype:

julia> @code_llvm convert(MyInt, 5)

; Function Attrs: uwtable
define i64 @julia_convert_71658(%jl_value_t*, i64) #0 {
top:
  ret i64 %1
}

If I replace x.val and y.val with Int(x) and Int(y) respectively, then your code works for both immutable and bitstype definitions of MyInt.

The only difference is the type definition and convert methods:

immutable MyInt <: Signed
    val::Int
end
convert(::Type{Int}, x::MyInt) = x.val
convert(::Type{MyInt}, x::Int) = MyInt(x)

versus

bitstype sizeof(Int)*8 MyInt <: Signed
convert(::Type{Int}, x::MyInt) = reinterpret(Int, x)
convert(::Type{MyInt}, x::Int) = reinterpret(MyInt, x)

So I don’t think it’s any easier to redefine methods using immutable.


#4

But to fully answer your question, my purpose is create a module for defining enums that can be used as an Integer, primarily for indexing into arrays.

I realise there is already an Enum module in base, but I want to be able to do things like:

julia> using IntEnums

julia> @IntEnum Regions north east south west
IntEnum Regions:
  north = 1
  east = 2
  south = 3
  west = 4

julia> counts = rand(1:1000, length(Regions))
4-element Array{Int64,1}:
 436
 859
 227
 303

julia> for region in Regions
           println(region, " ", counts[region])
       end
north 436
east 859
south 227
west 303

I have this working by using a single-field immutable type, but was wondering about the difference between this and using a primitive bitstype.


#5

:flashlight:
Bitstypes are used by Julia itself to develop the primitive types we all know and relish; and a bitstype may be used occasionally by a package writer who needs deeply individuated low-level (bit layout) behavior. Immutable types are allocated as given in memory (the indirection of array storage is avoided). That gives us the speed of memory immediacy without plowing through aspects of getting a bitstype to be robustly useful.