Giter Club home page Giter Club logo

bluestyle's Introduction

Blue: a Style Guide for Julia

Code Style: Blue

This document specifies style conventions for Julia code. These conventions were created from a variety of sources including Python's PEP8, Julia's Notes for Contributors, and Julia's Style Guide.

A Word on Consistency

When adhering to this style it's important to realize that these are guidelines and not rules. This is stated best in the PEP8:

A style guide is about consistency. Consistency with this style guide is important. Consistency within a project is more important. Consistency within one module or function is the most important.

But most importantly: know when to be inconsistent -- sometimes the style guide just doesn't apply. When in doubt, use your best judgment. Look at other examples and decide what looks best. And don't hesitate to ask!

Synopsis

Attempt to follow both the Julia Contribution Guidelines, the Julia Style Guide, and this guide. When convention guidelines conflict this guide takes precedence (known conflicts will be noted in this guide).

  • Use 4 spaces per indentation level, no tabs.
  • Try to adhere to a 92 character line length limit.
  • Use upper camel-case convention for modules and types.
  • Use lower case with underscores for method names (note: contrary to this, it is a common stylistic choice in the Julia base code to use lower case without underscores).
  • Import modules with using, with one module per line and at the top of the file when possible.
  • Comments are good, try to explain the intentions of the code.
  • Use whitespace to make the code more readable.
  • No whitespace at the end of a line (trailing whitespace).
  • Avoid padding brackets with spaces. ex. Int64(value) preferred over Int64( value ).

Contents

Code Formatting

Module Imports

Module imports should occur at the top of the file or right after a module declaration. Files loaded via an include should avoid specifying their own module imports and should instead add them to the file in which they were included (e.g. "src/Example.jl" or "test/runtests.jl").

A module import should only specify a single package per line. The lines should be ordered alphabetically by the package/module name (note: relative imports precede absolute imports).

# Yes:
using A
using B

# No:
using A, B

# No:
using B
using A

Imports which explicitly declare what to bring into scope should be grouped into: modules, constants, types, macros, and functions. These groupings should be specified in that order and each group's contents should be sorted alphabetically, typically with modules on a separate line. As pseudo-code:

using Example: $(sort(modules)...)
using Example: $(sort(constants)...), $(sort(types)...), $(sort(macros)...), $(sort(functions)...)

If you are only explicitly importing a few items you can alternatively use the following one-line form:

using Example: $(sort(modules)...), $(sort(constants)...), $(sort(types)...), $(sort(macros)...), $(sort(functions)...)

In some scenarios there may be alternate ordering within a group which makes more logical sense. For example when explicitly importing subtypes of Period you may want to sort them in units largest to smallest:

using Dates: Year, Month, Week, Day, Hour, Minute, Second, Millisecond

Explicit import lines which exceed the line length should use line-continuation or multiple import statements for the same package. Using multiple import statements should be preferred when creating alternate groupings:

# Yes:
using AVeryLongPackage: AVeryLongType, AnotherVeryLongType, a_very_long_function,
    another_very_long_function

# Yes:
using AVeryLongPackage: AVeryLongType, AnotherVeryLongType
using AVeryLongPackage: a_very_long_function, another_very_long_function

# No:
using AVeryLongPackage:
    AVeryLongType,
    AnotherVeryLongType,
    a_very_long_function,
    another_very_long_function

# No:
using AVeryLongPackage: AVeryLongType
using AVeryLongPackage: AnotherVeryLongType
using AVeryLongPackage: a_very_long_function
using AVeryLongPackage: another_very_long_function

Note: Prefer the use of imports with explicit declarations when writing packages. Doing so will make maintaining the package easier by knowing what functionality the package is importing and when dependencies can safely be dropped.

Prefer the use of using over import to ensure that extension of a function is always explicit and on purpose:

# Yes:
using Example

Example.hello(x::Monster) = "Aargh! It's a Monster!"
Base.isreal(x::Ghost) = false

# No:
import Base: isreal
import Example: hello

hello(x::Monster) = "Aargh! It's a Monster!"
isreal(x::Ghost) = false

If you do require the use of import then create separate groupings for import and using statements divided by a blank line:

# Yes:
import A: a
import C

using B
using D: d

# No:
import A: a
using B
import C
using D: d

Function Exports

All functions that are intended to be part of the public API should be exported. All function exports should occur at the top of the main module file, after module imports. Avoid splitting a single export over multiple lines; either define one export per line, or group them by theme.

# Yes:
export foo
export bar
export qux

# Yes:
export get_foo, get_bar
export solve_foo, solve_bar

# No:
export foo,
    bar,
    qux

Global Variables

Global variables should be avoided whenever possible. When required, global variables should be consts and have an all uppercase name separated with underscores (e.g. MY_CONSTANT). They should be defined at the top of the file, immediately after imports and exports but before an __init__ function. If you truly want mutable global style behaviour you may want to look into mutable containers or closures.

Function Naming

Names of functions should describe an action or property irrespective of the type of the argument; the argument's type provides this information instead. For example, submit_bid(bid) should be submit(bid::Bid) and bids_in_batch(batch) should be bids(batch::Batch).

Names of functions should usually be limited to one or two lowercase words separated by underscores. If you find it hard to shorten your function names without losing information, you may need to factor more information into the type signature or split the function's responsibilities into two or more functions. In general, shorter functions with clearly-defined responsibilities are preferred.

NOTE: Functions that are only intended for internal use should be marked with a leading underscore (e.g., _internal_utility_function(X, y)). Although it should be much less common, the same naming convention can be used for internal types and constants as well (e.g., _MyInternalType, _MY_CONSTANT). Marking a function as internal or private lets other people know that they shouldn't expect any kind of API stability from that functionality.

Method Definitions

Only use short-form function definitions when they fit on a single line:

# Yes:
foo(x::Int64) = abs(x) + 3

# No:
foobar(array_data::AbstractArray{T}, item::T) where {T<:Int64} = T[
    abs(x) * abs(item) + 3 for x in array_data
]
# Yes:
function foobar(array_data::AbstractArray{T}, item::T) where T<:Int64
    return T[abs(x) * abs(item) + 3 for x in array_data]
end

# No:
foobar(
    array_data::AbstractArray{T},
    item::T,
) where {T<:Int64} = T[abs(x) * abs(item) + 3 for x in array_data]

When using long-form functions always use the return keyword:

# Yes:
function fnc(x::T) where T
    result = zero(T)
    result += fna(x)
    return result
end

# No:
function fnc(x::T) where T
    result = zero(T)
    result += fna(x)
end
# Yes:
function Foo(x, y)
    return new(x, y)
end

# No:
function Foo(x, y)
    new(x, y)
end

When using the return keyword always explicitly return a value, even if it is return nothing.

# Yes:
function maybe_do_thing()
    # code
    return nothing
end

# No:
function maybe_do_thing()
    # code
    return
end

Functions definitions with parameter lines which exceed 92 characters should separate each parameter by a newline and indent by one-level:

# Yes:
function foobar(
    df::DataFrame,
    id::Symbol,
    variable::Symbol,
    value::AbstractString,
    prefix::AbstractString="",
)
    # code
end

# Ok:
function foobar(df::DataFrame, id::Symbol, variable::Symbol, value::AbstractString, prefix::AbstractString="")
    # code
end

# No: Don't put any args on the same line as the open parenthesis if they won't all fit.
function foobar(df::DataFrame, id::Symbol, variable::Symbol, value::AbstractString,
    prefix::AbstractString="")

    # code
