Crystal: Create array of module type

Created on 4 Jan 2017  路  31Comments  路  Source: crystal-lang/crystal

This should work:

module Moo
end

class Foo
  include Moo
end

class Bar
  include Moo
end

a = [] of Moo.class
a << Foo
a << Bar
feature compiler

All 31 comments

While I really like the expressibility this adds, "[] of Moo.class" looks a quite weird at first glance, as I'd assume that returns Module, meaning that a would be a Array of arbitrary modules.

Speaking of which, I can find no api docs for modules.

@yxhuvud you can interpret Moo.class to be a class that implements Moo interface, similar to what you do with Moo by itself:

ary = [] of Moo

Is interpreted as an array of Moo instances (or any object instance that implements Moo interface).

@yxhuvud re: api docs for Module: the only docs shown for class are due included/injected methods (serialization, comparison, etc).

You can find Module and Class documentation in the docs:

@luislavena But then #class for modules would do something utterly different compared to #class everywhere else. I object to the naming in the API, not the functionality of it. Moo.includer is probably what I'd would have choosen, as that also gives an obvious Moo.extender for the classes that has been extended by the module.

As for interpretations of implementing interfaces, would this allow for duck typed classes that have methods that match the API without including it which is what your suggested interpretation boils down to? I assumed that it didn't, but if it does, then I'd instead suggest a = [] of Moo.interface. Of course, the alternatives between includers and duck typed interfaces are not exclusive - both are possible at the same time.

As for Class API documentation, that exist at https://crystal-lang.org/api/0.20.3/Class.html. I guess the question can be boiled down into: Are modules first class objects that can be passed around, the way classes are? Sure, you can't include them dynamically the way you can instantiate classes, but they start getting methods and used for instantiating stuff like generics like Arrays, then they probably should be, and also have their own API docs like everything else.

@yxhuvud It already works like that, check the docs, section "Classes as restrictions"

@asterite: how about the extend case:

module Moo
end

class Foo
  extend Moo
end

class Bar
  extend Moo
end

a = [] of Moo.???
a << Foo
a << Bar

@yxhuvud That already works, if you do [] of Moo, because when you extend, you make the "metaclass" inherit the module.

I admit it's a bit confusing, but classes and metaclass are confusing.

I think I just got more confused, because for classes [] of Moo would mean [] is an array of instances of Moo, and I'd have expected both a and b to work:

module Moo
end

class Foo
  include Moo
end

a = [] of Foo
a << Foo.new
b = [] of Moo
b << Foo.new

That is, the elements of the array have the instance methods of the class/module.

(btw, you can use triple backticks followed by "cr" to get syntax coloring for crystal)

Yes, the above snippet works :-)

When you include Moo in Foo, instances of Foo are "instances" of Moo. So it works.

When you extend Moo in Foo, the class Foo is an "instance" of Moo.

When you do [] of Foo.class it means "array of class Foo or subclasses of Foo" (but not instances, just classes). What's missing is the same concept for "array of types (not instances) that include a module".

@asterite: Glad to hear that. Then I wasn't as confused as I thought. Then I repeat my question about the extend case, ie array of types that extend from a module to use your phrasing.

What question? :-)

It works:

module Moo
end

class Foo
  extend Moo
end

class Bar
  extend Moo
end

a = [] of Moo
a << Foo
a << Bar

But the use case is this:

module Action
  abstract def exec
end

class Jump
  include Action

  def exec
    puts "Jumping!"
  end
end

class Run
  include Action

  def exec
    puts "Running!"
  end
end

Now if you want an array of types that include Action, I can't do it. I might want to do that because I will create different instances, maybe pass them something in the constructor.

Ok, confusion gone after trying around examples. The case of arrays of instances from classes that extends from a module is missing, but perhaps that is a lot less necessary to have as it usually is the api of the generic content and not the api of the class of the generic argument that is relevant. Sorry for spamming your notification inbox :)

Related: https://forum.crystal-lang.org/t/array-co-something-inconsistency/238. Either @asterite example or [] of Moo.module should work in the end.

In the same regard, this currently does not work:

module TestInterface
end

class Test
  include TestInterface

  def build_tests : Array(TestInterface)
    [Test.new]
  end
end

It fails at compile time with Error: method must return Array(TestInterface) but it is returning Array(Test).

@pyrsmk [Test.new] of TestInterface

See https://github.com/crystal-lang/crystal/issues/3803.

Return types are checks, they don't give types to expressions.

So bad, this is a strong limitation with the interface paradigm. But I can understand this is a tricky subject.

Well, actually, in this case one would expect [Test.new] to kind of match Array(TestInterface) but then there's the covariance stuff so it's not clear what should be done here.

@asterite Maybe a workaround would be to add a way to say that value must be an Array of descendants of Type. By explicitly defining what we want, it can do the trick (maybe?).

Yes, that's essentially what covariance and conteavariance annotation are for. See for example C# who has them (I think kotlin too). However, implementing something like that in Crystal is not only a huge task, it might also be impossible to do because type annotations are optional.

I understand. So a better approach would be to rethink how I'm handling my return types with arrays and interfaces. ~For example, having a SomethingCollection class/struct that accepts any SomethingInterface would do the job.~

Doesn't work either since I still rely on an Array of SomethingInterface 馃槄

What you want to do is already possible. Maybe show us some code that's not working?

Note that this issue is about arrays of module types, not module instances. Your case should work. That the return type doesn't match the array type is a separate issue.

Note that this issue is about arrays of module types, not module instances.

Yes, I noticed the difference from my case afterward 馃槄

That the return type doesn't match the array type is a separate issue.

I agree, but it's strongly linked to my case anyway IMHO. Take this NodeCollection example:

module NodeInterface
end

struct Node
  include NodeInterface
end

module NodeCollectionInterface
  include Enumerable(NodeInterface)
  abstract def with(node : NodeInterface) : NodeCollectionInterface
end

struct NodeCollection
  include NodeCollectionInterface

  @nodes = [] of NodeInterface

  def initialize(@nodes : Array(NodeInterface)); end

  def each
    @nodes.each { |node| yield node }
  end

  def with(node : NodeInterface) : self
    self.class.new(@nodes.concat([node]))
  end
end

NodeCollection.new([Node.new, Node.new]).with(Node.new)

As expected, the compile fails on the initialize. And I can't see a way of handling the input otherwise.

@pyrsmk Replace your last line with this:

NodeCollection.new([Node.new, Node.new] of NodeInterface).with(Node.new)

I'm actually not sure this issue is valid, the original snippet doesn't make sense, now that I look back at it.

Woh! I wasn't aware that I could do that... So it seems there wasn't really no issue at all... 馃槄 Thanks!

What doesn't make sense in my snippet?

The type of an array is deduced from its elements. When you write [Node.new] the type becomes Array(Node). That's not compatible with Array(NodeInterface) because you can only add Node to that array, not any kind of NodeInterface. When you write [Node.new] of NodeInterface you are saying that the array is of NodeInterface, not just Node.

This is the one thing that bites Crystal developers the most. I don't know how we can improve the situation.

That completely makes sense now that you explained it, and to be honest this is one thing I didn't remember after reading the guide.

But the explanation is right here, at the bottom of the inheritance chapter... So I feel silly.

Maybe it needs more visibility in the summary, directly at the top with other edge cases.

This is the one thing that bites Crystal developers the most. I don't know how we can improve the situation.

Interesting, I always thought this was very clear.
(I mean, it is only expected that if a person writes Smth.new, Crystal can't auto-detect this as being anything other than type Smth. If a user wants to change or extend that, they need to be specify it manually like with that of NodeInterface)

Was this page helpful?
0 / 5 - 0 ratings

Related issues

asterite picture asterite  路  60Comments

benoist picture benoist  路  59Comments

MakeNowJust picture MakeNowJust  路  64Comments

stugol picture stugol  路  70Comments

fridgerator picture fridgerator  路  79Comments