Crystal: Add `T.new` in the type grammar

Created on 14 Apr 2016  路  5Comments  路  Source: crystal-lang/crystal

Right now we can do this:

class Foo; end
class Bar < Foo; end

a = [] of Foo.class
a << Foo
a << Bar

With Foo.class we are saying we want classes, not instances, so we go from Foo as an instance type to Foo.class as a class type.

It would seem that the opposite is not needed, because we can simply write:

a = [] of Foo # this implicitly means Foo instances

However, we've recently stumbled upon a case where this might be needed, and having it as a feature for completeness also makes sense, I think.

The code basically lets the user provide a tuple of types, and from there read rows from a DB that will be automatically cast to those types:

class Result(T)
  def initialize(@types : T, @query : String)
  end

  @rows : Array(Array(T.new))

  def rows
    # lazily compute rows
    @rows ||= ...
  end
end

def exec(types, query)
  Result.new(types, query)
end

exec({Int32, String}, "select ...")

The problem is that T in Result(T) is {Int32.class, String.class}, because we didn't pass {1, "foo"} to it, but we want @rows to be {Int32, String}, not {Int32.class, String.class}. By using T.new we can do this (by also adding the rule that doing T.new on a tuple of classes gives you back a tuple of instances, just like when you ask the class of a tuple it gives you a tuple of classes).

Another possible name instead of T.new could be T.instance, but I think T.new is nice because you basically want the type the results from _newing_, or creating an instance, of T.

As a final note, T.new with T being an instance type would just return T (no change), so one can use it to make sure to get an instance type and never a class type.

draft compiler

Most helpful comment

Actually... this is kind of possible with the current language:

# T has the instance type, U has the class type
class Result(T, U)
  def initialize(@types : U)
  end
end

# Match against a metaclass, T will be the instance type
def exec(types : T.class)
  Result(T, typeof(types)).new(types)
end

p exec(Int32) # => #<Result(Int32, Int32:Class):0x108fd1fd0 @types=Int32>

The problem is that in the original code a class is not passed, but a tuple of classes, so T.class won't match (a tuple instance is not a class).

exec({Int32, Float64})

Using a Tuple restriction doesn't work either:

def exec(types : Tuple(T))
end

exec({Int32, Float64}) # error

That's because Tuple(T) will match against a tuple of one type. What we really want is something like:

# Note that `*` here
def exec(types : Tuple(*T))
  # Here T will be a tuple type
end

exec({Int32, Float64}) 

because in reality a Tuple has variable number of type arguments, expressed itself as a tuple (gotta love recursion :-P).

In addition to all of that, and to implement the original use case, we'd probably want to do Tuple(*T.class) or something like that.

This definitely needs more thought, but for now I don't see that T.new is really needed (it also won't solve the original case, the main problem is the tuple type).

All 5 comments

Actually... this is kind of possible with the current language:

# T has the instance type, U has the class type
class Result(T, U)
  def initialize(@types : U)
  end
end

# Match against a metaclass, T will be the instance type
def exec(types : T.class)
  Result(T, typeof(types)).new(types)
end

p exec(Int32) # => #<Result(Int32, Int32:Class):0x108fd1fd0 @types=Int32>

The problem is that in the original code a class is not passed, but a tuple of classes, so T.class won't match (a tuple instance is not a class).

exec({Int32, Float64})

Using a Tuple restriction doesn't work either:

def exec(types : Tuple(T))
end

exec({Int32, Float64}) # error

That's because Tuple(T) will match against a tuple of one type. What we really want is something like:

# Note that `*` here
def exec(types : Tuple(*T))
  # Here T will be a tuple type
end

exec({Int32, Float64}) 

because in reality a Tuple has variable number of type arguments, expressed itself as a tuple (gotta love recursion :-P).

In addition to all of that, and to implement the original use case, we'd probably want to do Tuple(*T.class) or something like that.

This definitely needs more thought, but for now I don't see that T.new is really needed (it also won't solve the original case, the main problem is the tuple type).

I'm having troubles with getting the work around to work for something slightly different than the original issue.

class Result(T)
  def initialize(@types : T)
  end

def each
  each(@types) { |row| yield row }
end

private def each(types : Array(AnyUnion))
  result.stream_data {|row| yield decode(row) } 
end

private def each(types : Tuple(Class))
  result.stream_data {|row| yield decode_and_cast(row) } 
end
private def each(types : Tuple(Class, Class))
  result.stream_data {|row| yield decode_and_cast(row) } 
end
# and so on,

works fine, which is great. You can call result#each and stream out the data just fine, even casted down to single types. But if you do want to roll up all the rows into an array, it becomes difficult:

def to_a
  arr = Array(T).new
  each { |row| arr << row}
  arr
end

you'll get errors for example
no overload matches 'Array({String:Class, String:Class, Time:Class})#<<' with type {String, String, Time}

I think maybe I can have something that pulls out the first row, then iterates over the rest, but I'm not sure yet if that will help.

You can probably do something like Array(typeof(first)).new, where first does each { |row| return row }; raise "oh no"

(for crystal-pg's Result something similar can be done, but using typeof to get the type and then instantiating Result with another type argument U... I tried it but it's not very simple, but it's doable)

Something like the first trick worked :)

    def rows
      a = [first]
      each(@types) { |r| a << r }
      a
    end

    private def first
      each { |row| return row }
      # raise "oh no"
    end

The raise check doesn't seem necessary, what were you going for there? never mind found a reason :)

Was this page helpful?
0 / 5 - 0 ratings