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:
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
endconv.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
...Orform 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 write | Use instead |
|---|---|
| a loop that turns text into a number | conv.ParseInt, conv.ParseFloat, conv.ParseHex |
| number to text with padding or decimals | conv.FormatInt, conv.FormatHex, conv.FormatFloatPrec |
| splitting, trimming, joining, replacing text | the str methods and the str package |
| reading or writing JSON | json |
| pattern matching in text | regex |
| hashes, random bytes, base64 | crypto, encoding |
| sorting and keyed lookup | sort, 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 funcas 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, anddyn Shapefor 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.
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()
endFailure 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:
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")
end4. 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 becomeHashMap, lists becomeVector(and call.free()on them); - use
eachandrepeatrather than counting indexes by hand, andmatchrather than a long chain ofifon one enum; - let the compiler infer types with
:=, and usemutonly for what changes; - remember that
untilmeans 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.salamfinds errors without building;salam format app.salamputs the code in the standard layout;salam testruns thetest_functions;salam run app.salambuilds 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.