Not sure if this has been reported yet, but type restrictions in method arguments don't apply to the actual methods.
Reproducible example:
def ex_1(a : Bool = false)
a = "not a bool"
a
end
puts ex_1 # => "not a bool"
Expected behavior: Compile time error.
What actually happens: Program compiles and runs with a string as output: "not a bool".
I tried explicitly setting a to Bool inside the actual method but got the error that a is already defined.
a is of type Bool and has a default value of false. It should not lose its type restriction inside the actual method.
Tested on 0.24.1 and 0.24.1+123 [80cbe6603] (2018-01-15)
a : Bool is just a type restriction for the method argument. The type can change inside the method. This is expected behavior for me.
Thanks for the reply. It wasn't intuitive for me that a : Bool would only apply to the given argument, but it seems like that functionality may be useful in certain situations.
How would I make the restriction apply inside of the method as well? Ideally I'd like to use the same variable without making a new one.
There's no way to do that. I can't find a use for restricting the type of a variable (I never used it).
I think it's because I'm familiar with the way Java and friends handle type restrictions.
When I specify a type for a variable I expect it to only use that type, even in the method body. I already specified that a is a Bool, or a : Bool, so I expect a to be a Bool no matter what. I wouldn't want any bugs to show up because I forgot that a : Type only affects the given argument and not the actual a inside the method itself.
@GloverDonovan you can use different variable for that:
def foo(a : Bool)
b : Bool = false
b = "not a bool"
b
end
foo(true) # => raises compile-time error as expected
Maybe we could consider that as a change in the language, given that exp : Type already forces that to be of that type.
That said, type restrictions work in a completely different way than typing a variable. For example:
def foo(x : Array)
end
Here x will be the type that you pass to the method, not any kind of Array. The method is restricted to arrays, though. If you write:
x : Array
that actually doesn't compile because of a variable can't be of type Array (it can only be of a particular instance of an array).
I can't remember the last time (if any) I had a bug because a variable became of another type. The compiler usually catches this. And then, tests help a lot.
I don't think that forcing people to choose a new name when they want to convert their argument to a new type is a good idea. If I have (bad example) def do_something(path : String), I might want to do path = Path.new(path) in the body. That should be allowed.
I'm personally not sure why we have foo : Bar type restrictions in method bodies. I can't remember when or where it was discussed or the reason for it. The addition was fairly recent though...
People wanted it so I implemented it. Maybe it's useful to make sure for example a variable is of type UInt8 and doesn't accidentally spans into other integer types. Though that most commonly is needed for instance variables. I don't know... we can remove that feature, I guess.
Very slightly edited for readability only from IRC:
<RX14> @asterite the thing about local variable type restrictions in crystal vs other languages is that it doesn't *add* any new information
<RX14> it only creates more errors and strictness
<RX14> other then scoping (where you can use `foo = nil` to set scoping anyway), removing the type restrictions on any compiling piece of code won't change the type
<RX14> So there's little usecase in the first place
<RX14> restricting the size of numbers is a fairly good point, where you can accidentally generate a (UInt8 | Int32) and not realise it, but i'm not sure of where that would be a problem?
<RX14> if you're assigning that union to an ivar, you get an error
<RX14> if you're passing it to a lib function, you get an automatic cast
<~asterite> RX14 it means invisible slower code
<RX14> @asterite yeah there is that...
<RX14> but there is the method argument type restriction inconsistency
<~asterite> I guess the language will always remain inconsistent if we want it to work like that
<RX14> for the rare performance-critical places where this is needed, it'd be ideal to have a method which is clearly an assertion, not a restriction
<RX14> because local variables can't be restricted, they're already the smallest type the compiler can infer
<RX14> so a local variable type restriction is just a local variable type assertion
<RX14> ideally you could just do {% raise unless typeof(foo) == Int32 %} but that's a whole new can of worms
<~asterite> Another thing is that you could probably spot the performance problem with a profiler, so it's not a real issue
<~asterite> and if it happens and you can't stop it... then it's not an issue :-P
<RX14> I do see people try and misuse local variable type restrictions occasionally but it's rare
<RX14> so perhaps not worth thinking about too hard
<RX14> @asterite well it'd be pretty hard to spot with a profiler
<RX14> at least not without completely understanding the assembly
<RX14> which is beyond me
<RX14> I think it's better to just leave it as-is if anything
<RX14> introducing a special {% assert_type(foo) %} could happen but... ugly
Sorry I did not know about the difference between type restrictions and assertions earlier. To verify:
def a(b : String)
end
is a type restriction and
def a
b : String
end
is a type assertion?
I'd love a way to to both at the same time. Maybe something like this?
def a(b : String!)
b = 5 # Compile time error
end
@GloverDonovan Why don't you just do it like this:
def a(b_arg : String)
b : String = b_arg
b = 5 # Compile time error
end
That's just one more expression but doesn't need an additional language feature for somthing that most Crystal developers won't really use (I'm basing that assesment on the statements in this thread and my personal experience).
@GloverDonovan they're both called "type restrictions". My point was that in the case of type restrictions on local variables, they happen to be unable to restrict the types since local variables are always restricted to the most restricted type the compiler can infer. So I suggested calling them type assertions, because that's all they can really do. They're still called type restrictions officially though.
As @straight-shoota said, type assertions aren't used very often, so having a little bit of extra syntax here is probably fine.
Sorry for the late reply. Took me a while to think over this and I've been using more Crystal during that time.
Re-read the Crystal philosophy and some of the things I was trying to do aren't really Crystal. I'll use @straight-shoota's suggestion as needed but he's right (most of the time you won't need to use it). It's different than the other type-safe languages I've used, but I can adapt.
Ironically, I don't think the language should be changed now, but I do believe that the Type Restrictions page should be updated. It wasn't intuitive to me that Crystal would handle type restrictions like that. (Maybe add some of the things mentioned here?)
Closing this in favor of crystal-lang/crystal-book/pull/198.
Most helpful comment
a : Boolis just a type restriction for the method argument. The type can change inside the method. This is expected behavior for me.