LoadError: ArgumentError: Sampler for this object is not defined

Hi All,

I’m learning Julia and coming from R and a bit of Python. To help me figure Julia out (in a way that’s directly related to my current work), I’ve decided to write a module based on the Vose/Walker Alias method for sampling randomly from arbitrary discrete distributions. I realize there are some relevant functions in the StatsBase and Distributions packages, but I find learning through errors is helpful. I have encountered an error, though, that’s proven to be hard for me to resolve on my own and I’m hoping someone can point me in the right direction. I’m sure this problem boils down to confusion about Types and how to work with them properly.

Here’s the code:

#= implement the Vose algorithm in Julia http://www.keithschwarz.com/darts-dice-coins/
=#

using Random

import Base.rand
import Random: Sampler, SamplerSimple, SamplerTrivial, Repetition, eltype

rng = MersenneTwister()

n = 5
a = collect(1:n)
b = collect(1:n)
c = rand(Float32,n)

# discrete distribution struct
struct DiscreteDistribution
    prob::Vector
    support::Vector
end

struct AliasTable
    prob::Vector
    support::Vector
    accept::Vector
    alias::Vector
end

dd = DiscreteDistribution(c,a)

# build alias table
function vose_method(dd::DiscreteDistribution)
    n_prob = length(dd.prob)
    n_support = length(dd.support)
    sum_prob = sum(dd.prob)
    prob_scaled = dd.prob * n_prob
    accept = Vector(undef, n_prob)
    alias = Vector{typeof(dd.support[1])}(undef, n_prob)
    small = collect(1:n_prob)[prob_scaled .< 1]
    large = collect(1:n_prob)[prob_scaled .>= 1]
    while length(large) > 0 && length(small) > 0
        s = popfirst!(small)
        l = popfirst!(large)
        accept[s] = prob_scaled[s]
        alias[s] = dd.support[l]
        prob_scaled[l] = (prob_scaled[l] + prob_scaled[s]) - 1
        prob_scaled[l] < 1 ? push!(small, l) : push!(large, l)
    end
    while length(large) > 0
        l = popfirst!(large)
        accept[l] = 1
    end
    while length(small) > 0
        s = popfirst!(small)
        accept[s] = 1
    end
    return AliasTable(dd.prob, dd.support, accept, alias)
end

# sampling method
function sample_vose(rng, aliastable::AliasTable)
    j = rand(rng, 1:length(aliastable.support))
    outcome = rand(rng) <= aliastable.prob[j] ? aliastable.support[j] : aliastable.alias[j]
end

# new sampler
function Random.Sampler(::Type{<:AbstractRNG}, d::DiscreteDistribution, ::Repetition)
    SamplerSimple(d, vose_method(d))
end

Random.eltype(::Type{<:DiscreteDistribution}) = Int

sp = Random.Sampler(rng, dd)

isa(sp, SamplerSimple{<:DiscreteDistribution})

# extending rand
function Random.rand(rng::Type{<:AbstractRNG}, sp::SamplerSimple{<:DiscreteDistribution})
    sample_vose(rng, sp.data)
end

rand(rng, sp)

The last function call returns the error,

LoadError: ArgumentError: Sampler for this object is not defined

But, isa(sp, SamplerSimple{<:DiscreteDistribution}) returns true, so I’m not sure where the dispatch is running into problems.

Any information would be greatly appreciated. I did read the docs in the Standard Library for the Random module and was able to produce a similar error when the example function make_alias_table is defined to return a custom type (without that, if it returns a vector for example, the dispatch seems to work as expected). But, I can’t see why that’s not working either. In case it helps, here’s the code for the example straight out of the docs with the modification to make_alias_table just described,

using Random

import Base.rand
import Random: Sampler, SamplerSimple, Repetition, eltype

rng = MersenneTwister()

struct DiscreteDistribution
    probabilities::Vector
end

struct OtherType
    b::Vector
end

Random.eltype(::Type{<:DiscreteDistribution}) = Int

dd = DiscreteDistribution([0.2,0.2,0.2,0.2,0.2])

function make_alias_table(p)
    OtherType(p)
end

function Random.Sampler(::Type{<:AbstractRNG}, distribution::DiscreteDistribution, ::Repetition)
    SamplerSimple(distribution, make_alias_table(distribution.probabilities))
end

sp = Sampler(rng, dd)

function draw_number(rng, sp)
    rand(rng, sp.data)
end

function rand(rng::AbstractRNG, sp::SamplerSimple{<:DiscreteDistribution})
    draw_number(rng, sp)
