Moving constants to compile-time "lookup" functions with Val

Hi everyone,

This may be my most ??? question yet. I read this blog about using Val to use arguments at compile time, and I want to take this a step further by using Val to elide a long string of const declarations.

My MWE below is very close to my actual use case, that just has another boolean-like abstract type to contend with. The MWE in question splits a population into 4 categories depending on two boolean conditions: their Reputation and their Colour. I want to calculate the proportion of each category in the population, each time filtering by either Colour, or Reputation or both and I want the calculation of the indices used in the summation to be done at compile time.

e.g.

p = Population(MVector{4, Float64}([0.1, 0.2, 0.3, 0.4]))
p(Red) == 0.3 # 1 and 2 are Red
p(Bad) == 0.6 # 1 and 3 are Bad
p(Bad, Red) == 0.2 # 2 is both Bad and Red
# Calculating the indices used in the last one should be done at
# compile time, the values not.

MWE:

using StaticArrays

abstract type Reputation end
struct Good <: Reputation end
struct Bad <: Reputation end

abstract type Colour end
struct Red <: Colour end
struct Blue <: Colour end

const all_indices = 1:4
const goods = (1, 3)
const reds = (1, 2)

struct Population 
    proportion::MVector{4, Float64}
end

Base.getindex(p::Population, i) = getindex(p.proportion, i)
sum_indices(object, indices) = sum(getindex(object, i) for i in indices)

indices(::Type{Good}) = goods
indices(::Type{Bad}) = Tuple(setdiff(all_indices, indices(Good))) 
indices(::Type{Red}) = reds
indices(::Type{Blue}) = Tuple(setdiff(all_indices, indices(Red)))

indices(conditions...) = indices(conditions)
indices(conditions) = indices(Val(conditions))
function indices(::Val{conditions}) where {conditions}
    return Tuple(Intersect(indices(cond) for cond in conditions))
end

(p::Population)(conditions...) = sum_indices(p, indices(conditions))

p = Population(MVector{4, Float64}(0.1, 0.2, 0.3, 0.4))

p(Red, Good)
p(Good) + p(Bad)

This fails with the stacktrace:

ERROR: TypeError: in Type, in parameter, expected Type, got a value of type Tuple{DataType, DataType}
Stacktrace:
 [1] Val(x::Tuple{DataType, DataType})
   @ Base ./essentials.jl:714
 [2] indices(conditions::Tuple{DataType, DataType})
   @ Main ~/.julia/dev/Socioevolutionary/scripts/mwe.jl:28
 [3] (::Population)(::Type, ::Vararg{Type})
   @ Main ~/.julia/dev/Socioevolutionary/scripts/mwe.jl:33
 [4] top-level scope
   @ ~/.julia/dev/Socioevolutionary/scripts/mwe.jl:37

Is what I’m doing really dumb?
Is there a simpler way (please I hope so)?
Could I have just written out all of the indices myself? (Yes, I did, but then I nerdsniped myself into this question.)

Mh, if proportion is a MVector then it seems impossible to do it at compile time since the value might change at runtime.

It looks clearly like totally unneeded optimisation :stuck_out_tongue: which you seem aware of. So I assume we are here for the fun…

Here is my attempt

red  = Val( (:reds, true) )
blue = Val( (:reds, false) )
good = Val( (:goods, true) )
bad  = Val( (:goods, false) )

p = Val( (; population = (0.1, 0.2, 0.3, 0.4), reds = (1,2), goods = (1,3)) )

indices(::Val{p}) where {p} = eachindex(p.population)
function indices(val_p::Val{p}, ::Val{cond}, conditions... ) where {p, cond}
    cond_indices = getproperty(p, cond[1])
    other_indices = indices(val_p, conditions...)
    if cond[2] == true
        return intersect( cond_indices, other_indices)
    else 
        return setdiff( cond_indices, other_indices )
    end
end

function ratio(val_p::Val{p}, conditions...) where {p} 
    return sum( p.population[i] for i in indices(val_p, conditions...) )
end

ratio(p, red)
ratio(p, red, good)

1 Like

Oh my bad, I just mean that the indices (and intersections thereof) returned should be done at compile time! I’ll update the question.

Beyond the thing about MVectors, there’s also the issue that for T::Type; U::Type typeof((T, U))==Tuple{DataType, DataType} rather than Tuple{Type{T}, Type{U}}.

So one thing you could do is work directly with Tuple{T, U} instead of (T, U).

1 Like

Attempt #2 :wink:

using StaticArrays

struct Population{N, Red, Good}
    proportion::MVector{N, Float64}
end

red  = Val(:red)
blue = Val(:blue)
good = Val(:good)
bad  = Val(:bad)

indices(::Population{N,Red,Good}, ::Val{:red} ) where {N,Red,Good} = Red 
indices(::Population{N,Red,Good}, ::Val{:blue} ) where {N,Red,Good} = setdiff(Red, 1:N)
indices(::Population{N,Red,Good}, ::Val{:good} ) where {N,Red,Good} = Good 
indices(::Population{N,Red,Good}, ::Val{:bad} ) where {N,Red,Good} = setdiff(Good, 1:N)


function indices(p::Population{N,Red,Good}) where {N,Red,Good}
    return 1:N
end

function indices(p::Population{N,Red,Good}, cond, conditions... ) where {N,Red,Good}
    return intersect( indices(p, cond), indices(p, conditions...))
end

function ratio(p, conditions...)
    return sum( p.proportion[i] for i in indices(p, conditions...) )
end

p = Population{4,(1,2),(1,3)}([0.1, 0.2, 0.3, 0.4])

ratio(p, red)
ratio(p, red, good)

Of course, you can change the function names to make the operator call style possible… I just prefer to start with plain syntax when drafting stuff like this.

1 Like

If you really need those values computed at compile time, maybe you need to use generated functions, or store the result of every possible input as fields of Population, you are already using functors anyway.

1 Like