Skip to content

Instantly share code, notes, and snippets.

@asterite
Created November 9, 2014 20:45
Show Gist options
  • Select an option

  • Save asterite/2247449862b60396a00b to your computer and use it in GitHub Desktop.

Select an option

Save asterite/2247449862b60396a00b to your computer and use it in GitHub Desktop.
class Array(T)
def map(&block : T -> U)
ary = [] of U
each { |e| ary << yield e }
ary
end
end
class Foo
end
class Bar < Foo
end
class Baz < Foo
end
a = [Bar.new, Baz.new] # :: Array(Foo+)
a << Foo.new # ok
a << Baz.new # ok
b = a.map { Bar.new } # :: Array(Bar)
b << Foo.new # error: no overload matches 'Array(Bar)#<<' with types Foo
# El problema es que el U de map se infiere a Bar, el tipo más específico.
# Si queremos que b quede de tipo Array(Foo) deberíamos poder especificar ese U.
# Pero... ese U se infiere del tipo del bloque!
#
# En otros lenguajes uno podría pensar que map es map<U>(&block : T -> U)
# y entonces poder hacer:
#
# a.map<Foo> { Bar.new }
#
# Eso no está mal pero es agregarle más cosas al lenguaje.
#
# Una cosa que podemos hacer es castear el tipo del bloque a Foo, así
# el tipo inferido es Foo:
c = a.map { Bar.new as Foo }
# El problema es que antes de Crystal 0.5.3 el `as` funcionaba siempre de la siguiente manera:
# se filtra el tipo del valor (Bar) al tipo destino (Foo). Si filtramos Bar con Foo nos
# queda... Bar! Sería como "restringime Bar a los tipos que heredan de Foo". Para Bar
# el resultado es Bar. Entonces c queda de tipo Array(Bar), no cambió nada.
#
# En Crystal 0.5.3 si al filtrar un tipo A por B el resultado es A entonces el tipo
# resultante en realidad cambia y es B+:
d = a.map { Bar.new as Foo } # :: Array(Foo+)
d << Foo.new # ok
# Un ejemplo similar que no involucra herencia:
a = [1, 2, 3]
b = a.map { |x| x + 1 as Int32 | Float64 } # :: Array(Int32 | Float64) en Crystal 0.5.3, Array(Int32) en Crystal 0.5.2
b << 1.5 # ok en Crystal 0.5.3, error en Crystal 0.5.2
# De esa manera uno convive con la inferencia de tipos (si map infiere el tipo del bloque, ajusto el tipo del
# bloque a lo que yo quiero) en lugar de agregarle más features al lenguaje ;-)
@bcardiff
Copy link

bcardiff commented Nov 9, 2014

El "problema" es que no tenés varianza al usar un bloque.
Si esperás un tipo S2 -> T2 y te pasan S1 -> T1 cuándo se válido usar el bloque sin problema?
O dicho de otra forma, cuándo S1 -> T1 <: S2 -> T2.
Eso debería cumplirse cuando S2 <: S1 y T1 <: T2.

Si tuvieses eso en el lenguaje podrias hacer algo como (cambiando la interaz, con lo cual quizás no te guste)... Pero de esa forma un bloque que -> Bar bien podría usarse como -> Foo por dónde lo tenés que usar.

source = [1, 2, 3]
([] of Foo).collect source { |x| Bar.new }

class Array(T)
  def collect(source : S, &block : S -> T)
    source.each { |e| self << yield e }
    self
  end
end

Pero entiendo que el as lo usas para ayudar un poco al type inference.
Está bien como quedó.

@asterite
Copy link
Author

asterite commented Nov 9, 2014

@bcardiff sí, tu definición es la más correcta en realidad: "es para ayudar un poco al type inference" :-)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment