I would still argue that option 1. makes sense since the group table is logically part of the data and simplifies things a lot.
Sure 16 times more memory consumption sounds like a lot, but if you look at the absolute value, it is: For storing n group elements, we need let’s say n * 16 additional bytes. If you aren’t going to have millions of group elements in your workspace then that extra memory footprint is no big deal on computers nowadays.
(Also, option 1 is by far the easiest solution. No world-age issues, not mutable global state of your module…)
A few other ideas (if you still don’t like option 1):
Option 4:
You could use a macro to insert the group table into computations,
like:
G = SemiGroup()
a, b = G(1), G(2)
x = @sg G a * b
Option 5:
You multiply tuples:
i = 1
j = 10
x = (G, i) * (G, j)
By defining function (G::SemiGroup)(i) = (G,i) you could also write
x = G(i) * G(j)
This allows you to swap between both representations on the fly:
If you have millions of elements, just store the indices.
If you don’t care or if you need to compute stuff, switch to the tuples.
Creating a tuple is also very fast, probably faster than “somehow” finding the group table.
Option 6:
(I don’t like it too much. Since it mutates the global state, which can cause trouble like world-age issues. But maybe it could be improved.)
struct SemiGroupElement{GroupName}
i::UInt8
end
# define a group:
G = :F2
const F2_grouptable = UInt8[1 2; 2 1]
@inline group_table(::SemiGroupElement{G}) = F2_grouptable
function *(a::T, b::T) where {G, T <:SemiGroupElement{G}}
return SemiGroupElement{G}( group_table(a)[a.i, b.i] )
end
e = SemiGroupElement{:F2}(1)
b = SemiGroupElement{:F2}(2)
e * b == b
b * b == e
The names are not chosen well etc.