As I’ve continued to learn more about Crystal one thing I noticed is that there are several things that you can do which result in a runtime error. For example pulling an invalid index of an array using the []
method rather than the []?
method. Is reducing more runtime errors and replacing with a compiler error a long term goal of the project? Would pull requests be accepted for these types of issues?
What do you suggest to do here? In pretty much every language (even in Haskell!) indexing out of bounds results in a runtime error.
If the array gets manipulated at runtime how would you know how many items are in it at compile time, when you use []
at runtime?
Return nil
. In situtations where the code is written ahead of time (not where user input is involved) the compiler could check if a run-time error would occur and throw a compile-time error instead.
For example
my_array = [0,1,2,3]
puts my_array[99] # RuntimeError occurs here
Since we already know the array, and we see the key will not exist, and that there is no code that could possibly change the contents of the array in the program, the compiler could throw an error. This cannot account for larger programs where user inputs and changes to the array occur at runtime.
The approach from earlier and maybe the easier one is to always return nil if the index doesn’t exist. We’re checking if code is nil-safe anyways, if nil is returned we can then piggyback on the existing compile time checks for nil safeness. Why let a developers make the mistake of forgetting a ?
in their method call and having their program crash at runtime
The short answer, you can’t. The long answer, the compiler can analyze the code and take action on a case by case basis.
The larger question here, given a specific example and solution, is the Crystal team willing to take things that would normally result in a runtime error result in a compile time error instead.
Returning nil would be even worse than getting an exception. That could lead to other issues that an exception would catch. This probably would add a bunch of complexity for not much gain.
I’m interesting in learning more could you describe one of those issues?
You’re basically suggesting that []
should have a permanent Nil
in its possible return types. This would require that everything that uses this do nil checking. Plus there is already a method to return nil if its out of bounds. If that is what the user wants they can just use []?
.
That’s correct.
I’m suggesting in a future major release that should just be the default behavior rather than the user requesting it.
Isn’t a goal of the compiler to make sure all code is nil safe? Doesn’t nil checking happen already?
I guess this would be a question of “is this even possible” and “how much work would it take”. I mean in theory I guess it could work?
The other question would be with the inconsistencies it would bring, compile error sometimes, runtime exception others.
At one point in the past we concluded that returning nil by default was going to be extremely annoying to work with. In most cases if you want a value in an index and you expect it to be there, if nil is returned you would probably turn that into an exception anyway. For the cases you do want to handle it differently there’s the question variant.
@nsuchy I covered some of the design decisions in https://crystal-lang.org/2016/09/09/a-story-of-compromises-and-types.html
Raising on array access also has the benefit that it directly provides an error message saying which index was tried to access and doesn’t exist. That can be useful for the developer.
Fair point. Still though I’d adverse to any runtime error ever occuring, it’d be cool if the only errors most developers encountered were compiler errors rather than runtime errors.
I’ve wondered the same thing myself. It’s like Crystal is “all compile time” but then has this wart of a few Runtime Exceptions still. Maybe arrays could be typed with a “guaranteed size” when they’re passed in and out as parameters, somehow, to decrease the possibility of runtime exceptions being generated? It gets a bit tricky to imagine though…
Arrays are data structures with dynamic size. That’s their purpose. There is no way to validate the size and therefore the success of an index operation at compile time.
There is always a possibility of an index out of range error, which is actually a prime example of a runtime error that is unavoidable.
Obviously, instead of raising an exception, the array accessor could just return a value indicating that the index is not available (for example nil
). This is a viable option in many cases, that’s why we have Array#[]?
.
But this approach causes other issues. When the application code expects to get a value from the array, the return type always include a no-value value like nil
and the user needs to handle this case explicitly. Every time. And that means either raising a custom exception or ugly return values indicating the inability to perform the requested action, which spreads into other parts of the code. Crystal tries to avoid the latter and provide an easy way to use the former:
elem = array[index]? # returns element type or nil and nil needs to be handled
raise "element not found" unless elem
# vs
elem = array[index] # returns element type and raises in case of index error
A Tuple can be used if the collection is immutable.
The size being known at compile time, accessing an out-of-bound index will raise a compile time error.
I think we should have both options like it is now. Use []
when you want to throw the exception, []?
when you’d rather test for nil instead. If you don’t want runtime errors, don’t go out of index, or use []?
and deal with the nil.
There really isn’t another way this needs to be done, a programming language should not change itself for bad programming, sorry
I was thinking something along the same lines the other day. It seems…odd that crystal carefully checks for “nil” access and throws a compile time error. Except when it’s array access, then it’s runtime. I guess the question is would it be too burdensome to make returning nil “if absent” the default? I’d be OK with it, then your code would be safer anyway…
somewhat related: https://github.com/crystal-lang/crystal/issues/4776#issuecomment-429460078
@rogerdpack the problem with that is that a | Nil
will pollute all the types that are coming out of the array. You can use Array#fetch
to have that experience if you prefer. But we think Array(T)#[](index) : T
is the best compromise for array.
It “might” (gulp) just require a lot of .not_nil! 's everywhere…maybe… :)