Lesson 46 of 46 · Reference

Best practices

A program that compiles is only the start. This lesson is the checklist to run before you write code, and again before you call it done. The same rules are given to the Salam AI assistant, so what it writes for you follows them too.

1. The standard library first

Before you write a helper, find out whether the standard library already has it. A hand-written number parser, splitter, formatter or hash is nearly always worse than the library version: longer, slower, and silent about the cases the library reports as errors.

Take a Python function that turns hex text into a number:

def hex_to_decimal(hex):
    return int(hex, 16)

It is one line in Python because Python has the parser built in. Salam has it too, in conv. The port is one line as well, and a forty line loop over the digits would be the wrong answer:

bp-std-first.salam
import conv

func main:
    println conv.ParseHexOr("FF", 0 - 1), conv.ParseHexOr("0x10", 0 - 1)
    println conv.ParseHexOr("12zz", 0 - 1)
    try:
        println conv.ParseHex("1a2b")
        println conv.ParseHex("xyz")
    catch e:
        println "not a hex number:", e.message
    end
end
Output255 16 -1 6699 not a hex number: invalid character 'x'

conv.ParseHex accepts upper and lower case digits and a leading 0x, and it fails on bad input, exactly like int(s, 16) raising ValueError. conv.ParseHexOr returns a default of your choice instead of failing. A hand-made loop that stops at the first bad character would turn 12zz into 18 without a word.

How to look something up:

  • Search the library reference for a word describing the job: hex, split, sha256, base64, trim.
  • Read the signature and the example on the function's page. Most functions come in a throwing form and an ...Or form with a fallback.
  • Only when nothing fits, write the function yourself, keep it small, and put it in its own file so the next person finds it.
You would writeUse instead
a loop that turns text into a numberconv.ParseInt, conv.ParseFloat, conv.ParseHex
number to text with padding or decimalsconv.FormatInt, conv.FormatHex, conv.FormatFloatPrec
splitting, trimming, joining, replacing textthe str methods and the str package
reading or writing JSONjson
pattern matching in textregex
hashes, random bytes, base64crypto, encoding
sorting and keyed lookupsort, Vector, HashMap

2. Model the problem with types

Decide what the pieces of the program are before you write the code that connects them. Salam gives each kind of piece its own tool:

  • A thing with several related fields is a struct. Give it methods for what it can do, a static func as constructor, and keep fields private unless callers need them.
  • A choice from a fixed list (a state, a kind, a command) is an enum, taken apart with match, which makes the compiler check that every case is handled. Do not pass magic numbers or strings around.
  • Behaviour that several types share is an interface. Use a bound such as <T: Shape> when the type is known while compiling, and dyn Shape for a collection of mixed types.
  • Code that does not care about the type is generic: write larger<T> once instead of one copy per type.
bp-interface.salam
interface Shape:
    func area(): f64
end

struct Square:
    pub side: f64 = 0.0
    pub func area(): f64: ret this.side * this.side end
end

struct Circle:
    pub radius: f64 = 0.0
    pub func area(): f64: ret 3.0 * this.radius * this.radius end
end

func larger<T>(a: T, b: T): T:
    ret a > b ? a : b
end

func total(shapes: Vector<dyn Shape>): f64:
    mut sum := 0.0
    each s in shapes:
        sum += s.area()
    end
    ret sum
end

func main:
    println larger(3, 8), larger("pear", "apple")
    shapes := Vector {} as Vector<dyn Shape>
    shapes.push(Square { side = 2.0 })
    shapes.push(Circle { radius = 1.0 })
    println total(shapes)
    each s in shapes:
        s.free()
    end
    shapes.free()
end
Output8 pear 7

Failure belongs in the type of the function too. A function that can fail is marked throws and fails with throw; the caller either passes the error on with try or handles it in a try: and catch block. Do not return -1 or an empty string to mean "it did not work": the caller will forget to check it. Put cleanup next to what it cleans with defer, and give numbers that mean something a const name.

3. Small functions

A function does one job and its name says which. If you want to write a comment above a block to explain it, turn the block into a function with that name. About thirty lines is a good ceiling, and the fewer parameters the better. Helpers that are not part of the API stay private (no pub) and sit above the first pub func, which is also what the compiler requires.

This program turns a colour such as #ff8800 into a value. Every function is short, pair leaves the hex work to conv, the failure is a throw, Channel is an enum, and main only prints:

bp-design.salam
import conv

enum Channel: Red, Green, Blue end

