Type parameter used in annotation

I want to use a type alias in a generic type. but here I got into trouble:

class OrderedMap( K , V)
  enum Color : UInt8
      BLACK
      RED
  end
  private RED = Color::RED
  private BLACK = Color::BLACK
  class Node(K1,V1)
     #......
  end

  alias PNode = Pointer(Node(K,V))
  
  @root : PNode = PNode.null
......
end

this doesn’t work :

In lib\ordered_map.cr:38:30

 38 | alias PNode = Pointer(Node(K,V))
                                 ^
Error: undefined constant K

I tried to translate it from c++

class OrderedMap
    //struct Node ------ failed in crystal
    using pnode = *node<K,V> //the equivalent annotation

this type appears very often, so I do need a shorter name for it,instead of writingPointer(Node(K,V)) everywhere. Is there a better or just practicable way in crystal?

1 Like

I think you would write just Pointer(Node).
But I think it won’t let you do a Pointer to Node.

Unfortunately I can’t tell you what you should use instead :(

I wold better use alias MyNode = Node(K,V) because Node is a subclass of Reference,so there’s no eed to use a pointer.

the fitst insert method seems like

  def insert(key : K ,value : V) : self
    self.class.tree_insert(@root,key,value)
    self
  end
  protected def self.tree_insert(this : Node(K,V) ,key : K ,value : V,father : Node(K,V)? = nil)
    unless this
      this = Node(K,V).new(key,value)
      return
    end
    case cmp = key <=> this.key
    when cmp.zero?
      this.value = value
    else 
      new_node : Node(K,V)? = nil
      if cmp.positive?
        if this.right
          self.tree_insert(this.right,key,value,this)
        else
          new_node = this.right = Node(K,V).new(key,value,RED)
        end
      else cmp.negative?
        if this.left
          self.tree_insert(this.left,key,value,this)
        else
          new_node = this.left = Node(K,V).new(key,value,RED)
        end
      end
      if new_node && this.color.red? && father
        self.tree_recolor_balance(new_node.as(Node(K,V)),this.as(Node(K,V)),father.as(Node(K,V)))
      end
    end
  end

however, you can’t write this because when you assign this with a new object, you doesn’t change @root. so therefore the whole method did nothing:

PS C:\Users\homodeluna\Desktop\tests\cr_ordered_map> crystal spec
#<OrderedMap(Int32, Int32):0x1c9b0250b40 @root=nil>
#<OrderedMap(Int32, Int32):0x1c9b0250b40 @root=nil>
#<OrderedMap(Int32, Int32):0x1c9b0250b40 @root=nil>
#<OrderedMap(Int32, Int32):0x1c9b0250b40 @root=nil>
#<OrderedMap(Int32, Int32):0x1c9b0250b40 @root=nil>
#<OrderedMap(Int32, Int32):0x1c9b0250b40 @root=nil>
#<OrderedMap(Int32, Int32):0x1c9b0250b40 @root=nil>
#<OrderedMap(Int32, Int32):0x1c9b0250b40 @root=nil>

Pointer(Node) is also illegal:

In lib\ordered_map.cr:39:25

 39 | alias PNode = Pointer(Node)
                            ^---
Error: can't use OrderedMap::Node(K1, V1) as a generic type argument yet, use a more specific type

Correct me if I’m wrong, but I’m pretty sure the gist of the problem is you can’t have an alias that uses generics from another type like that.

Right. I don’t think there’s any way around this. You’ll just have to type a bit more. The implementation of Hash(K, V) has Entry(K, V) and we type Entry(K, V) in many places.

Maybe you can use a method or a macro?

Like…

macro pnode
  Pointer(Node(K, V))
end

It would also work if that’s a method.

pnode can’t be used in a type annotation, because this lower-case word isn’t regarded as a type:

protected def self.tree_insert(this : pnode ,key : K ,value : V,father =pnode.null)

In lib\ordered_map.cr:54:41

 54 | protected def self.tree_insert(this : pnode ,key : K ,value : V,father =pnode.null)
                                            ^
Error: unexpected token: "pnode"

maybe the best way up to now is to writePointer(Node(K,V)) every time I need.

about the alias command, is aliasing a type with type parameter just unpreferred?(I saw the hash implementation you do write Entry many times) or it’s just because this functionality is too far to implement? I thought such a function would be useful and has been implemented in many other languages.

Somewhat related: Request for generic alias · Issue #2803 · crystal-lang/crystal · GitHub

1 Like

The main issue is that types exist unrelated to type parameters.

If you have this:

class Foo(K, V)
  class Bar
  end
end

Then the class Bar exists only once for all Foo. You can do things like Foo::Bar and it works just fine.

But if you introduce something like this:

class Foo(K, V)
  alias Bar = Something(K, V)
end

then if you do Foo::Bar… what does that even mean? Maybe that’s an error and it tells you that K and V are unknown.

But then if you have a method inside foo:

class Foo(K, V)
  def foo
    Bar.new
  end
end

how would the compiler (or user) know that the K and V to use for Bar have to be the ones for Foo? It’s not clear…

It’s just too complex to understand and implement in my opinion. Or put another way: we can live without this feature. We could implement it, but it’s far from trivial.

that means this alias is contradictory with the “class is namespace” design. If we divide the space by each composition of type parameter, you’ll get a grand number of namespaces.

Just a hypothesis, I prefer seperating the namespaces, where Foo(Int32,Int32)::Bar and Foo(String,UInt32)::Bar both exists on thier own, and on the other side,Foo::Bar without specifying a type parameter is ambiguious.

This route isn’t wide and flat, and will result into countless details.Generic methods, generic restrictions, overload between many generic methods with different restrictions, partial instantiation … it’s just like learning c++ again.

maybe there’s a simpler way.

I suppose it would be thinkable to have constants (which includes aliases and types) at the generic instance type scope. But this would require a special syntax to distinguish them from the uninstantiated metaclass scope.
It’s probably not a very common use case, otherwise this problem would probably appeared more often.
And there’s a simple solution, instead of using an alias, just use its expansion directly everywhere, and it should work as expected (if not, you’re in a wrong scope again).

As someone who misses this feature, I would consider the following:

class Foo(K, V)
  alias Bar = Something(K, V)

  def foo : Bar
     Bar.new
  end
end

Foo::Bar.new # error can't infer the type parameters K, V for the generic class Something(K, V). Please provide them explicitly
Foo(Int32, String)::Bar.new # OK

(If we can be smarter with the error, then that’d be great)
I know this is hard from a technical point of view, because alias aren’t just an expansion, but theoretically speaking I don’t see a problem with consider them “just” an expansion for this use case.

3 Likes