Reader interface of multiple encoded types

Hey everyone,

I’m building a library to read several file formats that encode a single type of information (let’s say, A), but the header may encode another type. For example, a file encoding B may also include a A in the header. It should be possible to read both A and B from the same file. I’ve been a using generic abstract class to define the common interface, where the type var specify the encoded type:

class A; end
class B; end

abstract class Reader(T)
  abstract def read : T

  def initialize(@io : IO); end
end

class XYZ::Reader < Reader(A)
  def read : A
    ...
  end
end

I thought using something like abstract def read(T.class) : T to separate the reading of the different types (although the argument would be only used to select the correct method), but I don’t know how to specify multiple types in the type var to enforce that the read methods are implemented accordingly.

class XYZ::Reader < Reader(?)
  def read(type : A.class) : A
    ...
  end

  def read(type : B.class) : B
    ...
  end
end

Any suggestion will be appreciated.

Thanks.

Wouldn’t it just be a union of A and B? I.e. A | B.

See Union types - Crystal

EDIT: However that would require there to be a single method that returns A | B, not two methods that return each type. You might be able to use splat generics and iterate over them to define an abstract read method for each one.

EDIT2: NVM, you dont have access to *T outside of a method.

Maybe just Reader(A, B)?

1 Like

Use modules and include the same module twice:

class A; end
class B; end

module Reader(T)
  abstract def read(type : T.class) : T
end

class XYZ::Reader
  include ::Reader(A)
  include ::Reader(B)

  def initialize(@io : IO)
  end

  def read(type : A.class) : A
    A.new
  end

  def read(type : B.class) : B
    B.new
  end
end

reader = XYZ::Reader.new(STDOUT)
reader.read(A) # => #<A:...>
reader.read(B) # => #<B:...>

This is only if you need to be able to read both kinds of objects independently. If they are not truly independent, simply do XYZ::Reader < Reader(A | B) in the original example.

2 Likes

Thanks everyone for their suggestions.

@Blacksmoke16 Exactly, a union would not work for that reason.

@straight-shoota Using Reader(A, B) would require all readers to declare two encoded types, but most readers would only read one type (B would be Nil?)

@HertzDevil I also tested using modules but the Reader includes a lot more code, so it would be duplicated for each include. I think a stumbled an error at some point due to this sort of thing.

What if you did like

abstract class Reader(T)
  abstract def read : T

  def initialize(@io : IO); end
end

abstract class DualReader(A, B) < Reader(A)
  abstract def read : B
end

class XYZ::Reader < DualReader(A, B)
  def read(type : A.class) : A
    ...
  end

  def read(type : B.class) : B
    ...
  end
end

Assuming there aren’t cases where you need 3, 4, 5, … types this would prob work fine :shrug:.

Maybe there should be another module/interface here? Reader would only take care of reading, and all the other functionality would go in that other type that seems to be missing/separate.

Another question: in your first example ever, is the T used for something other than just specifying what the return type of read is? Because if not, then I think you don’t need a generic type, and you probably don’t need a base type. Just don’t define abstract methods. Sometimes things are simpler if we don’t use types that much.

@Blacksmoke16 I also thought about that, and I think it’s the best approach as there is no more than two types: it’s a main type plus an auxiliary type.

@asterite The idea is to declare a common reader interface for several file formats. The additional non-abstract methods deal with the underlying IO and other stuff. T is only used to specify the return type within the class, but it’s also used in macro code to generate some methods on the specified types, e.g., XYZ::Reader < Reader(Foo) would generate Foo.from_xyz, Foo#to_xyz, etc. I also thought about ditching the Reader interface and just search for read methods in the concrete types to gather the encoded types, but I’d lost any validation of the compiler as a simple typo (red) would break this silently.

If there are other ways to accomplished this, suggestions are very welcome.

Will the code be the same? Because in that case Crystal will use the last one. Or put another way: it won’t matter that the code is duplicate, because in the executable it will be there just once. That is, if the duplicated code is exactly the same. It costs nothing to the compiler to declare a same method many times.

I see, but as I mentioned, I found an error due to duplicate code, but I cannot remember what it was :confused: