Coding conventions
This page contains the current coding conventions for NPL. What follows are not strict rules, but general guidelines.
Source file organisation
Directory structure
NPL files should reside in the sources root, under the directory npl, e.g. $PROJECT_ROOT/src/main/npl
.
Source files should be separated into directories in line with their project purpose. General files should be under a
directory called shared
.
Test files should mirror as much as possible the directory and file structure of source files, e.g. have
$PROJECT_ROOT/src/main/npl/…/travel/trip-advisor.npl
tested in $PROJECT_ROOT/src/test/npl/…/travel/trip-advisor.npl
.
A test file may be broken into several smaller ones while following a naming convention of the type
trip-advisor_[number].npl
or trip-advisor_[topic].npl
or a combination of those suffixes.
Source file names
Protocols should have a file each (where dependencies allow), named after the protocol in
kebab case (i.e. all lowercase, with hyphens to separate the
words), e.g. a protocol called TripAdvisor
would be in a file called trip-advisor.npl
. Any functions that require
and act on a protocol's internal state should be declared within the protocol.
Functions that take a protocol instance as a parameter, but are not part of its internal state as such should be
suffixed with -companion
, e.g. trip-advisor-companion.npl
.
Functions that act just on structs, basic data types or no input should be in a file suffixed with -functions
, e.g.
times-series-functions.npl
.
Naming rules
Constants
Names of constants should use macro case (i.e. all uppercase, separating words by underscores).
const SOME_UNCHANGING_VALUE = 4;
User-defined types
User-defined type names should use Pascal case (i.e. start with an upper case letter and use camel humps).
protocol[p, q] Iou() {}
struct EmployeeDetails {
name: Text,
email: Text,
dateOfBirth: LocalDate
};
enum TrafficLight { GREEN, ORANGE, RED };
Functions, permissions, obligations, properties and variable names
Names of functions, permissions, obligations, properties and local variables should be descriptive and use camel case notation (i.e. start with a lower case letter and use camel humps). They should not use underscores, hungarian notation or type names.
Function names should almost never start with do
or run
.
timeSeriesList: List<TimeBlock>, // bad, contains type in var name
mTimeseries: List<TimeBlock>, // bad, hungarian notation
_timeSeries: List<TimeBlock>, // bad, underscores
x: List<TimeBlock>, // bad, the reader may not know the context of `x`
TimeSeries: List<TimeBlock>, // bad, not camel case
timeSeries: List<TimeBlock> // good
Test function names
Test methods should be named to describe the functionality under test and what is expected to happen.
// bad
@test
function testProtocolWorks(test: Test) -> {};
// good
@test
function testWhenSomeProtocolPermissionIsCalledAResultWithSomePropertyIsReturned(test: Test) -> {};
// good, you may also use underscores to improve readability
@test
function test_whenSomeProtocolPermissionIsCalled_aResultWithSomePropertyIsReturned(test: Test) -> {};
Choosing good names
The name of a contract is usually a noun or a noun phrase explaining what the contract represents, e.g. GasStorage
,
CustomerBill
.
The name of a function, permission or obligation is usually a verb or a verb phrase describing what it does, e.g.
updateGasLevel
, calculateBill
. The name should also suggest if the method is mutating the object or returning a new
one. For instance sort
is sorting a collection in place, while sorted
is returning a sorted copy of the collection.
The names should make it clear what the purpose of the entity is, so it's best to avoid using meaningless words (e.g.
Manager
, Wrapper
) in names.
Variable names should not contain type information - this can be derived from the signature.
var priceForwardCurveStruct: PriceForwardCurve = PriceForwardCurve(a = 1, b = 2, c = 3); // bad
var annualPriceForwardCurve: PriceForwardCurve = PriceForwardCurve(a = 1, b = 2, c = 3); // good
var paymentAmountInChf: chf = chf(50); // bad
var payment: chf = chf(50); // good
Note that when functions are duplicated for different argument types, it is fine to suffix the function name with the type information of its arguments. Alternatively, consider grouping the possible argument types into a union.
When using an acronym as part of a declaration name, capitalize it if it consists of two letters (IOStream
);
capitalize only the first letter if it is longer (XmlFormatter
,HttpInputStream
).
Localization
Unless contractually agreed differently, the code base sticks as much as possible to English for object names (protocols, permissions, functions etc.) to ensure easy understanding by developers and pave the way for potential reuse in libraries. Exceptions to this rule, in the case of projects with non-English language requirements:
-
Names that are exposed on APIs, which should be reflected as such in lower stacks, and not translated ("API first").
-
Canonical names in the context of the project (e.g.
"Technische Einheit"
) or domain specific terms that are difficult to translate into English (e.g."Lastgang"
)
Mixing English and non-English terms in naming is acceptable under these premises, as in importLastgangData
. In this
example, the verb should remain in English, as it is non-canonical.
Formatting
Indentation
Use 4 spaces for indentation.
For curly braces, put the opening brace in the end of the line where the construct begins, and the closing brace on a separate line aligned horizontally with the opening construct.
if (something) {
createSomething();
if (somethingElse) {
createSomethingElse();
};
};
Whitespace
Use white space to improve readability. Put space around {
and }
, before but not after (
, and after but not before
)
.
// no spaces
someFunctionCall(aValue);
// space before '(' and after ')'
if (someCondition) {
// ...
};
Also put a space after :
.
const SOME_VALUE = 0;
function doSomething(v: Number) -> {
// ...
}
Protocol headers
Protocol headers with a few constructor parameters can be kept on a single line.
protocol[party1, party2] GreatProtocol(param1: Number, param2: Text) {
// ...
}
Protocol headers with lots of constructor parameters (or long names) should really consider taking a struct as a parameter instead. But in rare cases, they can wrap over multiple lines as such.
protocol[party1, party2] ProtocolWithQuiteASubstantialNameButAlsoLoadsOfParameters(
param: Number,
anotherParam: Number,
yetAnotherParam: Number,
nameYourParamsBetterThanThis: Number,
orElse: Number,
nobodyWillBeAbleToReadYourCode: Number
) {
// ...
}
Struct formatting
Each field of a struct should be on a new line and indented with 4 spaces, unless it only has one value.
// OK - short.
struct SomeShortStruct { someValue: Number };
// A bit long.
struct SomeNotSoShortStruct { someValue: Number, yetAnotherValue: Boolean };
// Better if it is long.
struct SomeLongerStruct {
someValue: Number,
anotherValue: Boolean,
yetAnotherValue: Boolean
};
Function formatting
Functions should be no longer than 15 lines long; anything longer than this should be broken down into multiple functions.
Lambda formatting
Anonymous (lambda) functions should strive to put the argument and return type on the same line as the collection comprehension.
When wrapping short chained calls, put the .
operator on the next line, with a single indent.
When wrapping long chained calls, close the curly brace and parenthesis on the next line before the .
at the same
level as the original call.
Curly braces may be omitted for single statement anonymous functions.
// fine without curly braces
bananas.map(function(banana: Banana) returns SplitBanana -> split(banana));
// chained calls
bananas
.filter(function(banana: Banana) returns Boolean -> banana.color == Color.YELLOW)
.map(function(banana: Banana) returns SplitBanana -> split(banana))
.map(function(banana: SplitBanana) returns Number -> banana.numberOfPieces);
// closing braces/parentheses moved to the next line.
bananas.map(function(banana: Banana) returns SplitBanana -> {
debug("About to split a banana for some reason");
return split(banana);
}).fold(0, function(accumulator: Number, banana: SplitBanana) returns Number -> {
return accumulator + banana.numberOfPieces;
});
Best practices
Variables
Variables should generally not be reassigned in the same control flow.
// bad! multiple uses for same variable
function exampleOne() -> {
var someValue = 5;
insert(someValue);
someValue = calculateSomething();
report(someValue);
someValue = deriveSomething();
debug(someValue);
};
// fine - each path is a different control flow, and the variable is only actually assigned once
function exampleTwo() -> {
var someValue = if (someCondition) {
calculateSomething();
} else if (anotherCondition) {
calculateAnotherThing();
} else {
420;
};
callWith(someValue);
};
Functions
Within the function, check for errors as early as possible to adhere to the 'fail fast' principle.
// good
function executeExampleOne() returns Number -> {
if (!someSuccessCondition) {
return someFailureResult;
};
return someSuccessResult;
};
// bad
function executeExampleTwo() returns Number -> {
if (someSuccessCondition) {
return someSuccessResult;
};
return someFailureResult;
};
Functions should be no more than 3 indentations deep.
function exampleOfABadFunction() -> {
if (someCondition) {
if (someOtherCondition) {
// the inside of this conditional statement is TOO DEEP
if (yetAnotherCondition) {
debug("something");
};
};
};
};
Avoid boolean parameters or "flags", otherwise another developer must step into the function to get context on it.
// bad
function isBetweenBad(date1: DateTime, date2: DateTime, inclusiveBounds: Boolean) returns Boolean -> {
// ...
return true;
}
// clearer
function isBetweenInclusive(date1: DateTime, date2: DateTime) returns Boolean -> {
// ...
return true;
};
function isBetweenExclusive(date1: DateTime, date2: DateTime) returns Boolean -> {
// ...
return true;
};
Protocol layout
Don't arrange method declarations based on alphabetical order or visibility. Group related methods together to maintain logical flow and enhance readability for anyone reading the protocol in the top-to-bottom manner.
Conditional statements
- In function bodies, prefer to start with the negative scenario.
- Start
else
statements after the closing bracket of the correspondingif
. - Do not leave unnecessary syntactic elements in code just "for clarity".
// good
function conditional() -> {
if (someCondition) {
mutateSomething();
} else if (anotherCondition) {
mutateSomethingDifferent();
} else {
mutateSomethingTotallyDifferent();
};
performOtherMagic();
}
// bad - unnecessary else statement
function unnecessaryElse() returns Boolean -> {
if (someCondition) {
return true;
} else {
return false;
};
}
// good - no unnecessary else statement
function noUnnecessaryElse() returns Number -> {
if (someCondition) {
return 10;
};
return 2;
}
// bad - unnecessary if statement
function unnecessaryIf() returns Boolean -> {
if (someCondition) {
return true;
};
return false;
}
// good - just returns the predicate directly
function noUnnecessaryIf() returns Boolean -> {
return someCondition;
}
Comments
Use comments to clarify design decisions and aspects of the code that might be unclear. They're also valuable for detailing parameter limits and providing brief explanations where needed.
- They must not be used for TODOs.
- They must not document things that are obvious from the code.
- They must not be used in lieu of deleting code.
- They must not be used to highlight bugs
Tests
Tests should document the acceptance criteria that were implemented by the piece of code under test. Future developers may not have (easy) access to the tickets/requirements that were followed when developing a piece of code, and often tests serve as a form of documentation as well as protection against regression.
To that end, it must be clear what is being tested in a test and each test should generally only make one assertion,
i.e. test.assertEquals(...)
or test.assertFails(...)
. "All in one" tests may prevent against regressions, but in the
case of regressions it's harder to track down bugs, and in the case of bugs with passing tests, it's unclear for the
fixing developer to understand what past requirement was misunderstood.
When tests test full scenarios, the test should have a comment that describes the assumptions, test scenario and expected outcomes.
Tests should start with creation of all the necessary test objects (including expected result objects for comparison) with appropriate naming, and end with an assertion of the actual performance against the expected data.
Assertions should contain some text about the intended behaviour, and not an error message!
Tests that do not assert anything are not tests.
// bad - although the test will fail if `somePermissionThatMightFail` throws an exception
// it's not robust, and we haven't actually checked the behaviour of `somePermissionThatMightFail`,
// nor the state of `someObj`
@test
function testSomethingOne(test: Test) -> {
var someObj = SomeType[myParty]();
someObj.somePermissionThatMightFail[myParty]();
}
// better, but still bad - test will still fail if `somePermissionThatMightFail` blows up and
// if it succeeds we also check the new state of the protocol,
// BUT... the assertion text is an error message, instead of the intended behaviour
@test
function testSomethingTwo(test: Test) -> {
var someObj = SomeType[myParty]();
someObj.somePermissionThatMightFail[myParty]();
test.assertEquals(
expected = "/docs-0.0.1-SNAPSHOT?/conventions/SomeType$States.NEW_STATE",
actual = someObj.activeState().getOrFail().toText(),
message = "someObj had unexpected state!"
);
}
// perfect - the test asserts on state meaningfully, and the assertion contains a description
// of what ought to happen, not an error
@test
function testSomethingThree(test: Test) -> {
var someObj = SomeType[myParty]();
someObj.somePermissionThatMightFail[myParty]();
test.assertEquals(
expected = "/docs-0.0.1-SNAPSHOT?/conventions/SomeType$States.NEW_STATE",
actual = someObj.activeState().getOrFail().toText(),
message = "Some permission that might blow up changes the state to NEW_STATE."
);
}