Graf Zahl wrote:I have nothing against making things easier. But all the proposals I have seen here just do not convince me. They make it primarily easier to write bad code and that's simply not a good thing. Like dpJudas already said, my biggest issue with Javascript in particular is that the language basically has no self-documenting features, which makes getting into the code extremely difficult. In C++, C# or Java I always have a class declaration which already gives me a lot of info about what the class can do and more importantly, how it manages its data.
At some level, the alternative to writing bad code isn't writing good code — it's writing no code at all. You can help an inexperienced programmer improve, but you can't help someone who gave up before managing to write any working code. I'd rather have a little more bad code than fewer people making things overall.
Speaking as someone who's been having to get a handle of ZDoom guts for the first time recently, I disagree that class declarations are a documentation panacea.

I mean, this is a codebase with Actor::special1, Actor::special2, Actor::specialf1, Actor::specialf2, and Actor::weaponspecial. Not that far off from the hypothetical worst-case struct I wrote. I know none of us think ZDoom is the most beautiful codebase to ever grace the earth, but that's exactly my point: static typing doesn't magically make code good.
Graf Zahl wrote:Good luck writing unit tests for ZDoom. Sorry, but that goes way beyond my tolerance level. If you look at my own code you'll probably already see an entirely different style that is actually designed to make testing easier. What do you think I use to check the ZScript compiler? The tests I do aren't fully automated, of course, because I constantly need to check the bytecode output to see if everything is ok, but this is broken down in so many smaller parts that it can be properly tested.
But the vast majority of the old code is so complex, complicated and convoluted that all we can do is pray that it never breaks.
Forced to choose between the perfect static type system and automated tests, I'd take the tests in a heartbeat. (Hm — I don't know how tests for mods written in ZScript might work, but that's an interesting idea to think about.)
Chocolate Doom has a
testing setup that replays demos from Compet-N and verifies that some overall stats at the end came out the same, which is a great way to automate sanity checks. ZDoom demos are more brittle, of course, but you could have test wads that check stuff with ACS and a harness that runs them automatically and reads results from stdout. Tests don't necessarily need to be internal unit tests. I know Firefox has a huge pile of "reftests" that effectively just take a screenshot of a page and compare it to a known good rendering; ZDoom could do the same. Way better than nothing.
Graf Zahl wrote:Well, as a matter of fact you cannot do static checks on data that may or may not be valid at runtime. The best I could imagine is an error telling me that an unvalidated pointer is being used. Of course that would create so many false positives in real-life code that I question its usability.
It's already done in several real-life languages. I gave examples in Swift and Rust before, but I misread a detail about Swift and I forgot the easiest approach for Rust, so let me try that again.
Rust has no null value. Instead it has a generic wrapper,
Option<T>, which might be Some<T> or None. So you can tell right away from type names whether a given value is allowed to be "null": Option<T> is, but T is not. And when T is a pointer type, Option<T> compiles down to a regular nullable pointer, zero overhead.
Code: Select all
// Rust
// let's say foo is inferred to be Option<T> here
let foo = some_func();
// I can't use T methods on foo, because it's not actually a T.
// I can unwrap it with pattern matching, because Option is an enum of Some<T> and None:
match foo {
Some(x) => ..., // x is scoped to this expression
None => ...,
}
// Or for the common case of just caring about the contents...
// ("if let" is a special kind of pattern-matching block, not just expression assignment)
if let Some(x) = foo {
// This compiles to a null pointer check, and of course only runs
// if foo contains a value. x is scoped to this block
}
// Option also has some helper methods.
// For example, this lambda only runs if foo contains a value.
// bar is inferred to be Option<U>, where U is the return type of do_something.
let bar = foo.map(|x| x.do_something());
Rust is 100% statically typed and compiles much the same way as C++, but null pointer issues are greatly constrained. You know exactly which values may or may not exist, and the compiler doesn't let you use an optional value without
somehow acknowledging its optionalness first. If you really really want to defer the null check to runtime and accept a panic when it's null, you can do that too — and have a pretty good idea of where the problem is. But completely forgetting to check is impossible. Mozilla is currently writing a browser engine in Rust (and recently started shipping Rust code in Firefox), so it's perfectly usable.
I'm not familiar with the downcasting story, but it'd have to return an Option<T> regardless, so you'd deal with the result the same way.
The especially neat thing is that this is just a regular Rust type, not a magical builtin. ZScript probably doesn't need the type system plumbing that Rust uses to make this work, but it could special-case this: distinguish between nullable and not-nullable pointers, and have some statically-enforced way to only use nullable pointers after a check.