https://blog.codacy.com/an-in-depth-explanation-of-code-complexity/
I think a lot of people don't know these simple things:
- key to reducing source-code complexity = reducing number of possible cases
- reduce number of cases to think about
- reduce number of 'what-can-go-wrong?' scenarios
- number-of-cases are usually multiplicative between function-calls
A lot of articles on code-complexity focuses on reducing-number-of-if-statements (and nested-ifs)
But the core idea is this: reduce number of possible cases.
simply put, number-of-possible-cases ++
== number-of-possible-bugs ++
== number-of-tests-you-have-to-write (but won't) ++
But... there should be an answer to:
- how is having well-defined
Type
s mean reducingnumber-of-possible-cases
? - how to well-define types?
I'll explain about this below...
(examples are in Typescript, but can be applied to Python, etc)
Here's an example without types:
function receiveUserObject(user, receiveOption) {
// do things
return user.user_ID // funky
}
here, by looking at only the function-definition, there's near-♾️ possibilities on what input/output is going to be.
So... what now? well go look at all the usage, and carefully collect how this function is called.
(if funcA(...)
--> funcB(...)
--> receiveUserObject(...),
you should also investigate funcA
to be 100% sure.)
Now, suppose we have a typed version of that function:
type UserType = {
user_ID: string;
userName: string;
email: string;
}
type ReceiveOptionType = {
fromWhichServer: string;
}
function receiveUserObject(user: UserType, receiveOption: ReceiveOptionType) {
// what's in `receiveOption`?
}
Suppose you have User
type defined like this:
export type UserType = {
userName?: string;
email?: string;
user_ID?: string;
lastAccessed?: Datetime; // actually optional
experiencePoints?: number;
}
what's in a "user"? here's how to calculate the number-of-cases:
userName = string / null = 2 cases
email = string / null = 2 cases
...
Hence, the answer is: 2 x 2 x 2 x ...
Of course there's unit-testing and coverage, but writing tests to cover exponential-number-of-cases isn't fun.
Moreover, coverage doesn't help here:
- coverage is about "did the test-code run all the source-code?"
- having 100% coverage does not mean "tests covered ALL the possible cases"
- though 100% coverage should mean champaigns and celebration
Anyway, ** nullable-by-default is what most 'modern' languages tried to get away from: **
https://www.infoq.com/presentations/Null-References-The-Billion-Dollar-Mistake-Tony-Hoare/
Hence, Kotlin, swift, scala, etc --- all have:
- variables are non-null by default
- nullable variables should be marked as such, so compiler can check for possible NullExceptions
" what if userName is empty-string" ?
For this, we have dependent-typing
Simply put, with dependent typing, type-system can check whether a variable has passed some validation-function or not.
type IsNotEmptyStringType = string & {
// typescript voodoo magic here (TS doesn't officially support dependent typing, but...)
__IsNotEmptyStringType: null;
}
function validateNotEmptyString<T extends string>(s: T): T & IsNotEmptyStringType {
if (!s || s === '') {
throw new EmptyStringError();
}
return s as ( T & IsNotEmptyStringType)
}
// !!!usage example!!!
function doSomething(s: IsNotEmptyStringType) {
// s is guaranteed to be not-empty
}
So, we can have:
type UserType = {
...
email: ValidatedEmailString; // ensure validateEmail() called for this string.
}
Now, we still need to write tests. But the range of testing can be reduced:
- from "all the places where email can be created"
- to "validateEmail( ... )" (one function)
- Wasm (webassembly) is about:
write in 1 language, compile to wasm, embed/run it everywhere
- these kind of validation-function can be written in 1 language, and used by the client-side and server-side
-
so even when there are android(kotlin), ios (swift), web(ts), server(python), the validation-logic can be shared (hence fewer tests)
(or we can go full-kotlin and write everything in Kotlin... yay kotlin!!! )
-
( apple's policy on embedding wasm on iphone Apps isn't decided yet (maybe?) -- so um... idk )
Currently, "everything's optional (=nullable)" in protobuf3.
Thankfully protobuf team seem to have got on their senses and backed down -- https://stackoverflow.com/a/62566052 It's better than nothing, but has few problems:
- "required-by-default" is the preferred way to prevent mistakes
- it's adding "optional" field markings, but protobuf3 is already "optional-by-default"
- maybe they're thinking about changing to "required-by-default"?
- if so, there'll be a big breaking changes for everyone...
- the changes will have to be propagated to the implementations, which is... going to take long :(
No matter how much you try, you can’t stop people from sticking beans up their nose.
wise words: https://archive.uie.com/brainsparks/2011/07/08/beans-and-noses/
I still see a lot of proponents for "dynamic language is better than static ones!" and "tests can cover types too!"
As for dynamic lang > static lang
, things changed a lot:
- type-inference (don't have to write ALL the types - writing type-defs on functions will mostly do)
- better type rules (contravariant vs covariant types to represent things more precisely, dependent typing, etc)
And as for "tests can cover types too!":
- of course you can. But would you like to?
- are you sure your test will be 100% perfect?
But... of course! beans and noses... must be united!
the diameter of nostril = diameter of a bean.
Most electric-hardware can be thought as: "if it fits here, then it should fit here".
same for nose and beans.