struct Rgb:
    pub red: int = 0
    pub green: int = 0
    pub blue: int = 0

    pub func get(c: Channel): int:
        ret match c:
            Red: this.red end
            Green: this.green end
            Blue: this.blue end
        end
    end

    pub func to_str(): str:
        ret "rgb(" + this.red + ", " + this.green + ", " + this.blue + ")"
    end
end

func pair(text: str, from: int): int throws:
    ret (try conv.ParseHex(text.substr(from, 2))) as int
end

func parse_colour(text: str): Rgb throws:
    digits := text.starts_with("#") ? text.substr(1, text.len() - 1) : text
    if digits.len() != 6:
        throw Error.New(ErrorKind.InvalidArgument, "expected 6 hex digits, got " + digits)
    end
    ret Rgb { red = try pair(digits, 0), green = try pair(digits, 2), blue = try pair(digits, 4) }
end

func brightest(c: Rgb): Channel:
    mut best := Channel.Red
    each ch in [Channel.Green, Channel.Blue]:
        if c.get(ch) > c.get(best):
            best = ch
        end
    end
    ret best
end

func describe(text: str): str:
    try:
        c := try parse_colour(text)
        ret c.to_str() + ", strongest " + brightest(c).name()
    catch e:
        ret "invalid colour: " + e.message
    end
end

func main:
    println describe("#ff8800")
    println describe("0a141e")
    println describe("#12")
    println describe("#gg0000")
end
Outputrgb(255, 136, 0), strongest Red rgb(10, 20, 30), strongest Blue invalid colour: expected 6 hex digits, got 12 invalid colour: invalid character 'g'

4. Split it into files

Unless you are writing a short script, do not put a program in one file. Keep main.salam tiny, so that it only wires the parts together, and give each concern its own file or folder. Each file then has one reason to change, and a reader can find things by name:

colourtool/
  main.salam         reads the command line, calls the rest
  salam.toml         name, version, dependencies
  model.salam        Rgb, Channel
  parse.salam        parse_colour and its helpers
  parse_test.salam   test_ functions for parse.salam

Packages shows how files include each other and how a folder becomes a package. salam new name creates the starting layout. Tests sit next to the code in files ending in _test.salam and run with salam test.

5. Write Salam, not a translation

When you port code from another language, keep what it does and change how it says it:

  • classes become structs with methods, exceptions become throws, dictionaries become HashMap, lists become Vector (and call .free() on them);
  • use each and repeat rather than counting indexes by hand, and match rather than a long chain of if on one enum;
  • let the compiler infer types with :=, and use mut only for what changes;
  • remember that until means while (see loops).

6. Name things in the language of the file

A Salam file is written in one language, and its names should be too. In a Persian program give your functions, variables, parameters, struct fields, enum members and constants Persian names; in an English program use English ones. Do not mix the two in one file or one name, and do not spell Persian words with Latin letters (rang, khandan). Single letters such as i and x, well-known acronyms such as URL and JSON, test_ function names and the C names in an extern: block are fine in either language. Strings shown to people follow your audience, not the file.

How a name with several words is written depends on the language. In English a name never contains whitespace: use snake_case for functions, variables, parameters and fields, and PascalCase for types and the pub API, as the standard library does (parse_colour, red_value, Rgb). In Persian the standard is to separate the words with spaces, and the compiler accepts that for functions, methods, parameters, struct fields, enum members and variables, such as خواندن رنگ or جمع کل. Do not use underscores in Persian names. The grammar takes a single word for the name of a struct, enum or interface, for a constant, for a loop variable, for the catch variable and for lambda and extern: parameters; there join the words with a half-space (رنگ‌آمیزی), which the compiler treats exactly like a space. And no word of a multi-word name may be a keyword: بیشینه از is a fine name, بزرگتر از is not, because بزرگتر means gt.

The standard library already has a Persian name for everything, so a Persian program calls تبدیل.تجزیه شانزده and .طول(). salam translate converts keywords and library names but not the names you invented, so after translating a file, rename those by hand. The examples in this lesson do exactly that: compare parse_colour with its Persian twin خواندن رنگ on the Persian page.

7. Check your work

Run these after every change. They are quick, and the compiler is strict on purpose (see the compiler's rules):

  • salam check app.salam finds errors without building;
  • salam format app.salam puts the code in the standard layout;
  • salam test runs the test_ functions;
  • salam run app.salam builds and runs the program.

One file is fine when the task is small or you were asked for it. The first rule still applies: even a ten line script should call conv.ParseHex rather than reinvent it.