4 ms·
The main argument here apparently is "lets not add safety features because we have a limited pool of features before the language becomes too complex". I don't
by spion 10y ago
The main argument here apparently is "lets not add safety features because we have a limited pool of features before the language becomes too complex". I don't think thats a convincing argument. Its a question of which features you value. For example, I'd totally give up `public, private and protected` to be able to get rid of exception and `null` pointer problems forever.
> It is programmers who create defects – not languages.
In the case of nulls in typed languages, its the language's fault. The language is literally lying to you.
Lets say that Java is claiming, for example, that a method always returns a `string`. What is a string? Its a value of a certain class, that supports certain methods that return other values. In structurally typed ("duck typed") languages, the type can be defined by the methods you can call on the value (the interface it supports)
But this claim is just not true. The method may return null, and if you call the string methods on that value, your code may die a violent death. Its clear that the value `null` doesn't belong to the type `string`, since it doesn't support the contract that a `string` provides. So why is the programmers fault to expect that the type system doesn't lie to them? I think the programmer has the right to expect an actual string - not a number, not an `StringLike`, and definitely not null, all of which don't fully satisfy the `string` contract.
Unfortunately, the language doesn't give you the ability to discern between a string that cannot be null and a string-or-null (could be string, but also could be null). Because of this, you need to track all the places that might be null in your head to make sure you check for them, or look up the test code to remind yourself all the time.
Its a similar problem with exceptions. The language is claiming that functions that don't cause exceptions act the same way as those that do. But thats painfully untrue, and you would find that out the moment you try to `fopen` a file, read something from it, and then `fclose` it.
f = fopen('file.txt')
data = fread(f)
fclose(f)
Oops but the `read` call may throw, and you now have a dangling file handle. Good thing you knew that about `fread`! Now how about the other thousand methods in your code? Do you know whether any of those throw?
Whats that? You should be using `try-with-resources`, I hear? Okay, then what do I do with this code:
atomicIncrement(inFlightRequests)
result = doRequest()
atomicDecrement(inFlightRequests)
I should use try-finally. Thats true, but what if doRequest() doesn't throw? Then I don't need to. Why do those two look the same? Why is the language lying that they are the same?
Now lets see whats available in Swift:
* `try!` means - I don't expect this piece of code to throw, even though it "might". If it does, crash horribly.
* `try` means - I know that this piece of code throws, but I can't handle that exception here, so propagate it.
* no keyword - This code should not throw. If it returns a Result that may be an error, I want to handle it right here.
* `try?` similar as above, but discards the specific error and returns an Optional.
Thats it. You manage the risk, the language only makes that explicit. It also makes it really clear whats going on during code review.
Here is how the problematic piece of code looks when `doRequest` might throw:
atomicIncrement(inFlightRequests)
result = try doRequest()
atomicDecrement(inFlightRequests)
The possible bug is now very easy to spot. We don't need to remember whether `doRequest` throws or to look that up. Its clear that a finally or defer block is necessary here.
Whats even better is how the code looks like when `doRequest` doesn't throw!
atomicIncrement(inFlightRequests)
result = doRequest()
atomicDecrement(inFlightRequests)
Since we know the compiler would force a `try` keyword there if there was the possibility of exception, we know this code is correct just by glancing it. No need for `defer` or `finally`.
So are these two features not worth the cost? Maybe, if you are arguing that we don't need types as a tool to prevent errors. In that case, it would indeed be the programmer's fault for not writing a test for it.
But if you are going to have types, you might as well have types that tell the truth. Otherwise they're useless.
Finally, IMO this is not a good reason to hate on types. Stop complaining that the compiler wont accept your buggy code and take the time to do the changes. I'm happy when the compiler points out all the places that will be affected by the change - thats something that unit tests with mocks will never catch!
Imagine that a change in doRequest caused it to throw, where previously it did not. The compiler will now tell you about ALL places that may be affected. What would happen if it didn't? The `inFlightRequests` counter will suddenly start behaving strange and increasing indefinitely. What would happen if above a certain value new requests don't get queued? A bug that leads to denial of service.
Is it worth the hassle of changing all affected code? I don't know, what do you think?
There are much better complaints against type systems, like the fact that most are not able to understand certain advanced styles of metaprogramming yet. Or that it doesn't do control flow analysis, e.g. if I type
if (x != null) { doCode(x); }
then `x` should have its nullability removed in the block
or
if (x == null) { x = nonNullValue; }
then below this line, x should not be considered nulable. TypeScript for example does both of the above.