Julia Coding Style Guide for SIIP

Goals

  • Define a straightforward set of rules that lead to consistent, readable code.
  • Developers focus on producing high quality code, not how to format it.

Base

Code Organization

  • Import standard modules, then 3rd-party modules, then yours. Include a blank line between each group.

Module

  • When writing a module locate all the exports in the main module file.
  • Please include a copy of this .gitignore file

Comments

  • Use comments to describe non-obvious or non-trivial aspects of code. Describe why something was done but not how. The "how" should be apparent from the code itself.

  • Use complete sentences and proper grammar.

  • Include a space in between the "#" and the first word of the comment.

  • Use these tags in comments to describe known work:

    • TODO: tasks that need to be done
    • FIXME: code that needs refactoring
    • BUG: known bug that exists. Should include a bug ID and tracking system.
    • PERF: known performance limitation that needs improvement

Constructors

  • Per guidance from Julia documentation, use inner constructors to enforce restrictions on parameters or to allow construction of self-referential objects. Use outer constructors to provide default values or to perform customization.
  • Document the reason why the outer constructor is different.
  • Note that the compiler will provide a default constructor with all struct members if no inner constructor is defined.
  • When creating a constructor use function Foo() instead of Foo() = ... One exception is the case where one file has all single-line functions.

Exceptions

  • Use exceptions for unexpected errors and not for normal error handling.
  • Detection of an unsupported data format from a user should likely throw an exception and terminate the application.
  • Do not use try/catch to handle retrieving a potentially-missing key from a dictionary.

Asserts

  • Use @assert statements to guard against programming errors. Do not use them after detecting bad user input. An assert tripping should indicate that there is a bug in the code. Note that they may be compiled out in optimized builds in the future.
  • Consider using InfrastructureSystems.@assert_op instead of the standard @assert because it will automatically print the value of the expression. Unlike the standard @assert the Julia compiler will never exclude @assert_op in optimized builds.
julia> a = 3; b = 4;
julia> @assert_op a == b
ERROR: AssertionError: 3 == 4

Globals

  • Global constants should use UPPER_CASE and be declared const.
  • If global variables are needed, prefix them with g_.
  • Don't use magic numbers. Instead, define const globals or Enums (Julia @enum).

One-line Conditionals

Julia code base uses this idiom frequently: <cond> && <statement> Example:

    function fact(n::Int)
       n >= 0 || error("n must be non-negative")
       n == 0 && return 1
       n * fact(n-1)
    end

This is acceptable for simple code as in this example. However, in general, prefer to write out an entire if statement.

Ternary operators provide a way to write clean, concise code. Use good judgement.

Good:

    y = x > 0 ? x : -x

There are many examples in our codebase that use the form <cond> ? <statement> : <statement>. These can be expressed much more clearly in an if/else statement.

Logging

When adding a debug log statement consider whether it is appropriate to append _group = <some-name>. The packages use this Julia feature to suppress debug logging of entire groups at once. InfrastructureSystems defines LOG_GROUPS with commonly-used group names.

If you are developing a feature with functionality in a single file then you can let Julia use the default name (the base name of the file). However, if the feature spans files then you should use an existing group or add a new one. Group names should be of type Symbol and follow the PascalCase naming convention.

Common group names should be defined in InfrastructureSystems but packages can add their own as needed.

Unit Tests

All code should be tested. The packages in SIIP have a minimum of 70% coverage to be merged into master. This functionality is provided using Codecov

Whitespace

  • If many function arguments cause the line length to be exceeded, put one argument per line. In some cases it may make sense to pair some variables on the same line.
    function foo(
                 var1::String,
                 var2::String,
                 var3::String,
                 var4::String,
                 var5::String,
                 var6::String,
                 )
  • Do not surround equal signs with spaces when passing keyword args to a function or defining default values in function declarations.
  • Do not right-align equal signs when assigning groups of variables. It causes unnecessary changes whenever someone adds a new variable with a longer name.

Bad:

    x   = 1
    foo = 2

Good:

    x = 1
    foo = 2
  • Define abstract types on one line. Given the lack of IDE support for Julia, this makes it easier to find type definitions.

Bad:

    abstract type
        Foo
    end

Good:

    abstract type Foo end

All SIIP packages perform tests using JuliaFormatter if you are unsure of your format, you can run julia -e 'using JuliaFormatter; include(".github/workflows/formatter_code.jl")' at the root of the package. Make sure to have the latest version of JuliaFormatter in your main environment