end

# No: All args should be on a new line in this case.
function foobar(
    df::DataFrame, id::Symbol, variable::Symbol, value::AbstractString,
    prefix::AbstractString=""
)
    # code
end

# No: Indented too much.
function foobar(
        df::DataFrame,
        id::Symbol,
        variable::Symbol,
        value::AbstractString,
        prefix::AbstractString="",
    )
    # code
end

If all of the arguments fit inside the 92 character limit then you can place them on 1 line. Similarly, you can follow the same rule if you break up positional and keyword arguments across two lines.

# Ok:
function foobar(
    df::DataFrame, id::Symbol, variable::Symbol, value::AbstractString; prefix::String=""
)
    # code
end

# Ok: Putting all args and all kwargs on separate lines is fine.
function foobar(
    df::DataFrame, id::Symbol, variable::Symbol, value::AbstractString;
    prefix::String=""
)
    # code
end

# Ok: Putting all positional args on 1 line and each kwarg on separate lines.
function foobar(
    df::DataFrame, id::Symbol, variable::Symbol, value::AbstractString;
    prefix="I'm a long default setting that probably shouldn't exist",
    msg="I'm another long default setting that probably shouldn't exist",
)
    # code
end

# No: Because the separate line is more than 92 characters.
function foobar(
    df::DataFrame, id::Symbol, variable::Symbol, value::AbstractString; prefix::AbstractString=""
)
    # code
end

# No: The args and kwargs should be split up.
function foobar(
    df::DataFrame, id::Symbol, variable::Symbol,
    value::AbstractString; prefix::AbstractString=""
)
    # code
end

# No: The kwargs are more than 92 characters.
function foobar(
    df::DataFrame, id::Symbol, variable::Symbol, value::AbstractString;
    prefix="I'm a long default setting that probably shouldn't exist", msg="I'm another long default settings that probably shouldn't exist",
)
    # code
end

Keyword Arguments

When calling a function always separate your keyword arguments from your positional arguments with a semicolon. This avoids mistakes in ambiguous cases (such as splatting a Dict).

# Yes:
xy = foo(x; y=3)
ab = foo(; a=1, b=2)

# Ok:
ab = foo(a=1, b=2)

# No:
xy = foo(x, y=3)

