Concise way to create numeric type?

I am creating a numeric type that contains a single number field, for example:

struct T
v::Float64
T(v) = 0 <= v <= 1 ? new(v) : error("invalid v")
end

In case you are wondering, I am doing this to ensure that all variables of type T have a value between 0 and 1 (or some other invariant I might want to enforce).

Now I want to define all numeric operations on T, simply by calling the operation on the field v and returning a number, like:

Base.:(+)(x::T, y::T) = x.p + y.p

Is there a concise way of doing this for all numeric operations? I would also like to cover numeric functions. Maybe a macro?

I also tried

Base.convert(::Type{F}, p::T) where {F <: Number} = convert(F, p.p)

in the hope that my type would get promoted automatically to a number and then the numeric operations would work, but this does not work either.

Oh I figured it out.

struct T <: Number
    v::Float64;
    T(x) = 0 ≤ x ≤ 1 ? new(x) : error("invalid v")
end

Base.convert(::Type{N}, x::T) where {N <: Number} = convert(N, x.v)
Base.promote_rule(::Type{N}, ::Type{T}) where {N <: Number} = promote_type(N, Float64)

But now I am getting some strange errors. Simply executing:

T(0.2)

results in this big error:

T(Error showing value of type T:
ERROR: MethodError: Cannot convert an object of type Float64 to an object of type T
This may have arisen from a call to the constructor T(…),
since type constructors fall back to convert methods.
Stacktrace:
[1] Pair(::Symbol, ::T) at ./pair.jl:4
[2] show_default(::IOContext{Base.Terminals.TTYTerminal}, ::Any) at ./show.jl:134
[3] display(::Base.REPL.REPLDisplay{Base.REPL.LineEditREPL}, ::MIME{Symbol(“text/plain”)}, ::T) at ./REPL.jl:122
[4] display(::Base.REPL.REPLDisplay{Base.REPL.LineEditREPL}, ::T) at ./REPL.jl:125
[5] display(::T) at ./multimedia.jl:194
[6] eval(::Module, ::Any) at ./boot.jl:235
[7] print_response(::Base.Terminals.TTYTerminal, ::Any, ::Void, ::Bool, ::Bool, ::Void) at ./REPL.jl:144
[8] print_response(::Base.REPL.LineEditREPL, ::Any, ::Void, ::Bool, ::Bool) at ./REPL.jl:129
[9] (::Base.REPL.#do_respond#16{Bool,Base.REPL.##26#36{Base.REPL.LineEditREPL,Base.REPL.REPLHistoryProvider},Base.REPL.LineEditREPL,Base.LineEdit.Prompt})(::Base.LineEdit.MIState, ::Base.AbstractIOBuffer{Array{UInt8,1}}, ::Bool) at ./REPL.jl:646

What is going on here??

And even more surprisingly, T(0.2) + 0.2 correctly returns 0.4 without any problems.

You should define

Base.convert(::Type{T}, x::T) = x

or convert will get into an infinite recursion when you try to convert a T into a T. (I’m guessing show tries to do this.)

1 Like

Here’s a nice definition of + - * / for a Galois field using a loop over those operators and macros, from one of Andreas Noack’s talks:

import Base: +, -, *, /

for op in (:+, :-, :*)
    @eval begin
        ($op){P,T}(x::GF{P,T}, y::GF{P,T}) = GF{P,T}($(op)(x.data, y.data))
    end
end

Complete working example

# Scalar finite fields. P is the modulus, T is the integer type (Int16, Int32, ...)
immutable GF{P,T<:Integer} <: Number
    data::T
    function GF(x::Integer)
        return new(mod(x, P))
    end
end


# basic methods for scalar finite field
import Base: convert, inv, one, promote_rule, show, zero

function call{P}(::Type{GF{P}}, x::Integer)
    if !isprime(P)
        throw(ArgumentError("P must be a prime"))
    end
    return GF{P,typeof(x)}(mod(x, P))
end
convert{P,T}(::Type{GF{P,T}}, x::Integer) = GF{P}(x)
convert{P}(::Type{GF{P}}, x::Integer) = GF{P}(x)
convert{P,T}(::Type{GF{P,T}}, x::GF{P}) = GF{P,T}(x.data)
promote_rule{P,T1,T2<:Integer}(::Type{GF{P,T1}}, ::Type{T2}) = GF{P,promote_type(T1,T2)}
show(io::IO, x::GF) = show(io, x.data)

# define arithmetic operations
import Base: +, -, *, /

for op in (:+, :-, :*)
    @eval begin
        ($op){P,T}(x::GF{P,T}, y::GF{P,T}) = GF{P,T}($(op)(x.data, y.data))
    end
end

# Division requires slightly more care
function inv{P,T}(x::GF{P,T})
    if x == zero(x)
        throw(DivideError())
    end
    r, u, v = gcdx(x.data, P)
    GF{P,T}(u)
end
(/){P}(x::GF{P}, y::GF{P}) = x*inv(y)

x, y = GF{5}(9), GF{5}(8)
@show x
@show y
@show x + y
@show x - y
@show x * y
;
x = 4
y = 3
x + y = 2
x - y = 1
x * y = 2
1 Like

This is incorrect. Base already includes this definition.

@stevengj It did fix the errors… somehow it worked.

Yes, but the method that specializes on a concrete type has precedence.

julia> struct T <: Number
           v::Float64;
           T(x) = 0 ≤ x ≤ 1 ? new(x) : error("invalid v")
       end

julia> Base.convert(::Type{N}, x::T) where {N <: Number} = convert(N, x.v)

julia> @which convert(T, T(0.2))
convert(::Type{N}, x::T) where N<:Number in Main at REPL[2]:1
1 Like