Parametric types with multiple dependent parameters in Julia

568 Views Asked by At

I'm trying to understand parametric types in Julia with multiple parameters. Here's a simple example. Suppose I want to define a type for binary vectors where internally the vector is represented as the bits in the binary expansion of an integer. For example, the vector (1,0,1,1) would be represented by the integer 13.

One way to achieve this in Julia is to define a parametric type BinaryVector{n,T} with two parameters: n is the dimension of the vector and T is the type of the internal representation of the vector, e.g. UInt8.

abstract type AbstractBinaryVector end

struct BinaryVector{n, T} <: AbstractBinaryVector
    a::T
    function BinaryVector{n, T}(a::T) where {n, T<:Integer}
        return new(a)
    end
end

For convenience I want to define an outer constructor method that only requires specifying the parameter n and uses a reasonable default value for T based on n. I could default to using an unsigned integer type with enough bits to specify a binary vector of length n:

function typerequired(n::Integer)
    if n ≤ 128
        bitsrequired = max(8, convert(Integer, 2^ceil(log2(n))))
        return eval(Meta.parse("UInt"*string(bitsrequired)))
    else
        return BigInt
    end
end

function BinaryVector{n}(a::Integer) where {n}
    T = typerequired(n)
    return SymplecticVector{n, T}(a)
end

Does this implicitly define a new parametric type BinaryVector{n} with BinaryVector{n,T} a subtype of BinaryVector{n} for any integer type T? Note that I don't actually need a type BinaryVector{n}, I only want a convenient way of setting a default value for the parameter T since for example when n is 4, T will almost always be UInt8.

This distinction between BinaryVector{n} and BinaryVector{n,T} manifests in an unexpected way when I define functions for generating random binary vectors. Here's how I do it. The first function below is called using e.g. rand(BinaryVector{4,UInt8}) and it returns and object of type BinaryVector{4,UInt8}. The second function is the same except for generating arrays of random binary vectors. The third function is called as rand(BinaryVector{4}) and assumes the default value for the parameter T. The fourth is the array version of the third function.

import Base: rand
import Random: AbstractRNG, SamplerType

function rand(rng::AbstractRNG, ::SamplerType{BinaryVector{n, T}}) where {n, T}
    return BinaryVector{n, T}(rand(rng, 0:big(2)^n-1)...)
end

function rand(rng::AbstractRNG, ::SamplerType{BinaryVector{n, T}}, dims...) where {n, T}
    return BinaryVector{n, T}.(rand(rng, 0:big(2)^n-1, dims...))
end

function rand(rng::AbstractRNG, ::SamplerType{BinaryVector{n}}) where {n}
    T = typerequired(n)
    return rand(BinaryVector{n, T})
end

function rand(rng::AbstractRNG, ::SamplerType{BinaryVector{n}}, dims...) where {n}
    T = typerequired(n)
    return rand(BinaryVector{n, T}, dims...)
end

The first three functions work as expected:

julia> a = rand(BinaryVector{4, UInt8})
BinaryVector{4, UInt8}(0x06)

julia> typeof(a)
BinaryVector{4, UInt8}

julia> b = rand(BinaryVector{4, UInt8}, 3)
3-element Vector{BinaryVector{4, UInt8}}:
 BinaryVector{4, UInt8}(0x05)
 BinaryVector{4, UInt8}(0x00)
 BinaryVector{4, UInt8}(0x0e)

julia> typeof(b)
Vector{BinaryVector{4, UInt8}} (alias for Array{BinaryVector{4, UInt8}, 1})

julia> c = rand(BinaryVector{4})
BinaryVector{4, UInt8}(0x05)

julia> typeof(c)
BinaryVector{4, UInt8}

But when using the last function:

julia> d = rand(BinaryVector{4}, 3)
3-element Vector{BinaryVector{4}}:
 BinaryVector{4, UInt8}(0x07)
 BinaryVector{4, UInt8}(0x0e)
 BinaryVector{4, UInt8}(0x0b)

julia> typeof(d)
Vector{BinaryVector{4}} (alias for Array{BinaryVector{4}, 1})

the elements of d have type BinaryVector{4} rather than BinaryVector{4,UInt8}. Is there a way to force this function to return an object of type Vector{BinaryVector{4,UInt8}} rather than something of type Vector{BinaryVector{4}}?

Alternatively, is there a better way of doing all of this? The reason I'm not just defining a type BinaryVector{n} in the first place and always using the default unsigned integer type as the internal representation is it seems like calling the typerequired function each time a binary vector is created would be expensive if I'm creating a large number of binary vectors.


Full code sample:

abstract type AbstractBinaryVector end

struct BinaryVector{n, T} <: AbstractBinaryVector
    a::T
    function BinaryVector{n, T}(a::T) where {n, T<:Integer}
        return new(a)
    end
end


function typerequired(n::Integer)
    if n ≤ 128
        bitsrequired = max(8, convert(Integer, 2^ceil(log2(n))))
        return eval(Meta.parse("UInt"*string(bitsrequired)))
    else
        return BigInt
    end
end

function BinaryVector{n}(a::Integer) where {n}
    T = typerequired(n)
    return SymplecticVector{n, T}(a)
end


import Base: rand
import Random: AbstractRNG, SamplerType

function rand(rng::AbstractRNG, ::SamplerType{BinaryVector{n, T}}) where {n, T}
    return BinaryVector{n, T}(T(rand(rng, 0:big(2)^n-1)))
end

function rand(rng::AbstractRNG, ::SamplerType{BinaryVector{n, T}}, dims...) where {n, T}
    return BinaryVector{n, T}.(T.(rand(rng, 0:big(2)^n-1, dims...)))
end

function rand(rng::AbstractRNG, ::SamplerType{BinaryVector{n}}) where {n}
    T = typerequired(n)
    return rand(BinaryVector{n, T})
end

function rand(rng::AbstractRNG, ::SamplerType{BinaryVector{n}}, dims...) where {n}
    T = typerequired(n)
    return rand(BinaryVector{n, T}, dims...)
end


a = rand(BinaryVector{4, UInt8})
b = rand(BinaryVector{4, UInt8}, 3)
c = rand(BinaryVector{4})
d = rand(BinaryVector{4}, 3)
1

There are 1 best solutions below

0
Bill On

I believe that rand() is streamlining your type specification. Normally this would not affect the eventual output. You can however avoid using the vector construction version of rand() by using a for loop construct instead:

julia> d2 = [rand(BinaryVector{4}) for _ in 1:3]
3-element Vector{BinaryVector{4, UInt8}}:
 BinaryVector{4, UInt8}(0x09)
 BinaryVector{4, UInt8}(0x0a)
 BinaryVector{4, UInt8}(0x0a)

julia> typeof(d2)
 Vector{BinaryVector{4, UInt8}} (alias for Array{BinaryVector{4, UInt8}, 1})