Whitespace

  • Avoid extraneous whitespace immediately inside parentheses, square brackets or braces.

    # Yes:
    spam(ham[1], [eggs])
    
    # No:
    spam( ham[ 1 ], [ eggs ] )
  • Avoid extraneous whitespace immediately before a comma or semicolon:

    # Yes:
    if x == 4 @show(x, y); x, y = y, x end
    
    # No:
    if x == 4 @show(x , y) ; x , y = y , x end
  • Avoid whitespace around : in ranges. Use brackets to clarify expressions on either side.

    # Yes:
    ham[1:9]
    ham[9:-3:0]
    ham[1:step:end]
    ham[lower:upper-1]
    ham[lower:upper - 1]
    ham[lower:(upper + offset)]
    ham[(lower + offset):(upper + offset)]
    
    # No:
    ham[1: 9]
    ham[9 : -3: 1]
    ham[lower : upper - 1]
    ham[lower + offset:upper + offset]  # Avoid as it is easy to read as `ham[lower + (offset:upper) + offset]`
  • Avoid using more than one space around an assignment (or other) operator to align it with another:

    # Yes:
    x = 1
    y = 2
    long_variable = 3
    
    # No:
    x             = 1
    y             = 2
    long_variable = 3
  • Surround most binary operators with a single space on either side: assignment (=), updating operators (+=, -=, etc.), numeric comparisons operators (==, <, >, !=, etc.), lambda operator (->). Binary operators that may be excluded from this guideline include: the range operator (:), rational operator (//), exponentiation operator (^), optional arguments/keywords (e.g. f(x=1; y=2)).

    # Yes:
    i = j + 1
    submitted += 1
    x^2 < y
    
    # No:
    i=j+1
    submitted +=1
    x^2<y
  • Avoid using whitespace between unary operands and the expression:

    # Yes:
    -1
    [1 0 -1]
    
    # No:
    - 1
    [1 0 - 1]  # Note: evaluates to `[1 -1]`
  • Avoid extraneous empty lines. Avoid empty lines between single line method definitions and otherwise separate functions with one empty line, plus a comment if required:

    # Yes:
    # Note: an empty line before the first long-form `domaths` method is optional.
    domaths(x::Number) = x + 5
    domaths(x::Int) = x + 10
    function domaths(x::String)
        return "A string is a one-dimensional extended object postulated in string theory."
    end
    
    dophilosophy() = "Why?"
    
    # No:
    domath(x::Number) = x + 5
    
    domath(x::Int) = x + 10
    
    
    
    function domath(x::String)
        return "A string is a one-dimensional extended object postulated in string theory."
    end
    
    
    dophilosophy() = "Why?"
  • Function calls which cannot fit on a single line within the line limit should be broken up such that the lines containing the opening and closing brackets are indented to the same level while the parameters of the function are indented one level further. In most cases the arguments and/or keywords should each be placed on separate lines. Note that this rule conflicts with the typical Julia convention of indenting the next line to align with the open bracket in which the parameter is contained. If working in a package with a different convention follow the convention used in the package over using this guideline.

    # Yes:
    f(a, b)
    constraint = conic_form!(
        SOCElemConstraint(temp2 + temp3, temp2 - temp3, 2 * temp1),
        unique_conic_forms,
    )
    
    # No:
    # Note: `f` call is short enough to be on a single line
    f(
        a,
        b,
    )
    constraint = conic_form!(SOCElemConstraint(temp2 + temp3,
                                               temp2 - temp3, 2 * temp1),
                             unique_conic_forms)
  • Assignments using expanded notation for arrays or tuples, or function calls should have the first open bracket on the same line assignment operator and the closing bracket should match the indentation level of the assignment. Alternatively you can perform assignments on a single line when they are short:

    # Yes:
    arr = [
        1,
        2,
        3,
    ]
    arr = [
        1, 2, 3,
    ]
    result = func(
        arg1,
        arg2,
    )
    arr = [1, 2, 3]
    
    # No:
    arr =
    [
        1,
        2,
        3,
    ]
    arr =
    [
        1, 2, 3,
    ]
    arr = [
        1,
        2,
        3,
        ]
  • Nested arrays or tuples that are in expanded notation should have the opening and closing brackets at the same indentation level:

    # Yes:
    x = [
        [
            1, 2, 3,
        ],
        [
            "hello",
            "world",
        ],
        ['a', 'b', 'c'],
    ]
    
    # No:
    y = [
        [
            1, 2, 3,
        ], [
            "hello",
            "world",
        ],
    ]
    z = [[
            1, 2, 3,
        ], [
            "hello",
            "world",
        ],
    ]
  • Always include the trailing comma when working with expanded arrays, tuples or functions notation. This allows future edits to easily move elements around or add additional elements. The trailing comma should be excluded when the notation is only on a single-line:

    # Yes:
    arr = [
        1,
        2,
        3,
    ]
    result = func(
        arg1,
        arg2,
    )
    arr = [1, 2, 3]
    
    # No:
    arr = [
        1,
        2,
        3
    ]
    result = func(
        arg1,
        arg2
    )
    arr = [1, 2, 3,]
  • Triple-quotes and triple-backticks written over multiple lines should be indented. As triple-quotes use the indentation of the lowest indented line (excluding the opening quotes) the least indented line in the string or ending quotes should be indented once. Triple-backticks should also follow this style even though the indentation does not matter for them.

    # Yes:
    str = """
        hello
        world!
        """
    cmd = ```
        program
            --flag value
            parameter
        ```
    
    # No:
    str = """
    hello
    world!
    """
    cmd = ```
          program
              --flag value
              parameter
          ```
  • Assignments using triple-quotes or triple-backticks should have the opening quotes on the same line as the assignment operator.

    # Yes:
    str2 = """
           hello
       world!
       """
    
    # No:
    str2 =
       """
           hello
       world!
       """
  • Group similar one line statements together.

    # Yes:
    foo = 1
    bar = 2
    baz = 3
    
    # No:
    foo = 1
    
    bar = 2
    
    baz = 3
  • Use blank-lines to separate different multi-line blocks.

    # Yes:
    if foo
        println("Hi")
    end
    
    for i in 1:10
        println(i)
    end
    
    # No:
    if foo
        println("Hi")
    end
    for i in 1:10
        println(i)
    end
  • After a function definition, and before an end statement do not include a blank line.

    # Yes:
    function foo(bar::Int64, baz::Int64)
        return bar + baz
    end
    
    # No:
    function foo(bar::Int64, baz::Int64)
    
        return bar + baz
    end
    
    # No:
    function foo(bar::In64, baz::Int64)
        return bar + baz
    
    end
  • Use line breaks between control flow statements and returns.

    # Yes:
    function foo(bar; verbose=false)
        if verbose
            println("baz")
        end
    
        return bar
    end
    
    # Ok:
    function foo(bar; verbose=false)
        if verbose
            println("baz")
        end
        return bar
    end

NamedTuples

The = character in NamedTuples should be spaced as in keyword arguments. No space should be put between the name and its value. NamedTuples should not be prefixed with ; at the start. The empty NamedTuple should be written NamedTuple() not (;)

# Yes:
xy = (x=1, y=2)
x = (x=1,)  # Trailing comma required for correctness.
x = (; kwargs...)  # Semicolon required to splat correctly.

# No:
xy = (x = 1, y = 2)
xy = (;x=1,y=2)
x = (; x=1)

Numbers

Floating-point numbers should always include a leading and/or trailing zero:

# Yes:
0.1
2.0
3.0f0

# No:
.1
2.
3.f0

Ternary Operator

Ternary operators (?:) should generally only consume a single line. Do not chain multiple ternary operators. If chaining many conditions, consider using an if-elseif-else conditional, dispatch, or a dictionary.

# Yes:
foobar = foo == 2 ? bar : baz

# No:
foobar = foo == 2 ?
    bar :
    baz
foobar = foo == 2 ? bar : foo == 3 ? qux : baz

As an alternative, you can use a compound boolean expression:

# Yes:
foobar = if foo == 2
    bar
else
    baz
end

foobar = if foo == 2
    bar
elseif foo == 3
    qux
else
    baz
end

For loops

For loops should always use in, never = or โˆˆ. This also applies to list and generator comprehensions

# Yes
for i in 1:10
    #...
end

[foo(x) for x in xs]

# No:
for i = 1:10
    #...
end

[foo(x) for x โˆˆ xs]

Modules

Normally a file that includes the definition of a module, should not include any other code that runs outside that module. i.e. the module should be declared at the top of the file with the module keyword and end at the bottom of the file. No other code before, or after (except for module docstring before). In this case the code with in the module block should not be indented.

Sometimes, e.g. for tests, or for namespacing an enumeration, it is desirable to declare a submodule midway through a file. In this case the code within the submodule should be indented.

Type annotation

Annotations for function definitions should be as general as possible.

# Yes:
splicer(arr::AbstractArray, step::Integer) = arr[begin:step:end]

# No:
splicer(arr::Array{Int}, step::Int) = arr[begin:step:end]

Using as generic types as possible allows for a variety of inputs and allows your code to be more general:

julia> splicer(1:10, 2)
1:2:9

julia> splicer([3.0, 5, 7, 9], 2)
2-element Array{Float64,1}:
 3.0
 7.0

Annotations on type fields need to be given a little more thought. Using specific concrete types for fields allows Julia to optimize the memory layout but can reduce flexibility. For example lets take a look at the type MySubString which allows us to work with a subsection of a string without having to copy the data:

mutable struct MySubString <: AbstractString
    string::AbstractString
    offset::Integer
    endof::Integer
end

We want the type to be able to hold any subtype of AbstractString but do we need to have offset and endof to be able to hold any subtype of Integer? Really, no, we should be ok to use Int here (Int64 on 64-bit systems and Int32 on 32-bit systems). Note that even though we're using Int a user can still do things like MySubString("foobar", 0x4, 0x6); as provided offset and endof values will be converted to an Int.

mutable struct MySubString <: AbstractString
    string::AbstractString
    offset::Int
    endof::Int
end

If we truly care about performance there is one more thing we can do by making our type parametric. The current definition of MySubString allows us to modify the string field at any time with any subtype of AbstractString. Using a parametric type allows us to use any subtype of AbstractString upon construction but the field type will be set to something concrete (like String) and cannot be changed for the lifetime of the instance.

mutable struct MySubString{T<:AbstractString} <: AbstractString
    string::T
    offset::Integer
    endof::Integer
end

Overall, it is best to keep the types general to start with and later optimize them using parametric types. Optimizing too early in the code design process can impact your ability to refactor the code early on.

Package version specifications

For simplicity and consistency with the wider Julia community, avoid including the default caret specifier when specifying package version requirements.

# Yes:
DataFrames = "0.17"

# No:
DataFrames = "^0.17"

Comments

Comments should be used to state the intended behaviour of code. This is especially important when the code is doing something clever that may not be obvious upon first inspection. Avoid writing comments that state exactly what the code obviously does.

# Yes:
x = x + 1      # Compensate for border

# No:
x = x + 1      # Increment x

Comments that contradict the code are much worse than no comments. Always make a priority of keeping the comments up-to-date with code changes!

Comments should be complete sentences. If a comment is a phrase or sentence, its first word should be capitalized, unless it is an identifier that begins with a lower case letter (never alter the case of identifiers!).

If a comment is short, the period at the end can be omitted. Block comments generally consist of one or more paragraphs built out of complete sentences, and each sentence should end in a period.

Comments should be separated by at least two spaces from the expression and have a single space after the #.

When referencing Julia in documentation note that "Julia" refers to the programming language while "julia" (typically in backticks, e.g. julia) refers to the executable.

Only use inline comments if they fit within the line length limit. If your comment cannot be fitted inline then place the comment above the content to which it refers:

# Yes:

# Number of nodes to predict. Again, an issue with the workflow order. Should be updated
# after data is fetched.
p = 1

# No:

p = 1  # Number of nodes to predict. Again, an issue with the workflow order. Should be
# updated after data is fetched.

Documentation

It is recommended that most modules, types and functions should have docstrings. That being said, only exported functions are required to be documented. Avoid documenting methods like == as the built in docstring for the function already covers the details well. Try to document a function and not individual methods where possible as typically all methods will have similar docstrings. If you are adding a method to a function which already has a docstring only add a docstring if the behaviour of your function deviates from the existing docstring.

Docstrings are written in Markdown and should be concise. Docstring lines should be wrapped at 92 characters.

"""
    bar(x[, y])

Compute the Bar index between `x` and `y`. If `y` is missing, compute the Bar index between
all pairs of columns of `x`.
"""
function bar(x, y) ...

When types or methods have lots of parameters it may not be feasible to write a concise docstring. In these cases it is recommended you use the templates below. Note if a section doesn't apply or is overly verbose (for example "Throws" if your function doesn't throw an exception) it can be excluded. It is recommended that you have a blank line between the headings and the content when the content is of sufficient length. Try to be consistent within a docstring whether you use this additional whitespace. Note that the additional space is only for reading raw markdown and does not affect the rendered version.

Type Template (should be skipped if is redundant with the constructor(s) docstring):

"""
    MyArray{T,N}

My super awesome array wrapper!

# Fields
- `data::AbstractArray{T,N}`: stores the array being wrapped
- `metadata::Dict`: stores metadata about the array
"""
struct MyArray{T,N} <: AbstractArray{T,N}
    data::AbstractArray{T,N}
    metadata::Dict
end

Function Template (only required for exported functions):

"""
    mysearch(array::MyArray{T}, val::T; verbose=true) where {T} -> Int

Searches the `array` for the `val`. For some reason we don't want to use Julia's
builtin search :)

# Arguments
- `array::MyArray{T}`: the array to search
- `val::T`: the value to search for

# Keywords
- `verbose::Bool=true`: print out progress details

# Returns
- `Int`: the index where `val` is located in the `array`

# Throws
- `NotFoundError`: I guess we could throw an error if `val` isn't found.
"""
function mysearch(array::AbstractArray{T}, val::T) where T
    ...
end

If your method contains lots of arguments or keywords you may want to exclude them from the method signature on the first line and instead use args... and/or kwargs....

"""
    Manager(args...; kwargs...) -> Manager

A cluster manager which spawns workers.

# Arguments

- `min_workers::Integer`: The minimum number of workers to spawn or an exception is thrown
- `max_workers::Integer`: The requested number of workers to spawn

# Keywords

- `definition::AbstractString`: Name of the job definition to use. Defaults to the
    definition used within the current instance.
- `name::AbstractString`: ...
- `queue::AbstractString`: ...
"""
function Manager(...)
    ...
end

Feel free to document multiple methods for a function within the same docstring. Be careful to only do this for functions you have defined.

"""
    Manager(max_workers; kwargs...)
    Manager(min_workers:max_workers; kwargs...)
    Manager(min_workers, max_workers; kwargs...)

A cluster manager which spawns workers.

# Arguments

- `min_workers::Int`: The minimum number of workers to spawn or an exception is thrown
- `max_workers::Int`: The requested number of workers to spawn

# Keywords

- `definition::AbstractString`: Name of the job definition to use. Defaults to the
    definition used within the current instance.
- `name::AbstractString`: ...
- `queue::AbstractString`: ...
"""
function Manager end

If the documentation for bullet-point exceeds 92 characters the line should be wrapped and slightly indented. Avoid aligning the text to the :.

"""
...

# Keywords
- `definition::AbstractString`: Name of the job definition to use. Defaults to the
    definition used within the current instance.
"""

For additional details on documenting in Julia see the official documentation.

For documentation written in Markdown files such as README.md or docs/src/index.md use exactly one sentence per line.

Test Formatting

Testsets

Julia provides test sets which allows developers to group tests into logical groupings. Test sets can be nested and ideally packages should only have a single "root" test set. It is recommended that the "runtests.jl" file contains the root test set which contains the remainder of the tests:

@testset "PkgExtreme" begin
    include("arithmetic.jl")
    include("utils.jl")
end

Comparisons

Most tests are written in the form @test x == y. Since the == function doesn't take types into account tests like the following are valid: @test 1.0 == 1. Avoid adding visual noise into test comparisons:

# Yes:
@test value == 0

# No:
@test value == 0.0

Performance and Optimization

Several of these tips are contained within Julia's Performance Tips.

Much of Julia's performance gains come from being able to specialize functions on their input types. Putting variables and functionality in the global namespace or module's namespace thwarts this. One consequence of this is that conventional MATLAB-style scripts will result in surprisingly slow code. There are two ways to mitigate this:

  • Move as much functionality into functions as possible.
  • Declare global variables as constants using const.

Remember that the first time you call a function with a certain type signature it will compile that function for the given input types. Compilation is sometimes a significant portion of time, so avoid profiling/timing functions on their first run. Note that the @benchmark and @btime macros from the BenchmarkTools package can be useful as they run the function many times and report summary statistics of time and memory allocation, alleviating the need to run the function first before benchmarking.

Editor Configuration

Sublime Text Settings

If you are a user of Sublime Text we recommend that you have the following options in your Julia syntax specific settings. To modify these settings first open any Julia file (*.jl) in Sublime Text. Then navigate to: Preferences > Settings - More > Syntax Specific - User

{
    "translate_tabs_to_spaces": true,
    "tab_size": 4,
    "trim_trailing_white_space_on_save": true,
    "ensure_newline_at_eof_on_save": true,
    "rulers": [92]
}

Vim Settings

If you are a user of Vim we recommend that you add to your .vim/vimrc file:

" ~/.vim/vimrc
set tabstop=4       " Set tabstops to a width of four columns.
set softtabstop=4   " Determine the behaviour of TAB and BACKSPACE keys with expandtab.
set shiftwidth=4    " Determine the results of >>, <<, and ==.

" Identify .jl files as Julia. If using julia-vim plugin, this is redundant.
autocmd BufRead,BufNewFile *.jl set filetype=julia

Then create or edit .vim/after/ftplugin/julia.vim, adding the Julia-specific configuration:

" ~/.vim/after/ftplugin/julia.vim
setlocal expandtab       " Replace tabs with spaces.
setlocal textwidth=92    " Limit lines according to Julia's CONTRIBUTING guidelines.
setlocal colorcolumn=+1  " Highlight first column beyond the line limit.

Additionally, you may find is useful to use the julia-vim plugin which adds Julia-aware syntax highlighting and a few cool other features.

Atom Settings

Atom defaults preferred line length to 80 characters. We want that at 92 for julia. To change it:

  1. Go to Atom -> Preferences -> Packages.
  2. Search for the "language-julia" package and open the settings for it.
  3. Find preferred line length (under "Julia Grammar") and change it to 92.

VS-Code Settings

If you are a user of VS Code we recommend that you have the following options in your Julia syntax specific settings. To modify these settings open your VS Code Settings with CMD+, (Mac OS) or CTRL+, (other OS), and add to your settings.json:

{
    "[julia]": {
        "editor.detectIndentation": false,
        "editor.insertSpaces": true,
        "editor.tabSize": 4,
        "files.insertFinalNewline": true,
        "files.trimFinalNewlines": true,
        "files.trimTrailingWhitespace": true,
        "editor.rulers": [92],
    },
}

Additionally you may find the Julia VS-Code plugin useful.

Code Style Badge

Let contributors know your project is following the Blue style guide by adding the badge to your README.md.

[![Code Style: Blue](https://img.shields.io/badge/code%20style-blue-4495d1.svg)](https://github.com/JuliaDiff/BlueStyle)

The badge is blue: Code Style: Blue

bluestyle's People

Contributors

atsushisakai avatar bcj avatar bsnelling avatar christiankurz avatar christopher-dg avatar cmichelenstrofer avatar dellison avatar fchorney avatar felixcremer avatar gdalle avatar harryscholes avatar herbhuang avatar ho-oto avatar iamed2 avatar marcmush avatar mattbrzezinski avatar mjp98 avatar mzgubic avatar nickrobinson251 avatar nicoleepp avatar okonsamuel avatar omus avatar oxinabox avatar pitmonticone avatar raphaelsaavedra avatar rikhuijzer avatar rofinn avatar rturquier avatar singularitti avatar tylerloewen avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

bluestyle's Issues

vscode editor

I see vscode is left out of the editor section. It would be nice to include that, since it has a non-trivial development community!

Line limit to 120

I'm finding the 92 line limit very restrictive and forcing breaks more often than not. Between giving a function a meaningful parameter name and typing it you're more often than not breaking this line with a couple parameters.

I'd propose increasing this line length to 120, it allows you to reasonably read two files side-by-side in an editor. I think this is goes a bit hand in hand with #76 and I think makes things easier to read.

Some Linus thoughts from a fixed 80 character limit.

How to wrap chained short-circuiting logic operations?

If we have code the goes over the line limit, like

if primal_name isa Symbol || Meta.isexpr(primal_name, :(.)) || Meta.isexpr(primal_name, :curly)
    # do stuff
end

How should that be wrapped?

I think our preference would be for something like this

if (
    primal_name isa Symbol ||
    Meta.isexpr(primal_name, :(.)) ||
    Meta.isexpr(primal_name, :curly)
)
    # do stuff
end

does that seem right?

Revisit the spacing in binary operators

Hi!

Sometimes, when writing large equations, the rule of adding spacing to all binary operators can lead to a very verbose text. Compare, for example:

aux_U += P[n + 1, m + 1] * (C[n + 1, m + 1] * cos_mฮป + S[n + 1, m + 1] * sin_mฮป)
aux_U += P[n+1, m+1] * (C[n+1, m+1]*cos_mฮป + S[n+1, m+1]*sin_mฮป)

IMHO the latter is much more readable and understandable because the spacing is used to separate the factors.

Whitespacing rules inside of functions

Stemming from this merge request I think having a discussion about whitespacing for this style guide could be beneficial.

Currently we do not have any guidelines around whitespacing inside of functions. There have been a few occasions where I've seen code formatted similarly to this:

function do_request(aws::AWSConfig, request::Request; return_headers::Bool=false)
    response = nothing
    TOO_MANY_REQUESTS = 429
    EXPIRED_ERROR_CODES = ["ExpiredToken", "ExpiredTokenException", "RequestExpired"]
    REDIRECT_ERROR_CODES = [301, 302, 303, 304, 305, 307, 308]
    THROTTLING_ERROR_CODES = [
        "Throttling",
        "ThrottlingException",
        "ThrottledException",
        "RequestThrottledException",
        "TooManyRequestsException",
        "ProvisionedThroughputExceededException",
        "LimitExceededException",
        "RequestThrottled",
        "PriorRequestNotComplete"
    ]
    if isempty(request.headers)
        request.headers = LittleDict{String, String}()
    end
    request.headers["User-Agent"] = user_agent
    request.headers["Host"] = HTTP.URI(request.url).host
    @repeat 3 try
        aws.credentials === nothing || _sign!(aws, request)
        response = @mock _http_request(aws, request)
        if response.status in REDIRECT_ERROR_CODES && HTTP.header(response, "Location") != ""
            request.url = HTTP.header(response, "Location")
            continue
        end
    catch e
        if e isa HTTP.StatusError
            e = AWSException(e)
        end
        @retry if "message" in fieldnames(typeof(e)) && occursin("Signature expired", e.message) end
        @retry if ecode(e) in EXPIRED_ERROR_CODES
            check_credentials(aws.credentials, force_refresh=true)
        end
        @delay_retry if e isa AWSException &&
            (http_status(e.cause) == TOO_MANY_REQUESTS || ecode(e) in THROTTLING_ERROR_CODES)
        end
        @retry if e isa AWSException && (
            header(e.cause, "crc32body") == "x-amz-crc32" ||
            ecode(e) in ("BadDigest", "RequestTimeout", "RequestTimeoutException")
        )
        end
    end
    if request.request_method == "HEAD"
        return Dict(response.headers)
    end
    if request.return_stream
        return request.response_stream
    end
    if request.return_raw
        return (return_headers ? (response.body, response.headers) : response.body)
    end
    mime = HTTP.header(response, "Content-Type", "")
    if isempty(mime)
        if length(response.body) > 5 && String(response.body[1:5]) == "<?xml"
            mime = "text/xml"
        end
    end
    body = String(copy(response.body))
    if occursin(r"/xml", mime)
        xml_dict_type = OrderedDict{Union{Symbol, String}, Any}
        return (return_headers ? (xml_dict(body, xml_dict_type), Dict(response.headers)) : xml_dict(body, xml_dict_type))
    end
    if occursin(r"json$", mime)
        if isempty(response.body)
            return (return_headers ? (nothing, response.headers) : nothing)
        end
        info = request.ordered_json_dict ? JSON.parse(body, dicttype=OrderedDict) : JSON.parse(body)
        return (return_headers ? (info, Dict(response.headers)) : info)
    end
    return (return_headers ? (response.body, response.headers) : response.body)
end

Personally I find reading something like this to be very difficult and at a quick glance it's hard to tell everything that is happening in it. I'd like to propose a few additions to the styling guide

  1. Grouping similar one-line statements together
# Yes
response = nothing
TOO_MANY_REQUESTS = 429
EXPIRED_ERROR_CODES = ["ExpiredToken", "ExpiredTokenException", "RequestExpired"]
REDIRECT_ERROR_CODES = [301, 302, 303, 304, 305, 307, 308]

# No
response = nothing

TOO_MANY_REQUESTS = 429

EXPIRED_ERROR_CODES = ["ExpiredToken", "ExpiredTokenException", "RequestExpired"]

REDIRECT_ERROR_CODES = [301, 302, 303, 304, 305, 307, 308]
  1. Do NOT group multi-line statements together
# Yes
if request.request_method == "HEAD"
    return Dict(response.headers)
end

if request.return_stream
    return request.response_stream
end

# No
if request.request_method == "HEAD"
    return Dict(response.headers)
end
if request.return_stream
    return request.response_stream
end
  1. Do NOT whitespace nested control flow statements
# Yes
if isempty(mime)
    if length(response.body) > 5 && String(response.body[1:5]) == "<?xml"
        mime = "text/xml"
    end
end

# No
if isempty(mime)
    
    if length(response.body) > 5 && String(response.body[1:5]) == "<?xml"
        mime = "text/xml"
    end

end
  1. Optional line break between one-line statements for return
# Yes, preferable
if occursin(r"json$", mime)
    if isempty(response.body)
        return (return_headers ? (nothing, response.headers) : nothing)
    end

    info = request.ordered_json_dict ? JSON.parse(body, dicttype=OrderedDict) : JSON.parse(body)
    return (return_headers ? (info, Dict(response.headers)) : info)
end

# Yes, acceptable
if occursin(r"json$", mime)
    if isempty(response.body)
        return (return_headers ? (nothing, response.headers) : nothing)
    end

    info = request.ordered_json_dict ? JSON.parse(body, dicttype=OrderedDict) : JSON.parse(body)

    return (return_headers ? (info, Dict(response.headers)) : info)
end

# Yes, preferable
if occursin(r"json$", mime)
    if isempty(response.body)
        return (return_headers ? (nothing, response.headers) : nothing)
    end
    
    info = Dict()
    info["foo"] = "bar"
    delete!(info, "foo")
    info = request.ordered_json_dict ? JSON.parse(body, dicttype=OrderedDict) : JSON.parse(body)

    return (return_headers ? (info, Dict(response.headers)) : info)
end

# Yes, acceptable
if occursin(r"json$", mime)
    if isempty(response.body)
        return (return_headers ? (nothing, response.headers) : nothing)
    end
    
    info = Dict()
    info["foo"] = "bar"
    delete!(info, "foo")
    info = request.ordered_json_dict ? JSON.parse(body, dicttype=OrderedDict) : JSON.parse(body)
    return (return_headers ? (info, Dict(response.headers)) : info)
end
  1. Line break between control flow statement and return
# Yes
if isempty(mime)
    if length(response.body) > 5 && String(response.body[1:5]) == "<?xml"
        mime = "text/xml"
    end
end

return mime

# No
if isempty(mime)
    if length(response.body) > 5 && String(response.body[1:5]) == "<?xml"
        mime = "text/xml"
    end
end
return mime

typo in section Type Annotation

Using as generic types as possible allows for a variety of inputs and allows you code to be more general:

=>

Using generic types as much as possible allows for a variety of inputs and allows you code to be more general:

Remove line limit rule

As an alternative to #59
while the whole guide is suggests, we could be more explict about the line limit being a suggestion.

What if we removed the line limit rule, and said instead something a long the lines of:

Lines should be kept to a reasonable length.
In general 92 characters is a reasonable length, but for some things longer (or shorter) may better promote clarity.
For example rather than very long list of function arguments on one line instead ....
or for ternary expressions use if instread, e.g. ...
Conversely, for log messages its often clearer to have a very long string than to split the string up and then concatenate it back together.

Argument for this over #59 is that if you are going to set up to need to deal with really long lines, and thus have your editor either setup to wrap or to horisontal scroll, then once you are setup to do it for string literals it remains setup for other needs.
Counter argument is that strign literals are infact special, and you often don't need to care what is at the other end of the string -- because its almost always a log message or similar, so you only look at enough to workout what it is telling the user.

Just putting this alternative out there for consideration

Do we want whitespace around `//`

Currently guidance:

Surround most binary operators with a single space on either side: ...
Binary operators may be excluded from this guideline include: the range operator (:), rational operator (//), exponentiation operator (^)

But for the sake of implementing a formatter (#7, domluna/JuliaFormatter.jl#283), do we prefer 1//2 or 1 // 2?

No line breaks after `=`

Discussed w/ @nickrobinson251 offline, we should add rule that states to never line break after an = operation.

Example:

# Yes
function_result = some_long_function_that_goes_over_the_92_char_line_limit(
    some_param, 
    another_param
)

# No
function_result = 
    some_long_function_that_goes_over_the_92_char_line_limit(some_param, another_param)

Short-circuit logic used as control flow should only be used on a single line

This came up over at JuliaDatabases/LibPQ.jl#197 (comment)

e.g. for && and ||

# Yes
if last_log == curr
    debug(LOGGER, "Consuming input from connection $(jl_conn.conn). Stand by for landing.")
end

#ย No, over line limit:
last_log == curr && debug(LOGGER, "Consuming input from connection $(jl_conn.conn). Stand by for landing.")

# No, use an `if` conditional:
last_log == curr &&
    debug(LOGGER, "Consuming input from connection $(jl_conn.conn). Stand by for landing.")

(aside: we may want to use a different example in the guide, given #59 is an open question)

This is consistent with out current advice on ternary conditionals:

Ternary operators (?:) should generally only consume a single line

i.e.

# Yes:
foobar = if some_big_long_really_long_expr_here_long == 2
    barrrr_more_long
else
    bazzz_also_not_short
end

# No:
foobar = some_big_long_really_long_expr_here_long == 2 ? barrrr_more_long : bazzz_also_not_short

Unlike ternary conditionals ?:, chaning short-circuit logic as conditionals is fine e.g. this is okay

is_red(x) || is_blue(x) || is_yellow(x) && println("It's a primary colour!")

Prefer `return nothing` over bare `return`

I thought this was already the BlueStyle guidance, but i don't see it.

Which is preferred?
(a)

function maybe_do_thing()
    # code 
    return
end

or
(b)

function maybe_do_thing()
    # code 
    return nothing
end

If we are agreed on option (b) already we should write it down and i'll add it to the to-do list in domluna/JuliaFormatter.jl#283

Go public

@omus if you are happy with this, can you flick the button to switch over to public.

Guidelines for macros

I'm currently working with AWSCore.jl and came across macro usage being over the line limit. I wasn't able to find a guideline for it here, so I figured I'd open it up to discussion.

Examples:

@delay_retry if e isa AWSException &&
    (http_status(e.cause) == TOO_MANY_REQUESTS || ecode(e) in throttling_error_codes)
end

@retry if e isa AWSException && (
    header(e.cause, "crc32body") == "x-amz-crc32" ||
    ecode(e) in ("BadDigest", "RequestTimeout", "RequestTimeoutException")
)
end

@retry if :message in fieldnames(typeof(e)) && occursin("Signature expired", e.message) end

Some comments and areas that could use clarification

Good on you guys for publishing this ๐Ÿ‘ No idea how long it's been around but I'm just noticing it now with the badge.

Read through the guide and noticed a couple of things:

Imports

Should there be any distinction between where imports come from? To illustrate, here's what I've taken to doing:

# Base and Core go first.
using Base: front
using Core: Builtin

# And then stdlibs.
using Dates: year

# And then external packages.
using Parameters: @with_kw

Not sure if that's something you want to look into, just putting in my 2c.

Keyword Arguments

There should probably be a case for keyword-only functions. Should I use f(x=1, y=2) or f(; x=1, y=2)? I always use the latter.

Whitespace

Ranges: What about literals as offsets? e.g. should i use xs[x:y-1] or xs[x : y - 1]? I go with the former.

NamedTuples

When there's only one element: (; x=1) or (x=1,)? I use the former, but I also like to always use the semicolon. (x=1) does not work, it parses to x = 1.

Comments

If your comment comment cannot be fitted inline then place the comment above the content to which it refers

Maybe there should be a note about sentence splitting here? I would write the example as:

# Number of nodes to predict. Again, an issue with the workflow order. 
# Should be updated after data is fetched.
p = 1

Documentation

I think this discussion has been had before but I forget the outcome. Is one sentence per line okay? In the first example, I would have split the line after the first sentence, kind of like what I said about comments too. I'd rather have overlong lines for docstrings than lines that only contain a really long URL, personally. When it makes sense, I also split lines on commas if things get really long.


This has been a bit of a brain dump. Some of it is just reflective of my own preferences, but some of it probably warrants clarification. Don't feel obligated to agree with my opinions ๐Ÿ˜„

More guidelines on method names

Thanks for this wonderful guide! I wanted to ask if there could be slightly more clarification/modification concerning method names. Sep

It would be nice to have consistency with base when defining methods like isimmutable where readability isn't as much of an issue. This would be especially nice for cases where a package may have code that eventually moves to base or the standard library (however rare that may be).

Potential solutions:

  • Identify specific patterns that violate the underscore rule but are acceptable.
  • Some sort of word combination limit (like 2 letter words don't need to be followed with an underscore)

Root test clarification

For you root test suggestion, some explsination may be necessary on what are "arithmetic.jl" and "urils.jl". Are they actual code or tests? Are they tests for modules in the same folder as 'runtests.jl'? Coming from pytest background testfile names as 'test_something.jl' feel appropriate, even though you would not need test_ prefix for test discovery.

@testset "PkgExtreme" begin
    include("arithmetic.jl")
    include("utils.jl")
end

Filter out unrelated commits

We should probably recreate this repository where we filter out all commits that didn't edit this file internally. After we copy over just the filtered code to the original file the we can just do a git mv. We have a git guide in the wiki where this came from that explains how to do that.

Prefer `where {T}` to `where T`?

As far as i can am aware, BlueStyle has no opinion on this.

even the example code in this style guide itself is inconsistent, sometimes using where {T} and othertimes where T (or where {T <: Int} and othertimes where T <: Int).

We could stick with having no preference, however, for the sake of running a formatter (domluna/JuliaFormatter.jl#283, #7 ), what should a formatter do?

We have four options (to my mind, anyway)

  1. always prefer braces: where {T} to where T
  2. always prefer NO braces: where T to where {T} (when valid)
  3. sometimes prefer NO braces (in a subset of cases of the valid cases, based on some rule)
  4. do nothing

Option (4) is "do nothing". This isn't a great option for 2 reasons: (1) stylistically, we probably want a code-base to be consistent on this, it's one thing to say "either is fine", and another to say "both is fine". (2) effort-wise, the formatter can/does already fix these cases, so i see no reason to add functionality to leave a code-base less consistent.

I see no reason to come up with a subtle rule here (we've survived without it so far), so let's not do option (3), which is the most complicated.

This leaves options (1) and (2). Since we have no strong preference here (based on the current style guide), i say we stick with the decision made in domluna/JuliaFormatter.jl#53 and go with option (1) -- it's the more common pattern in Julia Base, and it's what JuliaFormatter does right now -- so for the sake of the formatter, prefer where {T} to where T.

About why

I'm a fan of the code style presented in this repository. However, as a reader, I'm curious about why certain styles should be adhered to. That way, I can remember it easier and it would be easier for people to agree with the guide.

Would it be possible to add more information about the why in this repo? For example, I switched to using

export f, g

and

export f
export g

instead of

export f,
    g

But am curious as to why these styles are preferred.

Add "Style Guide Blue" badge

We can use the badge [![code style blue](https://img.shields.io/badge/code%20style-blue-4495d1.svg)](https://github.com/invenia/Blue) style

And recommend that people using this style guide use this badge on their projects :)

(Also explicitly acknowledge black somewhere?)

Add guidelines on multiple assignments in a single line

Sometimes one might want to do multiple assignments in a single line to reduce number of lines of code, e.g.

Pmin, Pmax = get_pmin(fnm.system), get_pmax(fnm.system)

If this is considered bad style by us, we should probably add a comment about this in the docs.

Relax opinion on use of `;` in NamedTuples?

Current guidnace is "NamedTuples should not be prefixed with ; at the start" (related to #22)

As the current examples show this leads to these three cases having different style:

xy = (x=1, y=2)
x1 = (x=1,)  # Trailing comma required for correctness.
xs = (; kwargs...)  # Semicolon required to splat correctly.

I can't help but feel that this is a simpler rule:

xy = (; x=1, y=2)
x1 = (; x=1,) 
xs = (; kwargs...) 

Also, I write a lot of code in codebases that follow BlueStyle and I don't recall anyone ever corrected me on adding/removing a ;, so i wonder how much this guidance is even followed right now.

(related to #7, domluna/JuliaFormatter.jl#283 (comment))

Guidelines for nested functions

Just wanted to open up a chat about if functions should be nested, and if so where should they live?

function _bar(v::String)
  return string(v, v)
end

function foo()
  var = "Test"

  return _bar(var)
end

OR

function foo()
  function _bar(v::String)
    return string(v, v)
  end

  var = "Test"
  return _bar(var)
end

OR

function foo()
  var = "Test"

  function _bar(v::String)
    return string(v, v)
  end

  return _bar(var)
end

multi-line ternary operator

Blue:

Ternary operators (?:) should generally only consume a single line.

While reviewing TypeDBClient.jl code, I have code like this:

    owner_req = thing_type === nothing ?
        ThingRequestBuilder.attribute_get_owners_req(iid) :
        ThingRequestBuilder.attribute_get_owners_req(iid, proto(thing_type))

Is it preferred to write it in the long if-condition form?

    owner_req = if  thing_type === nothing
        ThingRequestBuilder.attribute_get_owners_req(iid) 
    else
        ThingRequestBuilder.attribute_get_owners_req(iid, proto(thing_type))
    end

A typo in vim setting?

Thank you for great documents!!

I think this is a typo in vim setting:

setlocal colorcolumn+=1

It should be:

setlocal colorcolumn=+1

Guidelines on `export`

The BlueStyle conventions are great!

I was wondering if people would be interested in discussing adding some guidelines on when to use export and how that relates to the current section on using using and import. Additionally, it would be nice to clarify if how this guideline on module imports changes if at all in various contexts (REPL, scripts, packages, etc).

Space after `,`?

I can't actually find the bit that says to do:
foo(a, b) not foo(a,b)

Nor for Union{A, B} not Union{A,B}

Maybe i am just missing it.

Simplify some current guidance to "have a blank line between multi-line blocks"?

Noticed when writing up domluna/JuliaFormatter.jl#283 (see domluna/JuliaFormatter.jl#283 (comment), related to #7), but better to discuss here :)

We currently have two rules:

  • Use blank-lines to separate different multi-line blocks.
  • Use line breaks between control flow statements and returns.

i wasn't sure how to translate the first one into a rule for a Formatter to follow, unless it just mean "have a blank line after end". If it can be translated to that, then the second one is just a special-case (assuming "control flow" here means only while.. end and if... end statements, which it seems to be, based on the example given).

I suggest we simplify these two rules in one. Below are some options for the new rule:


Option 1:

  • Add a blank line after end in multi-line blocks
# Yes:
if foo
    println("Hi")
end

for i in 1:10
    println(i)
end

# No:
if foo
    println("Hi")
end
for i in 1:10
    println(i)
end
# Yes:
function foo(bar; verbose=false)
    if verbose
        println("baz")
    end

    return bar
end

# No:
function foo(bar; verbose=false)
    if verbose
        println("baz")
    end
    return bar
end
  • Note that in this second example, the No case is currently listed as OK.

Option 2

  • Add a blank line between multiple multi-line blocks
# Yes:
if foo
    println("Hi")
end

for i in 1:10
    println(i)
end

# No:
if foo
    println("Hi")
end
for i in 1:10
    println(i)
end
  • Note we now longer give an opinion on the second example above (the blank line after end / before return)

Option 3

  • Stop giving guidance on this

For what it is worth, i'd happily go with Option 3. In practice, i'm happy enough writing code that follows Option 2, but Option 1 seems a little too fussy to me.

Soften guidelines for `return`

When using long-form functions always use the return keyword

We may want adjust the the hard stance on using return with long-form functions. Some examples that adding in the return makes things worse:

function load!(tz_source::TZSource, file::AbstractString)
    open(file, "r") do io
        load!(tz_source, file, io)
    end
end

# VS

function load!(tz_source::TZSource, file::AbstractString)
    return open(file, "r") do io
        load!(tz_source, file, io)
    end
end
function isarchive(path)
    @static if Sys.iswindows()
        success(`$exe7z t $path -y`)
    else
        success(`tar tf $path`)
    end
end

# VS

function isarchive(path)
    return @static if Sys.iswindows()
        success(`$exe7z t $path -y`)
    else
        success(`tar tf $path`)
    end
end
function VariableTimeZone(name::AbstractString, args...)
    new(name, args...)
end

# VS

function VariableTimeZone(name::AbstractString, args...)
    return new(name, args...)
end
function extract(archive, directory, files=AbstractString[]; verbose::Bool=false)
    ...

    run(cmd)
end

# VS

function extract(archive, directory, files=AbstractString[]; verbose::Bool=false)
    ...

    return run(cmd)  # Note: We don't actually want to return anything from this function
end

I would suggest we re-word to something like:

When using long-form functions prefer the use the return keyword especially when control flow constructs are used. Avoid the use of return when your function should not return anything.

Do we prefer no whitespace in type declarations?

i.e. do we these changes make the style better or worse?

-const CoVector = Union{Adjoint{<:Any, <:AbstractVector}, Transpose{<:Any, <:AbstractVector}}
+const CoVector = Union{Adjoint{<:Any,<:AbstractVector},Transpose{<:Any,<:AbstractVector}}

-struct NamedDimsStyle{S <: BroadcastStyle} <: AbstractArrayStyle{Any} end
+struct NamedDimsStyle{S<:BroadcastStyle} <: AbstractArrayStyle{Any} end

-function Base.BroadcastStyle(::Type{<:NamedDimsArray{L, T, N, A}}) where {L, T, N, A}
+function Base.BroadcastStyle(::Type{<:NamedDimsArray{L,T,N,A}}) where {L,T,N,A}

It'd be good to decide what the formatter should do in these cases (#7, domluna/JuliaFormatter.jl#283)

Style for (sub)modules?

e.g. don't indent inside `module ... end"?

# Yes
module Foo

export bar

bar() = 1

end

# No
module Foo

    export bar

    bar() = 1

end

Perhaps other rules? e.g. Blank line after module and before end?

# Yes
module Foo

export bar
bar() = 1

end

# No
module Foo
export bar
bar() = 1
end

it'd also be kind nice to add a comment after end e.g. end # module?

Raised by @iamed2 in JuliaDatabases/LibPQ.jl#197 (comment)

Remove or fix triple quote rule that isn't about style

Triple-quotes use the indentation of the lowest indented line (excluding the opening triple-quote). This means the closing triple-quote should be aligned to the least indented line in the string. Triple-backticks should also follow this style even though the indentation does not matter for them.

# Yes:
str = """
   hello
   world!
   """
# No:
str = """
   hello
   world!
"""

This is about style, and i think it should be removed. Consider:

greeting1 = """
    hello
    world!
    """

greeting2 = """
        hello
        world!
    """

Which of these follow the style rule? it might be tempting to say greeting1 does, since the least indented line and the (closing) triple-quotes are eqally indented (at one indent / 4 spaces). BUT greeting1 and greeting2 are not equivalent, so there's no syntactic "style choice" to be made between them, since they are semantically different:

julia> greeting1 == greeting2
false

julia> print(greeting1)
hello
world!

julia> print(greeting2)
    hello
    world!

We can only make style choices between semantically equal code. So the only choice to be made is between

greeting1 = """
    hello
    world!
    """
greeting3 = """
hello
world!
"""

julia> greeting1 == greeting3
true

"use the indentation of the lowest indented line" is not a style guide. It is an explanation of how triple-quoted string work;
quoting the manual:

triple-quoted strings are also dedented to the level of the least-indented line

So both the current text and current example should be removed, since they're not about style.

That said, we could change this to be a statement about which to prefer between greeting1 and greeting3.
Am i right in saying the greeting1 is preferred?

i.e. the rule would be something like

Triple-quotes and triple-backticks written over multiple lines should have the closing quotes indented once (excluding docstrings).

# Yes:
str = """
   hello
       world!
   """
# No:
str = """
hello
   world!
"""

for what it's worth JuliaFormatter currently does not have an opinion on this, so for domluna/JuliaFormatter.jl#283 we could add this a rule (i.e. so greeting3 would be "fixed up" to be like greeting1).

julia> using JuliaFormatter  

julia> str = """
           greeting1 = \"\"\"
               hello
               world!
               \"\"\"
           """;

julia> print(format_text(str))  
greeting1 = """
    hello
    world!
    """

julia> str = """
           greeting3 = \"\"\"
           hello
           world!
           \"\"\"
           """;

julia> print(format_text(str))  
greeting3 = """
hello
world!
"""

(came up in domluna/JuliaFormatter.jl#283, related to #7)

Multi-line comments use `#` or `#=`?

Looking at the guide I came across this example:

# Yes:

# Number of nodes to predict. Again, an issue with the workflow order. Should be updated
# after data is fetched.
p = 1

# No:

p = 1  # Number of nodes to predict. Again, an issue with the workflow order. Should be
# updated after data is fetched.

What are peoples thoughts on this style:

#=
Number of nodes to predict. Again, an issue with the workflow order. Should be
updated after data is fetched. 
=#
p = 1

Documentation for Internal Functions

Documentation is nice to define inputs/outputs and what a function does. Extending this out to internal functions would be nice as well, especially when coming back to revise the function. However, I don't think it's necessary to outline a full docstr for these internal functions.

Any thoughts on this? Suggestions for a format? Maybe something like,

function _foobar(x, y; z)
    """
    Explanation of what foobar does, maybe explain why it does things in a certain way?
    Have docstr inside the function for quick glance value that a function is internal?
    """
end

Exempting literal strings from line limits?

It is not uncommon to want verbose logging messages (in the internal Invenia codebase, and Production systems more generally).

If there are multiple log statements, it is easy to end up with 5-10 lines dedicated some form of logging messages. But once a function is over ~30 lines it becomes hard to read, just because of the amount of vertical space used. So for log messages, the horizontal space limit puts notable pressure on the vertical space.

One option to address this would be to exempt literal strings from line limits, then codebases can choose to allow log messages that are greater than 92 chars, without incurring lots of vertical space.
e.g.
to allow

function do_thing(status)
    is_good(status) || warn(LOGGER, "Whoa there. Things aren't all good. Doing the thing may lead to some seriously weird stuff happening.")
    return the_thing()
end

rather than things like this:

function do_thing(status)
    is_good(status) || warn(
        LOGGER,
        "Whoa there. Things aren't all good. " *
        "Doing the thing may lead to some seriously weird stuff happening."
    )
    return the_thing()
end
function do_thing(status)
    if !is_good(status)
        warn(
            LOGGER,
            string(
                "Whoa there. Things aren't all good. ",
                "Doing the thing may lead to some seriously weird stuff happening."
            )
        )
    end
    return the_thing()
end

Indicate where exports are defined

The style guide states that exports should be grouped by theme. For example, we could have something like

include("generate")
include("showcode")

export code_block, output_block
export @sc, sc, CodeAndFunction, @sco, sco, scob

Although I don't have big problems with this approach. I do wonder if it could be made more clear to the reader of code what the "themes" here are.

I was thinking that, maybe, it would be an idea to do

include("generate")
export code_block, output_block
include("showcode")
export @sc, sc, CodeAndFunction, @sco, sco, scob

Now, it is super clear what the theme is. For situations where a function is defined in multiple files, that is, multiple methods for the function are defined, the style can fallback to placing the export below all the includes with a white line in between.

The benefits of this approach would be that it's easy to locate where exports are defined, even when browsing code via the GitHub website.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.