Flexible inherited types

I understand that I can use self in basic classes when I want to define them in such a way, that they create proper types also in their inherited counterparts.
However, I’m stuck when using that in combination with Hashes, for example:

class A
    @hash = Hash(Int32,self).new
    def m(arg) : self # Error: method B#m must return B but it is returning A+
        @hash.delete(arg).not_nil!
    end
end

class B < A
end

B.new.m(42)

Is that a bug or a feature - or do I miss something? ;)

Correct, however, this doesn’t apply to instance variables because they are inherited from parent classes (meaning you can access @hash in B) — the type of @hash is still Hash(Int32, A).

Adding this to A should help:

class A
  # ...

  macro inherited
    def m(arg) : self
      super.as(self)
    end
  end
end

Then m should be typecast as expected on any descendant classes.

Without fully knowing what you’re trying to do, a question: do you need #m to have a specific return type?

Take a look at some tweaks I made to your example here in the playground. Assuming you want A to manage a hash table of values of A and its sub-classes, then removing the return type for #m (and in arg for my added #p method) lets me get past the compile error.

Apologies if I’ve oversimplified the problem too much or completely missed the point. :slight_smile:

Thanks for all your answers - it seems I started with a too short example…
Leaving away the return type in the initial example helped, also casting the return type helped. In my application, however, I then stumble into the next issue when using WeakKeyMap with this implementation (Hash instead works fine):

require "weak_ref"

class WeakKeyMap(K, V)
    private class Key(T)
        @hasher = Crystal::Hasher.new
        def initialize(k : T)
            @k = WeakRef(T).new(k) # using WeakRef(T) because subclassing doesn't work
        end
        def value
            @k.value
        end
        def ==(other : Key(T))
            value == other.value
        end
        def hash(hasher)
            if value = @k.value
                key_hasher = value.hash(Crystal::Hasher.new)
                @hasher = key_hasher
            else
                # this is crucial, since after GC is setting .value to nil, the hashes are different and deletion doesn't work anymore
                key_hasher = @hasher
            end
            key_hasher.hash(hasher)
        end
    end
    include Enumerable({Key(K), V})
    @hash = Hash(Key(K), V).new
    # only the following three methods are sensibly defined
    def []=(key : K, value : V)
        @hash[Key.new(key)] = value
    end
    def []?(key : K) : V|Nil
        @hash[Key.new(key)]?
    end
    def [](key : K) : V
        self[key]?.as(V) # V may or may not include Nil
    end
    # the following methods don't make much sense for this lazily cleaning container class
    private def delete(key : K)
        raise("n/a")
    end
    private def each(& : (T->))
        raise("n/a")
    end
    # no #size, no #keys nor #values
end

class RootedTree(EdgeType)
    @children : Hash(EdgeType, RootedTree(EdgeType))
    def initialize
        @children = Hash(EdgeType, RootedTree(EdgeType)).new
    end
    def add_subtree(name : EdgeType, subtree = RootedTree(EdgeType).new) : RootedTree(EdgeType)
        @children[name] = subtree
    end
    def remove_subtree(name : EdgeType) : RootedTree(EdgeType)
        @children.delete(name).not_nil!
    end
    def each(&block : EdgeType->)
        @children.each do |e,n|
            yield(e,n)
        end
    end
end

class X(T) < RootedTree(T)
    def remove_subtree(name : T) #: X(T) # leaving away return type helps, hence returning more general type (more OO)
        super#.as(self) # or casting return type helps
    end
end

h = WeakKeyMap(RootedTree(Int32),Bool).new # doesn't work
# h = Hash(RootedTree(Int32),Bool).new # works

rt = RootedTree(Int32).new
h[rt] = true
st = RootedTree(Int32).new
rt.add_subtree(42, st)
rt.each {|e| p e}
rt.remove_subtree(42)

rt = X(Int32).new
h[rt] = true # in case of WeakMap: Error: instance variable '@key' of Hash::Entry(WeakKeyMap::Key(RootedTree(Int32)), Bool) must be WeakKeyMap::Key(RootedTree(Int32)), not WeakKeyMap::Key(X(Int32))
st = X(Int32).new
rt.add_subtree(42, st)
rt.each {|e| p e}
rt.remove_subtree(42)

I’m not sure if there’s a cure for that as well.