Redundancy in Programming Languages
digitalmars.com
digitalmars.com
This is completely opposite to the philosophy of the APL-family languages, and those who have learned them seem to also argue strongly that the conciseness reduces errors and makes for higher productivity.
Probably the best example of redundancy in a programming language is unit tests.
Some of my worst experiences have been fighting unit tests that were "overspecified", i.e. testing for inputs which would otherwise be clearly impossible in the context of the whole system, with the result that any trivial change to the code can cause test failures that consume time to diagnose, despite zero change to the ultimate functionality of the system.
"Overspeced" unit tests? They should only be testing the API contracts and all edge cases. And refactoring your code should not result in changes to the unit tests as the purpose of the unit test is to tell you if your refactoring broke something. If you have to change your unit tests when you refactor, you're doing something wrong.
Fighting unit tests is a code smell for testing implementation details or poorly designed API that needs to keep changing.
I've been bitten by "clearly impossible in the context of the whole system" way too many times. That said, I'm not going to do horribly complex implementation detailed validation. But sanity checks are worth their weight in gold.
I disagree a bit here. Without getting pendantic about units and refactors, in practice, it is common to want to modify methods that have unit tests for them. Maybe you want to break out the method into two, or break the class into two classes, or you want to improve the method's interface, change the types it takes, its parameters, the structure it returns, etc.
All these changes will most likely break your unit tests. If they didn't break the unit tests for the method, I'd actually be curious what it was even testing.
Above this, people often in practice also unit tests private methods, so you've often got to deal with those type of unit tests as well even if you're only touching the inside of a class and leaving the public methods intact.
And beyond refactoring, sometimes you are simply adding a new feature or fixing a bug, and this will result in you maybe choosing to modify the behavior of some existing class and its methods. These changes will also break your unit tests.
What all the use cases I just listed have in common is that all of them have good reasons to break the unit tests, and will result in people simply going and changing the tests to reflect the new changes.
Off the top of my head...
1. When reusing code, unit tests that specify unexpected inputs are useful because you don't know the future use case. That's a rather weak reason to write the unit tests now, but a reason.
2. There is no way to enforce what is an expected input to a function, once a system grows large enough. Changes in the upstream dataflow being traced through the system by investigating every unit test is impractical. Being defensive up front, is reasonable.
...and thus you shouldn't care about it. I've seen enough problems caused by attempts at "futureproofing" to know that YAGNI is always a good choice. When/if it does matter, then you can do the work; but inevitably, requirements always seem to change in such a way that your attempts to anticipate them results in code that's structured to be flexible in exactly the wrong way, adding unnecessary complexity, or even hinders the desired changes.
This is exactly what typechecking does for you and is the reason I think trying to build any large system without them is a bad idea.
Having types, isn't a binary choice. eg Generics, plain objects, functions, pointers. There are inevitably gaps in how the type system can ensure your what your outputs will be (even if it's a halt).
def getString: String = {
"I am a value to be resolved"
}
def wrapper1(auth)(thunk: => String): String = {
if(auth)
thunk
}
def wrapper2(auth)(valueAlreadyResolved: String): String = {
if(auth)
valueAlreadyResolved. // Auth check did nothing, code already ran!!!
}
wrapper1(auth)(getString)
wrapper2(auth)(getString)
These two method signatures/behavior are different but the syntax to call them is identical! One executes "getString" only when the parameter is referenced (v1), the other (v2) executes it before the wrapper is even entered.This sort of flexibility can be handy, but it can also shoot you in the foot. Some languages like I think Java make you do something like "::getString" to pass a function pointer. That redundancy makes it explicit and prevents the mistake above which could have serious implications.
Well the great thing with that issue is that you can use tools that will forcefully put semi-colons in code and no rely on automatic insertion at runtime.
There is no excuse today not to do that. Of course, sticking to a specific coding style and enforcing it automatically with QA tools during CI is also something easy to do and should be mandatory for any codebase.
It’s editing lines that have semis. Any “to end of line” operation (and there are tons) you have to think about whether you want the semi or not, which often depends on what the future context will be. This is one of those things where once you get used to “power-editing” in semi-free, it’s very difficult to go back. And note that the bulk of my work is semi-d, I only go semi-free on greenfield projects. So it’s not a case of having more experience one way or the other.
Any lisper/schemester will tell you that reducing syntax makes power editing simple, but for some reason JS people love their semi’s. Beats me.
Replace "people" with business and you are right. I feel many devs would prefer to invest the time once rather than hack it once then go back and add unit tests
AFAICT, that is much more often said about _programs_ (and perhaps libraries) than about programming _languages_.
For a trivial example, `unsigned long long` is annoyingly long to type, `ulong` is much better. `uint64` seems at first impression to be better because the bit length is in the name, but digits are harder to touch-type, not so pleasing to look at, and after using the language for a day the reminder that it is 64 bits gets tedious.
Interesting blogpost though I do not agree on everything you say but I respect your perspective as a language designer and developer.
Redundancy is evil... but also essential. What characteristics distinguish one end of redundancy from the other?
Hard to say. Some call it simply "good taste".