Lesson 18 of 25 · Larger programs

Packages and files

Larger programs are written across several files. Each file belongs to a package, and a package shares with the rest of the program only what it marks pub.

Packages

A file that begins with package name belongs to that package. Only the functions, constants, types and fields marked pub can be used from outside it; everything else is an internal detail that you are free to change later without breaking anyone. A file without a package line is in the package main.

pkg-lib.salam
package tools

pub const VERSION := "1.0"

func double(n: int): int:
    ret n * 2
end

pub func quadruple(n: int): int:
    ret double(double(n))
end

This file has no main of its own, so on its own it does not run; it is meant to be included by a program, as in the next section.

Including your own files

include name "file.salam" brings in a file of your own project under a short name, and you reach its public members as name.thing. The path is relative to the file that does the including.

pkg-include.salam
include tools "pkg-lib.salam"

func main:
    println tools.quadruple(5)
    println "tools version:", tools.VERSION
end
Output20 tools version: 1.0
  • The name is optional: include "lib/money.salam" is reached as money.Format(...), after the file name. Give it a name whenever the file name is long or unclear.
  • A path that starts with @/ starts at the project root instead of at the current file, so include "@/lib/money.salam" means the same file from every folder of the project. Build from the project's folder so the root is where you expect it.
  • Do not start a path with ./. The compiler stops with E008: include path './lib/money.salam' must not start with './' and tells you the two spellings it does accept.
  • A file that cannot be found is an error too (E008: included file not found), never something skipped quietly.

Importing a library

Standard library packages are imported by name, without quotes: import str, import os. A package inside another one is written with a dot, as in import encoding.json, and is used through its last part, json.Get(...). In short: import is for libraries and include is for your own files.

pkg-import.salam
import str
import math

func main:
    text := "  hello world  "
    println str.Trim(text)
    println str.Contains(text, "world")
    println math.Sqrt(81.0), math.Pow(2.0, 10.0)
end
Outputhello world true 9 1024
pkg-dotted.salam
import encoding.json

func main:
    order := `{"id": 42, "item": "notebook", "qty": 3}`
    if not json.Valid(order):
        println "not valid JSON"
        ret 1
    end
    println "order", json.GetInt(order, "id")
    println json.GetInt(order, "qty"), "x", json.Get(order, "item")
    println json.Has(order, "coupon")
end
Outputorder 42 3 x notebook false

An import nobody uses is an error (E082: unused import), so the imports at the top of a file always tell the truth about what the file depends on.

pub and private

Privacy works on struct members too. In this bank package, owner is public but balance is not, so the only way to change a balance is through Deposit and Withdraw, which check every amount. The helper valid_amount is private as well. Account.Open is a static constructor, the usual way to hand out a ready-made value of a type whose fields are private.

pkg-bank.salam
package bank

pub const MAX_DEPOSIT := 1000000

struct Account:
    pub owner: str = ""
    balance: int = 0

    pub static func Open(owner: str): Account:
        ret Account { owner = owner }
    end

    pub func Balance(): int:
        ret this.balance
    end

    pub mut func Deposit(amount: int): bool:
        if not valid_amount(amount) or amount > MAX_DEPOSIT:
            ret false
        end
        this.balance += amount
        ret true
    end

    pub mut func Withdraw(amount: int): bool:
        if not valid_amount(amount) or amount > this.balance:
            ret false
        end
        this.balance -= amount
        ret true
    end
end

func valid_amount(amount: int): bool:
    ret amount > 0
end
pkg-private.salam
include bank "pkg-bank.salam"

func main:
    mut acc := bank.Account.Open("Sara")
    println acc.Deposit(500), acc.Withdraw(800), acc.Withdraw(120)
    println acc.owner, acc.Balance()
    println acc.Deposit(bank.MAX_DEPOSIT + 1)
end
Outputtrue false true Sara 380 false

From the program, acc.balance would be refused with E017: field 'balance' is private in struct 'Account' (mark it 'pub'), and calling the private helper with E017: 'valid_amount' is not exported by package 'bank' (mark it 'pub'). Make something pub because other code is meant to use it, not to silence an error: everything public is a promise you have to keep.

A package across a folder

When a package grows, split it across several files in one folder. Name the folder after the package and give it a file of the same name: shop/shop.salam. Including that file brings in every other .salam file in the folder as part of the same package.

FileWhat it holds
pkg-folder.salamThe program; it includes shop/shop.salam
shop/shop.salampackage shop, import str, the private price_label and the public Welcome
shop/cart.salampackage shop and the public Total
pkg-folder.salam
include shop "shop/shop.salam"

func main:
    println shop.Welcome("Sara")
    println shop.Total([1250, 399, 205])
end
OutputWelcome back, Sara 3 items, 18.54

The files of one package share everything: cart.salam calls the private price_label from shop.salam, and uses str without importing it, because the package already imports it once.

Names in two languages

In an English file you write the English names, and a Persian one such as str.طول is an error; a Persian file needs the Persian names. Every standard library function carries both: its Persian name is written next to it in the library source with @fa. Give your own public functions both names the same way, and Persian files can call them too:

pkg-bilingual.salam
@en "Twice"
@fa "دوبار"
pub func Twice(n: int): int:
    ret n * 2
end

func main:
    println Twice(21)
end
Output42

A Persian program that includes this file calls دوبار(۲۱) and gets the same 42.

The order of definitions

Salam asks for a fixed order of the top-level definitions in every file, so that every file reads the same way, and each step has its own error code:

  1. package
  2. Imports, then includes (an import after an include is E108)
  3. Constants and global variables (E084 and E085 when they come after a function or type)
  4. Types: structs, enums, interfaces and impl blocks (E087 when one comes after a function)
  5. Functions, private ones first and public ones last

salam format tidies a file up and does much of this moving for you.

Laying out a project

salam new shop creates a folder shop with one file, salam.salam, which holds main. Inside that folder a bare salam build builds that file into a program named after the folder. From there a project usually grows like this:

  • salam.salam stays short: it reads arguments, calls into the packages and prints results.
  • Each area of the program gets a folder that is a package: cart/cart.salam, store/store.salam, each split into more files as it grows.
  • Packages include each other with @/ paths, so moving a file does not break the includes inside it.
  • Each package keeps its public surface small and documents it with @en and @fa names.

Every standard library package has a page of its own in the library reference, and the next lesson collects the errors the compiler reports, including the ones above.