Search code examples
crystal-lang

Recursive type not unifying with itself


The following code fails to compile with error:

type must be Tuple(Thing::Ish, Slice(UInt8)), not Tuple(Array(Array(Thing::Ish) | UInt8) | UInt8, Slice(UInt8))

These two types seem equivalent to me... and adding an .as(Ish) in the right place works... what am I missing? Why do these types not unify?

module Thing
    alias Ish = UInt8 | Array(Ish)

    def self.decode(bytes : Bytes) : {Ish, Bytes}
        case bytes[0]
            when 0x00..0x17
                {bytes[0], bytes + 1}
            when 0x18
                MultiItemDecoder.new(0x80, ->(x: Bytes) { Thing.decode(x) }).decode(bytes)
            else
                raise "unknown"
        end
    end

    class MultiItemDecoder(T)
        def initialize(@base : UInt8, @item_decoder : Bytes -> {T, Bytes})
        end

        def decode(bytes): {Array(T), Bytes}
            decode_some(bytes + 1, bytes[0])
        end

        def decode_some(bytes, n)
            items = n.times.map do
                item, bytes = @item_decoder.call(bytes)
                item
            end
            {items.to_a, bytes}
        end
    end
end

Solution

  • This works:

    module Thing
      alias Ish = UInt8 | Array(Ish)
    
      def self.decode(bytes : Bytes) : {Ish, Bytes}
        case bytes[0]
        when 0x00..0x17
          {bytes[0], bytes + 1}
        when 0x18
          MultiItemDecoder.new(0x80, ->(x : Bytes) { Thing.decode(x) }).decode(bytes)
        else
          raise "unknown"
        end
      end
    
      class MultiItemDecoder(T)
        def initialize(@base : UInt8, @item_decoder : Bytes -> {T, Bytes})
        end
    
        def decode(bytes) : {Ish, Bytes}
          decode_some(bytes + 1, bytes[0])
        end
    
        def decode_some(bytes, n)
          items = n.times.map do
            item, bytes = @item_decoder.call(bytes)
            item.as(Ish)
          end
          {items.to_a.as(Ish), bytes}
        end
      end
    end
    
    Thing.decode(Bytes[1, 2, 3])
    

    The thing is that Array(Array(Ish)) is not an Array(Ish), because for that it must be Array(Array(Ish) | UInt8) (note that it's an array of a union).

    This all boils down to how things are represented in memory.

    My advice is to avoid using recursive aliases. They are not intuitive and we might eventually remove them from the language.