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).
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.
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)