Skip to content

Latest commit

 

History

History
433 lines (274 loc) · 12.4 KB

arweave_styleguide.md

File metadata and controls

433 lines (274 loc) · 12.4 KB

Arweave code style

The main development language of the Arweave client is Erlang, and as the number of developers of the project continues to grow this style guide will act as a means of keeping the codebase clean and comprehensible.

Code comprehensibility

Module header comments

Each module should have a simplistic comment at the top that encompasses and describes the set of functions that can be found within it.

Module description comments should be prefixed with '%%%' .

%% Example: head of ar_serialize.

-module(ar_serialize).
-export([full_block_to_json_struct/1, block_to_json_struct/1, ...]).
-export([tx_to_json_struct/1, json_struct_to_tx/1]).
-export([wallet_list_to_json_struct/1, block_index_to_json_struct/1, ...]).
-export([jsonify/1, dejsonify/1]).
-export([query_to_json_struct/1, json_struct_to_query/1]).
-include("ar.hrl").
-include_lib("eunit/include/eunit.hrl").

%%% Module containing serialisation/deserialisation utility functions for use in the HTTP server.

Function clause comments

Function clause comments should be placed above the header.

Every function should have a comment describing its purpose, unless the function signature explains it well enough.

Function comments should not include implementation details unless absolutely required, the code itself should be the main conveyor of the specific implementation.

It is more important to comment exported functions.

A specification (-spec) may be used to document the function, as an alternative or an addition to the comment.

Function description comments should be prefixed with '%% @doc'.

%% Example

%% @doc Takes a list containing tx records and returns the number of those 'not_found'.
count_unavailable_txs(TXList) ->
    length([TX || TX <- TXList, TX == not_found]).

Sparing use of comments inside function bodies

Comments should only need to be used inside functions if the code being described has high complexity and without description would take reasonable time to trace and understand.

If the written code does have high complexity consider if descriptive variable names, code abstraction and or basic refactoring could improve the comprehensibility before resorting to commenting the code block.

%% Bad
sign_verify_test(Keypair) ->
	% Deconstructs keypair into two separate terms, Pub and Priv.
	{Priv, Pub} = Keypair,
	% Generates an integer between 1 and 100 to sign and then verify.
	Data = floor(rand:uniform() * 100),
	% Sign the data generated above.
	SignedData = sign(Priv, Data),
	% Verify the signed data.
	verify(Pub, SignedData).

%% Good
sign_verify_test({Priv, Pub}) ->
	Data = floor(rand:uniform() * 100),
	SignedData = sign(Priv, Data),
	verify(Pub, SignedData).

Use the minimal descriptive words for function names

Function names should be descriptive enough to explain the high-level purpose of the function whilst remaining short enough as to not hinder the code readability.

The name of the module can sometimes be used to help increase clarity without increasing the wordiness of the function name.

%% Bad function names
ar_tx:generate_data_segment_for_signing(TX).
ar_util:pretty_print_internal_ip_representation(IPAddr).
ar_retarget:is_current_block_retarget_block(Block).

%% Good function names
ar_tx:to_binary(TX).
ar_block:generate_block_from_shadow(BShadow).
ar_serialize:block_to_json_struct(Block).

Tests should be defined at the tail of the module

Tests should be defined as the last thing present within a module and should be prefixed with the following comment.

% Tests: {module name}

Maximum of eighty characters per line

A maximum of eighty characters should be present on any singular line.

To help enforce this styling consider using a ruler, most extensible editors will have this functionality by default or a simple plugin should be available to help.

Do not use if

The true -> subclause of the if clause is confusing because true suggests the if expression evaluates to true, while the clause is executed when the expression is false. Use case instead.

Try to avoid deeply nested code

Deeply nested code should be avoided as it can mask a large set of alternative code paths and can become very difficult to debug.

Code that uses case or receives structures should aim for a singular level of nesting and at most two levels of depth.

%% Bad
contains_data_tx([]) -> false;
contains_data_tx(TXList) ->
	[TX|Rest] = TXList,
    case is_record(TX, tx) of
        true ->
            case byte_size(TX#tx.data) > 0 of
                true -> true;
                false -> contains_data_tx(Rest)
            end;
        false -> error_not_tx.
    end.

%% Better
contains_data_tx([]) -> false;
contains_data_tx([TX|Rest]) when is_record(TX, tx) ->
	case byte_size(TX#tx.data) > 0 of
	    true -> true;
		false -> contains_data_tx(Rest)
	end;
contains_data_tx(_) ->
	error_not_tx.

Deconstruct arguments in the function header

The maximum number of variables should be deconstructed within the function clause header and not the clause body.

This makes the arguments to the function explicit and helps debugging as should the wrong form of data be provided no matching function clause will be found.

%% Bad
server(State, Keypair) ->
    Keypair = {Priv, Pub},
	State#state { peers = Peers, heard = HeardMsg, ignored = IgnoredMsg },
	...

%% Good
server(State#state {
    peers = Peers,
    heard = HeardMsg,
    ignored = IgnoredMsg
}, {Priv, Pub}) ->
	...

Atoms should be lowercase and separated by underscores

For easy recognisability the Arweave codebase uses descriptive lowercase atoms where multiple words are separated by the underscore character.

%% Bad atoms
'iAtom'
'block not found'

%% Good atoms
unavailable
block_not_found

Record definitions should include descriptions of fields

When a new record is defined the information regarding the purpose of each field should be included in a comment inline with the field it pertains to. These comments should be prefixed with a singular '%' character and aim to be as concise as possible.

-record(tx, {
	id = <<>>, 			% TX UID (Hash of signature)
	last_tx = <<>>, 	% Wallets last TX hash.
	owner = <<>>, 		% Public key of transaction owner.
	tags = [], 			% Indexable TX category identifiers.
	target = <<>>, 		% Wallet address of target of the tx.
	quantity = 0, 		% Amount of Winston to send
	data = <<>>, 		% Data body (if data transaction).
	signature = <<>>, 	% Transaction signature.
	reward = 0 			% Transaction mining reward.
}).

Redundant or deprecated code should be removed

Should existing code be made redundant with the implementation of new developments, this old code should be removed. It should not be left cluttering the code base as either code or comment.

If reference to these old implementation details is still required they will remain present in the project repositories version control.

Modules should export the minimal number of functions

Modules should export the minimum number of functions in which are externally required and these exports should be logically ordered.

This helps show the interface that the module exposes. Via looking at the exports other engineers should be able to identify which functions they need understand and are available for external use.

%% Bad function exporting
-compile(export_all).

%% Good function exporting
-export([sign/2, verify/3]).
-export([to_address/1]).

Variable names should be descriptive of the data they contain

Variable names should be descriptive of the data in which they are representing.

This helps in understanding the purpose of a codeblock without the need for verbose comments detailing its purpose.

%% Bad variables
sign_data(X, Y) ->
	{A, B} = X,
	sign(A, Y).

%% Good variables
sign_data(Keypair, Data) ->
    {Priv, Pub} = Keypair,
    sign(Priv, Data).

Use ar:console/1-2 to present information to the end user

Writing to the Erlang console is done via the ar:console/1-2 functions. This will also be written to the log file.

ar:console("Started mining on block height ~B", [Height]),
ar:console(
	[
		node_joined_successfully,
		{height, NewB#block.height}
	]
),

Use ar:info/1-2, ar:warn/1-2, ar:err/1-2 to generate log entries.

All three types of messages will be written to the one and only log file. Note! Errors (generated by ar:err/1-2) will also be displayed in the console.

ar:warn("Could not retrieve current block. Will retry in ~B seconds", [?REJOIN_TIMEOUT]),
ar:err(
	[
		node_not_joining,
		{reason, cannot_get_full_block_from_peer},
		{received_instead, NewB}
	]
),

Don't log huge messages

Avoid logging huge messages. Truncate arguments, e.g. with ~P like this:

ar:warn("Invalid Block Hash List: ~P", [BI, 100]),

Tuple construction/deconstruction

When constructing or deconstructing tuples ensure a space between each comma separated element.

%% Bad
{ one,two,three, A, B, C}

%% Good
{one, two, three, A, B, C}

List deconstruction

When deconstructing a list into head and tail ensure that a space is placed on either side of the '|' separation character.

%% Bad
[Head|Tail]

%% Good
[Head | Tail]

Record construction/deconstruction

When constructing or deconstructing a record ensure that a space is placed on either side of each field being handled.

%% Bad
State#state {first="hello", second="world"}

%% Good
State#state { first = "hello", second = "world" },

Function arguments on new lines

If the arguments for a given function call exceed the previously stated line length limit (80 characters) or contain an inline function split the arguments each on to new lines.

%% Bad
example() ->
	TotalTime = lists:foldl(fun(X, Acc) -> X + Acc end, 0, [12, 15, 8, 21, 35, 33, 14]),
	...

%% Better
example() ->
	TotalTime = lists:foldl(
		fun(X, Acc) -> X + Acc end,
		0,
		[12, 15, 8, 21, 35, 33, 14]
	),
	...

Error handling

Functions with side effects (in Erlang it boils down to IO) should return an {ok, ...} tuple upon successful execution, and error_code or {error_code, ...} otherwise.

When invoking functions with side effects, failing fast by only pattern matching against {ok, ...} is encouraged.

In rare cases when even unexpected failures have to be processed, like in the HTTP event loop, try/catch may be used.

Put tests for the module X into X_tests.erl

It is usually very difficult to separate tests from the actual code in the search results unless tests reside in the dedicated files. For instance, using separate files for tests makes it easier to see in how many places a particular function is used.

Version control

The Arweave client codebase is hosted on Github, the below standards define the criteria for committed code. We aim to adhere to these standards as to make it as easy possible for new contributors to get involved.

All committed code must be commented

All committed code should be fully commented and should aim to fit the styling as detailed in this document. Committing uncommented code is unhelpful to all those maintaining or exploring the project.

Code pushed to master must work

All code committed to the master branch of the Arweave project should be fully functioning.

This is a strict requirement as this is the prime location of where end users will be obtaining the software to join and participate in the network.

Commits should aim to be as atomic as possible

Code commits should aim to be a single logical change or addition to the codebase, though if not possible all logical alterations should be explained in the commit message, each separated by a comma.

- Added generic protocol implementation.
- Removed ar_deprecated.
- Added block shadows, refactored HTTP iface.

Commit message syntax

To keep the repository clean a set structure for commit messages has been decided.

  • The first character should be capitalized.
  • The message should be succinct.
  • The message should be in the imperative mood.
  • Multiple actions should be comma separated.

Commit description

In addition to a message, a commit should have a description focusing on why the change was made rather than what was made.

Commit example

Add arweave style guide

Inconsistent styling made it hard for us to view, comprehend, and edit the code so we had a discussion and agreed on the common style.