The Crystal Programming Language Forum

Forall of type union

This compiles but doesn’t do anything useful, it just puts one of the argument types in both A and B, and I am wondering if I should file it as a bug or not.

def contains(type : A|B) forall A, B
    puts "Called for union."
end

The thing here is that the parameter type restrictions are used to solve the method dispatch, but the invocation happens with actual types.

With the current state is like if the parameter types restrictions are removed completely and a method specialization appears instead for the call site.

What is the call to that method and what would you expect to happen?

Also note that mathematically, and in the language, T | T is just T. So A and B could be the same type just fine.

This was intended to be a method to emit the type of the contents of a Generic.
I have implementations like this:

def contains(type : Array(T)) forall T
  T
end

def contains(type :  Hash(K, V)) forall K, V
  V
end

The problem is that sometimes there are union types such as Array(X) | Nil. So, I can have an implementation like this:

def contains(type : A.class|B.class) forall A, B
end

and that will be called, but just returns one type in the union, the same one for both A and B.

I guess I have no choice but to use macros and pick apart TypeNodes?

Thanks

Bruce

Would something like this do the trick for you?

def remove(a, t : T.class) forall T
  raise "" if a.is_a?(T)
  a
end

v = 3 || "str"
pp!({v, v.class, typeof(v)}) # => {3, Int32, (Int32 | String)}

w = remove(v, String)
pp!({w, w.class, typeof(w)}) # => {3, Int32, Int32}

not_nil! and compact_map does something similar to remove Nil from the result. This is a more generic instance of that.

I would not assume unions have an order.

Well, that is the most interesting expression I’ve seen in the language so far. It looks like it’s forcing the else condition of a is_a?(T), but I don’t understand how. Is this a guaranteed feature of the language?

If a == "str" the remove(a) would have raise on runtime, is not different from {Object, Nil}#not_nil!.

Since raise "" : NoReturn the returning type is typeof(a) - T | NoReturn which is typeof(a) - T .

The program is equivalent to:

def remove(a, t : T.class) forall T
  if a.is_a?(T)
    raise ""
  else
    a
  end
end

Maybe that has a more straightforward interpretation.

What is interesting is that S - T can’t be expressed in the type grammar, but yet this things can be done.

Enumerable(T)#compact_map is able to return Array(T - Nil) due to typeof + not_nil!

  def compact_map
    ary = [] of typeof((yield first).not_nil!)
    each do |e|
      # ...
    end
    ary
  end

Thank you. I think that because this:

def one_type_of(type, A|B|C|D|E|F|G|H), forall A,B,C,D,E,F,G,H
   A
end

Returns exactly one of the types in the union type, I think I can use the remove() method you wrote to enumerate all of the types. It would be recursive because of the need to define a new variable in each pass.

To get one of the types (in a deterministic form) going to macros will be easier, definitely

def one_type_of(t : T.class) forall T
  {{T.union_types.first}}
end

pp! one_type_of(Union(String, Int32)) # => Int32
pp! one_type_of(Union(Bool, Nil))     # => Bool
pp! one_type_of(Char)                 # => Char

I’m still not sure the experiment you want, it seems you aim for these operations to play with types

def remove(s : S.class, t : T.class) forall S, T
  x = uninitialized S
  raise "" if x.is_a?(T)
  typeof(x)
end

def one_type_of(t : T.class) forall T
  {{T.union_types.first}}
end

def atomic(t : T.class) forall T
  {{T.union_types.size}} == 1
end

pp! one_type_of(String | Int32)                         # => Int32
pp! remove(String | Int32, one_type_of(String | Int32)) # => String

t = String | Int32 | Bool
pp! t, atomic(t), one_type_of(t)
# => (Bool | Int32 | String)
# => false
# => Bool

t = remove(t, one_type_of(t))
pp! t, atomic(t), one_type_of(t)
# => (Int32 | String)
# => false
# => Int32

t = remove(t, one_type_of(t))
pp! t, atomic(t), one_type_of(t)
# => String
# => true
# => String

Well, it seems just using macros is easier. I didn’t understand to use them in a generic, as you have. I really appreciated learning that bit of language voodoo, as well. I’ll keep it in my tool bag.

Thanks

Bruce

Could you maybe show some example calls and expected outputs, so we can understand what exactly you are trying to achieve?

May I scream for just a tiny moment?

Bruce Perens? The Bruce Perens?

:scream:

1 Like