Why doesn’t work?
module Test
class Point
def initialize(@arr : Array(Int32 | Float64))
end
end
def self.union_type_brainf8ck
a: Int32 | Float64
b: Int32 | Float64
a = 2.0
b = 2.0
point = Point.new [a, b]
p! point
end
end
Error: instance variable '@arr_init' of V::Point must be Array(Float64 | Int32), not Array(Float64)
Apparently type declarations don’t pin the entire union type, only the member that was assigned first.
a : Int32 | Float64
b : Int32 | Float64
a = 0.0
b = 0
typeof(a) # => Float64
typeof(b) # => Int32
I would say this is unexpected. But maybe there’s a reason for that…?
Anyways, I’m not sure if type declarations with unions are very useful.
For this use case, instead of tying down the variables, I would recommend to create the array with the appropriate type: [a, b] of Int32 | Float64
.
mavu
November 5, 2022, 2:58pm
3
Is there maybe a syntactic ambiguity hiding in there?
Because the typeof() could be interpreted as:
give me the type of whatever is stored in this variable
give me the type of the variable
I think those are 2 different things to ask, right?
Is there a way in crystal to ask the second question?
When I implemented this I did it as a type restriction. That is, the variable can only hold those types, but it’s compile. Time can be narrower than that.
That said, I think someone should change that to be a type declaration instead.
1 Like
typeof()
returns the compile time type of an expression (i.e. what can be stored in a variable). Object#class
returns the runtime type of a value.
So if you have a variable that can be either int or float, you might get this:
x = [0, 0.0].first
typeof(x) # => Float64 | Int32
x.class # => Int32
1 Like
Yet another example of TypeDeclaration
making things unnecessarily complex when a = 2.0.as(Int32 | Float64)
would do.
Also I don’t think a: Int32 | Float64
should compile without a space before the colon, since it doesn’t as a def argument.
1 Like
I think there’s value in saying “I want this local variable to be of this type, regardless of what I assign to it later on”.
4 Likes
Okay, I wanted to fix this by making it work like this:
a : Int32 | String = 0
typeof(a) # => Int32 | String
But then I thought… what happens if we later assign a new value to a
?
a : Int32 | String = 0
typeof(a) # => Int32 | String
a = "hello"
typeof(a) # => ?
a.size # Will this work??
The main issue is that if we fix the type of a
to always be Int32 | String
then the type flow won’t work.
I think that’s the reason I chose to make a : T
mean “a can be anything that fits into T, but the actual compile-time type can be a subset of T”.
So now I’m thinking that this is correct semantic.
2 Likes
There is a variant of this issue where uninitialized
variable types also aren’t fixed but pointers to them are:
x = uninitialized Char | Float64
x = 1.0
typeof(x) # => Float64
pointerof(x).value = '😂'
x # => 1.0000000000285358
x.unsafe_as(UInt64).to_s(16) # => "3ff000000001f602"
pointerof(x).value # => '😂'
zw963
November 14, 2022, 4:00am
12
Yes, i consider current solution is perfect, it did it job (limit possible type) very well.
The newbie probably need some time to understood it.