Floats or Integers or Vectors with specific ranges?


#1

Very basic questions:

  • can I define an Int8 Type that can only range from 5 to 20, but throws an error if I try to assign 21 ?
  • can I define a Float64 Type that can only range from 1.0 to Infinity, if I try to assign 0.5 ?
  • can I define a Vector (e.g., of Floats) that must be of length 5 (or null), but throws an error if I try to assign a vector of length 4 or 6??

I suspect no, except with my own code (e.g., checking when assigning, structs with custom initializers that are checking, etc). Types are already complicated enough. I am just asking for confirmation.


#2

Huh? How else would you define something, but with your own code?

julia> struct FiveToTwenty <: Unsigned
           val::UInt8
           function FiveToTwenty(val)
               @assert val >= 5 && val <= 20
               new(UInt8(val))
           end
       end

julia> Base.show(io::IO, v::FiveToTwenty) = print(io, v.val)

julia> FiveToTwenty(4)
ERROR: AssertionError: val >= 5 && val <= 20
Stacktrace:
 [1] FiveToTwenty(::Int64) at ./REPL[159]:4

julia> FiveToTwenty(14)
14

julia> FiveToTwenty(24)
ERROR: AssertionError: val >= 5 && val <= 20
Stacktrace:
 [1] FiveToTwenty(::Int64) at ./REPL[159]:4

julia> isbits(FiveToTwenty(14))
true

Ideas for others would be similar. For the vector, you may just want to use a mutable struct with 5 fields, or wrap an MVector (from StaticArrays.jl).


#3

The better question is, what are you trying to do? If you’re trying to constrain optimization variables, there’s better ways to do that. If you’re trying to enforce constraints in a nonlinear equation, a transformation could help. Etc.


#4

thanks. Chris, I am just trying to impose discipline on my code, and perhaps get lucky finding errors along the way (rather than with asserts). I am also just trying to learn the feature set of julia. I believe the optimization routines have box constraints, which I will work with later.

Elrod, are you running 0.6.2?

julia> struct FiveToTwenty <: Unsigned
                  val::UInt8
                  function FiveToTwenty(val)
                      @assert val >= 5 && val <= 20
                      new(UInt8(val))
                  end
              end

julia> FiveToTwenty(14)
Error showing value of type FiveToTwenty:
ERROR: MethodError: no method matching leading_zeros(::FiveToTwenty)
Closest candidates are:
  leading_zeros(::Union{Int128, Int16, Int32, Int64, Int8, UInt128, UInt16, UInt32, UInt64, UInt8}) at int.jl:238
Stacktrace:
 [1] hex(::FiveToTwenty, ::Int64, ::Bool) at ./intfuncs.jl:543
 [2] display(::Base.REPL.REPLDisplay{Base.REPL.LineEditREPL}, ::MIME{Symbol("text/plain")}, ::FiveToTwenty) at ./REPL.jl:122
 [3] display(::Base.REPL.REPLDisplay{Base.REPL.LineEditREPL}, ::FiveToTwenty) at ./REPL.jl:125
 [4] display(::FiveToTwenty) at ./multimedia.jl:218
 [5] eval(::Module, ::Any) at ./boot.jl:235
 [6] print_response(::Base.Terminals.TTYTerminal, ::Any, ::Void, ::Bool, ::Bool, ::Void) at ./REPL.jl:144
 [7] print_response(::Base.REPL.LineEditREPL, ::Any, ::Void, ::Bool, ::Bool) at ./REPL.jl:129
 [8] (::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

Your example (after I get it to work) was unintuitive to me. I thought of a struct as a composite type, while an Int is a base type; and I think you are defining a struct to be a subtype of a base type. Gotta learn and experiment more…

/iaw


#5

You need to define Base.show for this type.

julia> struct FiveToTwenty <: Unsigned
                         val::UInt8
                         function FiveToTwenty(val)
                             @boundscheck val >= 5 && val <= 20
                             new(UInt8(val))
                         end
                     end

julia> FiveToTwenty(14)
Error showing value of type FiveToTwenty:
ERROR: MethodError: no method matching leading_zeros(::FiveToTwenty)
Closest candidates are:
  leading_zeros(::Union{Int128, Int16, Int32, Int64, Int8, UInt128, UInt16, UInt32, UInt64, UInt8}) at int.jl:238
Stacktrace:
 [1] hex(::FiveToTwenty, ::Int64, ::Bool) at ./intfuncs.jl:543
 [2] display(::Base.REPL.REPLDisplay{Base.REPL.LineEditREPL}, ::MIME{Symbol("text/plain")}, ::FiveToTwenty) at ./REPL.jl:122
 [3] display(::Base.REPL.REPLDisplay{Base.REPL.LineEditREPL}, ::FiveToTwenty) at ./REPL.jl:125
 [4] display(::FiveToTwenty) at ./multimedia.jl:218
 [5] eval(::Module, ::Any) at ./boot.jl:235
 [6] print_response(::Base.Terminals.TTYTerminal, ::Any, ::Void, ::Bool, ::Bool, ::Void) at ./REPL.jl:144
 [7] print_response(::Base.REPL.LineEditREPL, ::Any, ::Void, ::Bool, ::Bool) at ./REPL.jl:129
 [8] (::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

julia> x = ans;

julia> Base.show(io::IO, v::FiveToTwenty) = print(io, v.val)

julia> x
14

The hint was that the error said "Error showing…"
You created the object just fine, but the error came in displaying the actual value. This is why we could still assign the value (without error), as long as we followed the operation with a semicolon.
Then, defining a show method let you display it.

In Julia, if you want a custom type with certain properties, you can just define it on your own.
If you don’t like always having the error checking, you can

julia> struct FiveToTwenty2 <: Unsigned
                                val::UInt8
                                @inline function FiveToTwenty2(val)
                                    @boundscheck begin @assert val >= 5 && val <= 20 end
                                    new(UInt8(val))
                                end
                            end
julia> Base.show(io::IO, v::FiveToTwenty2) = print(io, v.val)

julia> f(x) = (@inbounds x = FiveToTwenty2(x); x)
f (generic function with 1 method)

julia> FiveToTwenty2(21)
ERROR: AssertionError: val >= 5 && val <= 20
Stacktrace:
 [1] FiveToTwenty2(::Int64) at ./REPL[52]:4

julia> f(11)
11

julia> f(21)
21

Thus, once you’ve confirmed your code never violates the bounds, you can add the @inbounds and turn bounds checking off.

Although if someone else put a lot of time into a solution, of course it’s easier to take advantage of their work. People’ve asked a few times what the best way to actually discover packages is though…

Anyway, if you’re facing constrained optimization problems with straightforward constraints (eg, 1 < y < infinity), a straightforward approach is as in:


Where you transform the variables from the constrained to an unconstrained version.
Eg, -infinity < log(y-1) < infinity. The above package handles transforms like this, as well as providing everything else, like Jacobians and the inverse transforms.

For the third case,

julia> using StaticArrays

julia> FiveVector1{T} = MVector{5,T}
StaticArrays.MArray{Tuple{5},T,1,5} where T

julia> FiveVector2{T} = SizedArray{Tuple{5},T}
StaticArrays.SizedArray{Tuple{5},T,N,M} where M where N where T

julia> FiveVector1(randn(5))
5-element MVector{5,Float64}:
  0.804218
  3.43472 
 -0.786301
  0.47925 
  1.69257 

julia> FiveVector2(randn(5))
5-element StaticArrays.SizedArray{Tuple{5},Float64,1,1}:
 -1.37541 
  0.969937
  0.364058
 -0.364662
 -2.35112 

#6

thanks, again, Elrod. Grrr…I should have made this multiple questions. I had expected a “no work” and move on. julia feels more like perl, in that there are so many things to discover, than like C, where it is easy to learn and then remember everything.

thank you for explaining where I messed up on the FiveToTwenty. instead of defining show, is it possible to tell julia that the FiveToTwenty type can be promoted to an Integer? Then, not only would the print work automatically , but also examples like

julia> 15+FiveToTwenty(14)
ERROR: no promotion exists for Int64 and FiveToTwenty

I did not see a working example in the docs, so I tried

julia> promote_rule(::Type{Int}, ::Type{FiveToTwenty}) = Int
ERROR: error in method definition: function Base.promote_rule must be explicitly imported to be extended

As a sidenote, Pascal had a few type innovations decades ago, one of which were range types. julia’s innovations in the type realm are fascinating, and not just a little syntactic sugar, but a lot. One needs a lot of insulin to digest it.


#7

If you want to extend a function from another module, you have to preface the name with the module (see the error message).
For example, instead of show, I had to say Base.show.

In your case, that means (you also need to define convert methods, so Julia knows how to do the conversion):

Base.promote_rule(::Type{Int}, ::Type{FiveToTwenty}) = Int
Base.convert(::Type{Int}, x::FiveToTwenty) = Int(x.val)
FiveToTwenty(14) + 2
FiveToTwenty(14) + 20

The output is of a regular integer (64 bit on 64 bit systems).
That doesn’t for for showing though.
Honestly, I haven’t messed with displays for custom types nearly enough, so I’m not really sure what the best way to do things are. If you delete the <: Unsigned part, it will display automatically as:

julia> struct FiveToTwenty3
              val::UInt8
              @inline function FiveToTwenty3(val)
                  @boundscheck begin @assert val >= 5 && val <= 20 end
                  new(UInt8(val))
              end
         end

julia> FiveToTwenty3(8)
FiveToTwenty3(0x08)

julia> isbits(ans)
true

A fallback show for numeric types doesn’t seem to be defined. The above is normal, although not especially pretty.

If you’re adding a lot of new types, you can

import Base: promote_rule
promote_rule(::Type{Int}, x::FiveToTwenty) = Int(x.val)

In Julia, you can get range types like

julia> typeof(1:20)
UnitRange{Int64}

julia> typeof(linspace(0.3, 17, 100))
StepRangeLen{Float64,Base.TwicePrecision{Float64},Base.TwicePrecision{Float64}}

I love Julia’s innovations.
Multiple dispatch is incredible.
I think it can take a while to get used to, but how it lets you get low/zero overhead performant abstractions with extreme generalization…just mind blowing!

Like, about a month ago I was working on a problem, and my answers were nonsense.
The functions involved inverting and multiplying a few (ill-conditioned) matrices. Simply calling BigFloat.() on the inputs -> everything worked, the answers were right, and I had derivative-enhanced quadrature rules.

More impressive of course is how automatic differentiation “just works” on most code, or how @ChrisRackauckas got a bunch of functions to also work on GPUArrays…