Checking tuple size during compile time

I’m having an issue that makes me think I need to check tuple size at compile time (see below).
Since I still don’t feel very proficient with Crystal’s macros, I’m poking around, but didn’t manage so far coming up with something very useful…

struct Tuple(*T)
    def self.size
        {{@type.size}}
    end
end
abstract class A
    abstract def m(i : Tuple)
end

class B<A
    # no restriction on tuple size
    def m(i : Tuple)
    end
end

class C<A
    # specialization which has restriction on tuple size
    def m(i : Tuple)
        raise("ko") if typeof(i).size < 2 # only a runtime check, doesn't help
        raise("ko") if i.size < 2 # only a runtime check, doesn't help
        # raise("ko") if i.is_a?(Tuple(Int32)) # understood by compiler, but very specific
        i[1] # yields: Error: index out of bounds for Tuple(Int32) (1 not in -1..0)
    end
end

a = [B.new] of A
a[0].m({0})

Think the easiest way would be to do something like:

class C<A
  def m(i : Tuple(*T)) forall T
    {% raise "ko" if T.size < 2 %}
  end
end

Captures the types of the tuple via a free var then checks the size of that tupleliteral type, raising a compile time error based on your business logic.

that was not quite what I needed, but very close :slight_smile: thank you!
I put it in the following snippet along with some more explanatory comments.

Unfortunately, in my real project this just led to the next problem: actually I do have a union type as method argument, as you can see in the fragment below as well. If I uncomment the second to last line I get Error: undefined constant T, because for the Array version T isn’t defined.

I am reluctant of creating redundant code (splitting the method into an array and tuple version); is there a neat macro for dealing with this as well?

abstract class A
    abstract def m(i : Array|Tuple(*T)) forall T
end

class B<A
    # no restriction on tuple size
    def m(i : Array|Tuple(*T)) forall T
    end
end

class C<A
    # specialization which has restriction on tuple size
    def m(i : Array|Tuple(*T)) forall T
        {% if T.size >= 2 %} # supresses: Error: index out of bounds for Tuple(Int32) (1 not in -1..0)
            i[1] # only works if tuple has at least two elements
        {% else %}
            raise("ko") # never called!
        {% end %}
    end
end

a = [B.new] of A
a[0].m({0}) # now compiler also "instantiates" C with #m(Tuple(Int32)), hence macros in C#m necessary

# C.new.m([1]) # this triggers: Error: undefined constant T
# (because here from the union part the array is used...)

Maybe a private method that uses Enumerable as argument, and have public Array and Tuple that delegates to it.

But if you are already allowing Array, from a user perspective if I have a tuple I could call to_a and use the that overload. I’m not sure how much you are gaining by restricting the tuple size.

Either way, at this point another way to restrict the tuple size is by using normal arguments, with as many overloads as you want. def m(a, b), def m(a, b, c), … with or without type restrictions. You can invoke it via m(*tuple) and if there is a match it will called. I would shared the code between the more flexible Array/Enumerable by explicit calls m(a[0], a[1]) if a.size == 2 or something like that.

Understood.

What I wanted to do is to have a collection of generic n-dimensional table classes that allow indexing by the user by both Array and Tuple.
Tuple alone is not enough, since some algorithms need to explicitly be able to generate n-dimensional indices on-the-fly and Tuple dimension is fixed on compile time (as is StaticArray as well).
So, generally I need Array but wanted to have Tuple as well, whenever it’s possible, without bloating the code.

When I have to delegate to a private member (one of your suggestions), I need to do the type checking in the delegate (outside) and will have four delegates (two per #[]? and #[]=). Doesn’t convince me.

I probably could use StaticArray for indexing and make it part of the generic class’ type, but that involves quite some rewrites and I fear the syntax will be bulky… so I’m not looking forward to this.

Working with explicit splats is not generic enough.

So probably I’ll just do away with the Tuples…

What made me wonder from the very beginning is that code not using class C at all is triggering a compiler error because of some type deductions coming from C; it’s the last two lines in the initial example (I’m not sure right now if anybody saw it, because at least at my computer it’s just past the view scope and you have to scroll).

a = [B.new] of A
a[0].m({0})

Obviously, any (concretely typed) method thrown at any placeholder for A needs to be fulfillable by all of its subclasses (e.g. B and C). Caught me by surprise (and gives me headaches). But I guess that’s what inheritance is about.