Skip to content

Instantly share code, notes, and snippets.

@devdoomari3
Last active August 27, 2020 09:46
Show Gist options
  • Save devdoomari3/7aff9aeff0195bde73c6068bebebf8c9 to your computer and use it in GitHub Desktop.
Save devdoomari3/7aff9aeff0195bde73c6068bebebf8c9 to your computer and use it in GitHub Desktop.
On Code Complexity, Types, and Protobuf

https://blog.codacy.com/an-in-depth-explanation-of-code-complexity/

I think a lot of people don't know these simple things:

  1. 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
  1. 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 Types mean reducing number-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)

1. Using Types to limit number of input/output cases

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`?
}

2. why "nullable-by-default" is a disaster

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:

3. "But shouldn't you write tests for validating those anyway?"

" 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)

4. future of validation

  • 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 )

5. recent changes in protobuf3

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:

  1. "required-by-default" is the preferred way to prevent mistakes
  2. 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...
  1. the changes will have to be propagated to the implementations, which is... going to take long :(

final thoughts: you can't stop someone determined to put beans up the nose

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment