Why Crystal allows reassigning a variable with a different type

As for the strict typed language, I find it strange that Crystal allows to reassign the variable with a different type. I would expect that after the type of the variable was infered from it’s usage, assigning a different type value to that variable in the same scope would produce an error.

This works fine and is concidered a feature of the language:

x = 1
x = "str"

If you explicitly define a type of the variable, you cannot reassign it a different type value (like it’s expected in a strict typed language):

x : Int32
x = 1
x = "str"  # Error: type must be Int32, not (Int32 | String)

But that does not work with the method parameters:

def method(x : Int32)
  # I would expect this produce an error, but it's not
  x = "str"
end

method(0)  # "str" : String

Allowing to redefine the variable if the type was infered from the default value could be expected to be consistent with this feature of the language:

def method(x = 0)
  x = "str"
end

Summarising:

  • Why strict typed language allows to reassign the variable with the different type if the type was infered from the first usage?
  • Why it allows to reassign the method parameter with a different type value even it it was explicitly typed in the definition of the method?
1 Like

One of Crystal’s features is that method arguments and local variables do not need to be explicitly typed.

The issue you’re seeing is more so because you are totally overriding the variable. x used to be an Int32 on line 1, but is now a String on line 2 because there wasn’t an explicit type guard saying what that local var should be.

The method example is similar, you have a type guard on a method that only allows that type, but would not prevent you totally overriding it in the method body. Adding a return type to the method would be one way to ensure the method returns what you are expecting. That example would also not work with instance vars as those always have to have a type.

Redefining is sometimes useful and also makes code more compatible with ruby:

def login(user : String, id : String)
  user = Users.find(user)
  id = id.to_i? || new_id
  ...

or

node = first
while node # node can be nil
  node.process
  node = node.next #oops, item has type Node, not (Node | Nil)
end

etc.

Also this won’t prevent errors “user declared another variable with same name” - because user can still declare another variable but with same type.

I think compatibility with Ruby should not be an argument: Crystal is Crystal, not Ruby - it only “borrows” a very nice syntax of Ruby, but it should adapt it the way it fits the strict typed language.

My concern is this: I expect that stictly typed language saves me from unexpected change of the type of my variables. And Crystal (because of this allowed variable redefinition) does not ensure this.

Example:

This works fine

def push(a : Array(Int), v : Int32)
  a  <<  v
end

a = [0]
a = push(a, 1)  #=> [0, 1]

But if I make a mistake (use = instead of << in my method), my method wrongly returns an Int32 instead of Array(Int32), but compiler is still happy about that:

def push(a : Array(Int), v : Int32)
  a = v  # intended to write a << v
end

a = [0]
# No error
a = push(a, 1)  #=>  1

And somewhere further in the code I will be surprised that a is not an array any more.

Of course, I could restrict the type of the variable by explicitly typing it:

def push(a : Array(Int), v : Int32)
  a = v  # intended to write a << v
end

a : Array(Int32)  # this saves me
a = [0]
a = push(a, 1)  #=>  Error: type must be Array(Int32), not (Array(Int32) | Int32)

But that urges you to always use explicit typing and the very idea of Crystal beeing smart to infer the types from the first usage of the variables then becomes obsolete.

I love Crystal being smart and requiring very little explicit type declarations, and that’s why I think it should not allow so easily to redefine the variables if their type was infered from the fist usage.

@konovod, I see your point in redefinition beeing useful sometimes, but I would rather not allow it in the strict typed language and would write your code like I put it below (but would feel more assured that compiler helps me if I accidentaly did something not intended):

def login(user_name : String, user_id : String)
  user = Users.find(user_name)
  id = user_id.to_i? || new_id
  ...
2 Likes

You’re problem here could be solved by just adding a return type to your push method.

https://play.crystal-lang.org/#/r/6ice

Sure, but this again means that if you want to be safe, then you must always be explicit about all the types. And that is not what Crystal phylosophy of being smart to infer implicitly is about.

1 Like

This is a feature of the language. Method args and variables do not have to be explicitly typed; but they can be if you want it to be of a specific type.

Of course this is only an issue for local variables, instance vars have to be typed and are able to have their type inferred.

You’ll just have to explicitly type your local vars which you can do inline btw a : Array(Int32) = [0], or use a class/struct which would provide guaranteed type safety.

The safety of the language comes from the compiler preventing to invoke an undefined method on an object. Type restrictions are just there for a bit more clarity and for overloads. They are not there for type safety.

3 Likes

As you mentioned in your example, if you do a=v you’ll get a compile time error later on. The safety remains.

1 Like