end

Many thanks for any help!
Chris

I looked only at the simplified second example, the problem is that in your draw_number function, you call rand on sp.data which is of type OtherType, but you didn’t define what this should do. When OtherType is replaced with Vector, the Random module already knows how to sample from that. So you can for example add

Random.Sampler(::Type{RNG}, ot::OtherType, n::Repetition) where {RNG<:AbstractRNG} =
    SamplerSimple(ot, Sampler(RNG, ot.b, n))

rand(rng::AbstractRNG, sp::SamplerSimple{<:OtherType}) = rand(rng, sp.data)

Ah of course. I think that’s probably it. I’ll double check and then mark this as the solution. Thanks!

Okay, so you were right about the simplified example. Once corrected, I was then able to track down the type mismatch in the signature for the call to rand in the first example. Apparently, julia did not like the variable rng having the type constraint of ‘Type{<:AbstractRNG}’. After changing it to simply ‘rng::AbstractRNG’ (as it appeared in the simplified example), the first example worked. I don’t understand why yet. But, for those interested and maybe working through the documentation as I was, here’s the revised code for the first example that appears to be working:

#= implement the Vose algorithm in Julia http://www.keithschwarz.com/darts-dice-coins/
=#

using Random

import Base.rand
import Random: Sampler, SamplerSimple, SamplerTrivial, Repetition, eltype

rng = MersenneTwister()

n = 5
a = collect(1:n)
b = collect(1:n)
c = rand(Float32,n)

# discrete distribution struct
struct DiscreteDistribution
    prob::Vector
    support::Vector
end

struct AliasTable
    prob::Vector
    support::Vector
    accept::Vector
    alias::Vector
end

dd = DiscreteDistribution(c,a)

# build alias table
function vose_method(dd::DiscreteDistribution)
    n_prob = length(dd.prob)
    n_support = length(dd.support)
    sum_prob = sum(dd.prob)
    prob_scaled = dd.prob * n_prob
    accept = Vector(undef, n_prob)
    alias = Vector{typeof(dd.support[1])}(undef, n_prob)
    small = collect(1:n_prob)[prob_scaled .< 1]
    large = collect(1:n_prob)[prob_scaled .>= 1]
    while length(large) > 0 && length(small) > 0
        s = popfirst!(small)
        l = popfirst!(large)
        accept[s] = prob_scaled[s]
        alias[s] = dd.support[l]
        prob_scaled[l] = (prob_scaled[l] + prob_scaled[s]) - 1
        prob_scaled[l] < 1 ? push!(small, l) : push!(large, l)
    end
    while length(large) > 0
        l = popfirst!(large)
        accept[l] = 1
    end
    while length(small) > 0
        s = popfirst!(small)
        accept[s] = 1
    end
    return AliasTable(dd.prob, dd.support, accept, alias)
end

# sampling method
function sample_vose(rng, aliastable::AliasTable)
    j = rand(rng, 1:length(aliastable.support))
    outcome = rand(rng) <= aliastable.prob[j] ? aliastable.support[j] : aliastable.alias[j]
end

# new sampler
function Random.Sampler(::Type{<:AbstractRNG}, dd::DiscreteDistribution, ::Repetition)
    SamplerSimple(dd, vose_method(dd))
end

sp = Random.Sampler(rng, dd)

# extending rand
function rand(rng::AbstractRNG, sp::SamplerSimple{<:DiscreteDistribution})
    sample_vose(rng, sp.data)
end

rand(rng, sp)

If anyone can explain the difference, then, between rng::Type{<:AbstractRNG} and rng::AbstractRNG to me, I’d appreciate it. And, why does it seem to work in the call to Random.Sampler, but not in the call to rand?

rand expects an AbstractRNG object, not a type. It’s like the difference between 1 and Integer; 1 is an instance of an Int <: Integer, whereas Integer is a type. Hopefully this example helps illustrate:

julia> f(a::Integer) = a
f (generic function with 1 method)

julia> f(::Type{<:Integer}) = "f(Integer) was called"
f (generic function with 2 methods)

julia> f(1)
1

julia> f(Int)
"f(Integer) was called"

rand wants an AbstractRNG object, whereas Random.Sampler just wants the type. Not sure why Random.Sampler just needs the type though.

Of course, I see it now. Thanks!

Generating a random value has two stages, calling Sampler and then calling rand. Sampler is not meant to consume entropy bits (only rand is), so the rng type is enough.