DEV Community

Cover image for Design By Contract, Immutability, Side Effects and Gulag
Juan Carlos
Juan Carlos

Posted on

Design By Contract, Immutability, Side Effects and Gulag

World Of Bugs

There are 1.4 billion insects for every human, most of them on your code

The concepts on this post are useful for any language, examples are from Nim.

Defensive Programming usually takes the form of a Unittest,
Unittest checks a "unit" of code, validates using assertions on special test runs,
runs a code block and asserts that the result equals a predefined "sample" result.

This is good, but is has some limitations,
the input, data and context is very often faked (mocked),
the input is usually not asserted because is kinda fixed,
the output is result of the inputs so neither is completely real world,
it runs on "dedicated" automated tests runs.

  • How can tests be more integrated and close to the code where the bug lives?

Side Effects

Bugs is the entropy of the universe trying to reverse engineer your code while you are still writing it

Side Effect Free programming is focused on eliminating side effects on the code,
and monitoring or controlling them where you can not eliminate them.

Functional programming tries to reduce Side Effects,
without Side Effects the Bugs can not spread on the code, helps reducing Bugs.

Side Effects examples:

  • Writing to terminal or files, reading from terminal, files or user input.
  • Mutating in-place of variables, globals, objects, types, data structures.
  • Date/time functions, Win32 API/Registry, Databases, HTTP, GUI, exceptions.

Nim has Side Effects Tracking, Functional advantages while still using OOP.
Simply changing proc to func will check for side effects on your functions!.

Immutability

Immutability changes everything!

Immutability of variables helps to fight Bugs by not allowing unwanted mutations,
using Immutability whenever possible is very encouraged.

Nim is immutable by default and has 3 kinds of variables:

  • let its a runtime immutable variable.
  • const its compile-time immutable variable.
  • var its runtime mutable variable.

Contracts

TDD is Poor-Man's Contracts

Some programming languages use a different way of dealing with all this,
languages like Ada or Eiffel, use Design by Contract. Applied Hoare logic.

Contracts can assert on input, data and context on the beginning of a code block,
and can also assert on the result at the end of a code block,
it lives in the code block as close as possible to it, it runs an actual real run,
everything is real inputs, data, context and whatnot,
on programming languages that support compile-time code execution all this can happen at compile time if desired.

Contra

The night is dark and full of errors

Contra reimagines Design By Contract in a modern KISS way,
all the benefits of Assertive Programming on statically typed compiled language,
like everything on Nim it takes a lot of care about performance,
it allows fast contracts with zero cost at runtime with a comfy API,
requires minimal modifications on existing code, just 2 lines to add a Contract.

Lets take a buggy example code and add a Contract to it:

proc myFunction(mustBePositive: int): int =
  result = mustBePositive - 1
Enter fullscreen mode Exit fullscreen mode

Remember that mustBePositive must be Positive, lets use the function:

proc myFunction(mustBePositive: int): int =
  result = mustBePositive - 1

echo myFunction(0)
Enter fullscreen mode Exit fullscreen mode

This is a Bug, mustBePositive is not Positive (0 - 1 is -1), lets add Contra,
and ensure that there are no Side Effects using func:

import contra

func myFunction(mustBePositive: int): int =
  preconditions mustBePositive > 0 # Require 
  postconditions result > 0        # Ensure
  result = mustBePositive - 1

echo myFunction(0)
Enter fullscreen mode Exit fullscreen mode

The Contract shields our code from the Bug, wont allow the Bug to spread.

  • preconditions takes preconditions separated by commas, asserts on arguments or local variables.
  • postconditions takes postconditions separated by commas, must assert on result, can assert on local variables.

To keep things simple, no body nor invariants blocks are required,
you can pass invariants mixed on the postconditions but is optional,
it can be used on JavaScript, interpreted NimScript, runtime and compile-time.
Your CI service can compile and run a binary with all assertions enabled.

Contra adds 0 lines of code to Release build and 9 lines of code to Debug build.

Gulag Driven Development

Make your users work for you

You can now delegate the bug hunting on collaborators and trusted users,
encouraging them to try development binaries compiled with all assertions enabled.

But the same code can also produce a binary with all assertions disabled,
Contra produces no code when build for release, then you pwn on all Benchmarks.

Nim has automatic Dead Code Elimination.

  • Convert your users into being your Gulag CI!.

Install

nimble install contra
Enter fullscreen mode Exit fullscreen mode

Assuming you already have Nim installed.
Try Nim now, you are missing a ton of cool stuff.

Thank you for playing

👑

Top comments (3)

Collapse
 
eterps profile image
Erik Terpstra

Nice post! I'm relatively new to Nim, hadn't found out about the side effects tracking yet.
Also you state that 'the contract shields our code from the bug' but don't mention what happens at compile-time or runtime in the example (i.e. what is the error message you will get).

Collapse
 
juancarlospaco profile image
Juan Carlos

Vanilla assert error, as the DbC uses in general.
Maybe on the future it can self-document the contract using CommentStmt.

Collapse
 
jamieghassibi profile image
JamieGhassibi • Edited

This is great, but code becomes more cluttered and violates separation of concerns. Is there anyway to move pre- and post-conditions to the end of a file where all tests can be collected, then somehow inject the code into the functions, perhaps via macro? Think of it like decorators. For example:

import contra

func fn1(x: int): int = x - 1
func fn2(x: int): int = x - 1

# Bottom of file
contra fn1: # Like block-label syntax
    preconditions x > 1, x < 20
    postconditions result > 0

contra fn2:
    preconditions x > 1, x < 20
    postconditions result > 0

And this would run exactly as if you had written the following:

import contra

func fn1(x: int): int =
    preconditions x > 1, x < 20
    postconditions result > 0
    x - 1

func fn2(x: int): int =
    preconditions x > 1, x < 20
    postconditions result > 0
    x - 1

I am new to Nim, so I don't know if the above syntax or similar is possible via a module import.

Cheers~