I wrote a library tallboy to generate text based tables a while ago to learn Crystal.
Recently I rewrote it and Initially I was looking for golang like interfaces but in Crystal you have to explicitly include modules as interfaces.
Later I found that Crystal has structural typing and it does 90% of what I wanted but I am unsure the following.
# say I have an array of integers
arr = [1, 2, 3]
# and and class Group which holds an array of integers or strings
class Group
def initialize(@items : Array(Int32 | String)
end
end
# I can't just pass the array in
Group.new(arr)
# I have to first type cast each element
Group.new(arr.map(&.as(Int32 | String)))
Is using .as the best approach here and does anyone actually define modules as interfaces? I found using modules it’s cumbersome to cast back and forth.
and if you checked out the library any feedback at all I would love to hear it.
It depends a bit on the greater part of the code but two alternative venues may be either directly declaring the array of the right type, [1, 2, 3] of Int32|String or being more liberal in what your constructor accepts, @items : Array(Int32)|Array(String)|Array(Int32|String)
An alternative is doing the map inside Group’s constructor. Then client code doesn’t need to worry about mapping, they can just pass array of int, array of string, or array of both.
But I don’t see how this is related to modules and interfaces.
Thanks asterite for giving me another idea to try.
I really enjoy some the structural typing support because different objects just need to implement the same method and it just works. Whereas when trying to use modules as interfaces I found it too be cumbersome so I’m just wondering if that’s the right approach in Crystal.
A good solution would probably to provide a .new overload which casts the array to the expected data type:
class Group
def self.new(items)
new(items.map(&.as(Node))
end
def initialize(@items : Array(Node))
end
end
Another alternative could be to make Group generic and add a generic argument to define the type of @items. But I suppose that probably wont fit nicely with your usecase.
Unfortunately the .new overload didn’t work in the following which should
alias ItemType = String | Int32 | Bool
class Group
def self.new(arr)
new(arr.map(&.as(ItemType)))
end
def initialize(@arr : Array(ItemType))
end
end
Group.new([1, 2])
it’s pattern matching the initialize method instead of the .new with this error
121 | def initialize(@arr : Array(ItemType))
^---
Error: instance variable '@arr' of Group must be Array(Bool | Int32 | String), not Array(Int32)
I can get around it by using a different method name
class Group
def self.from(arr)
new(arr.map(&.as(ItemType)))
end
end
Maybe a bug, but it’s actually a pending design decision around covariance and contravariance. The restriction Array(ItemType) will match an Array(Int32).
But there’s a simpler way:
alias ItemType = String | Int32 | Bool
class Group
@array : Array(ItemType)
def initialize(array : Array)
@array = array.map(&.as(ItemType))
end
end
Group.new([1, 2])