Language guide

Everything about writing Fubby, from the first line to classes. Every sample here was run against Fubby 0.1.2, and the output shown is what it printed.

Programs and comments

A program is a set of functions, classes and interfaces in a UTF-8 file ending in .fab. It starts at main, which takes no parameters and returns nothing. Semicolons are optional, and indentation has no meaning.

// A line comment
/* A block comment */

/// A doc comment. `fubby shady docs` includes these.
func main() {
    print("one"); print("two")
    print("three", 4, true)
}
one
two
three 4 true

Values and operators

TypeExamplesNotes
Int42, -764-bit. Overflow is an error, and never wraps around.
Float3.1464-bit floating point. Not exact, so avoid it for money.
Booltrue, falseConditions must be Bool. if 1 { } is an error.
String"hello\n"UTF-8. Escapes are \n, \t, \" and \\.
List[1, "two"]Can mix types, and is shared when passed around.
Map{"a": 1}Keys are always strings. Also shared when passed around.
nullnullNo value. Only allowed where the type permits it.
func main() {
    print(7 / 2, 7.0 / 2, 7 % 3)
    print(2 + 3 * 4, (2 + 3) * 4)
    print("total: " + 5)
    print(1 < 2 && !false)
    print(1 == 1.0, "a" != "b")
}
3 3.5 1
14 20
total: 5
true
true true

Dividing two Int values gives an Int. && and || stop as soon as the answer is known. + joins text with any value except null. Comparing with == compares lists and maps by their contents, and objects by identity.

The operators, from highest precedence to lowest, are: unary ! -, then * / %, + -, < <= > >=, == !=, &&, ||, and finally the range ...

Variables and types

let declares a variable, and const declares one you can't reassign. A type is optional, and goes before the name. With a type, the variable can only ever hold that type.

func main() {
    let count = 1
    let String title = "Notes"
    const Int limit = 10
    count += 1
    count *= 5
    print(count, title, limit)

    let Float ratio = 2
    print(ratio / 4)
}
10 Notes 10
0.5
Type nameAccepts
Int, Bool, StringExactly that type
FloatA float, or an integer, which it converts
List, MapA list or a map
AnyEvery value, including null
DogThat class, a subclass, or a class implementing that interface
String?That type, or null

Scope

A variable lives until the end of the block that declares it. You can reuse a name in a separate block, but you can't declare a name that's still visible. This prevents a common kind of bug.

func main() {
    if true {
        let message = "inside"
        print(message)
    }
    print(message)
}
fubby: app.fab:6:5: check error: Undefined variable: message
1 check error(s)

Null and optional types

A type can hold null only if it ends in ?. A plain String can never be null, so you can trust it.

func find(String key) -> String? {
    if key == "name" {
        return "Fubby"
    }
    return null
}

func main() {
    let String? found = find("age")
    if found == null {
        print("not found")
    }
    print(find("name"))
    print(found)
}
not found
Fubby
null

Using null with + is an error, so missing data can't silently turn into blank text. JSON null and SQL NULL both become Fubby's null.

func main() {
    let String name = null
}
fubby: app.fab:2:5: check error: Type mismatch: name is String, but the value is null
1 check error(s)

Control flow

if, while and for

Ranges include both ends, and count down if the first number is larger. A for over a list walks a snapshot, so changing the list inside the loop is safe.

func main() {
    let n = 2
    if n == 1 {
        print("one")
    } else if n == 2 {
        print("two")
    } else {
        print("many")
    }

    let i = 0
    while i < 2 {
        print("while", i)
        i += 1
    }

    for x in 1..3 {
        print(x)
    }
    for x in 3..1 {
        print(x)
    }
    for color in ["red", "blue"] {
        print(color)
    }
}
two
while 0
while 1
1
2
3
3
2
1
red
blue

break and continue

break leaves the nearest loop, and continue skips to its next pass. Using either outside a loop is an error.

func main() {
    for i in 1..10 {
        if i == 2 {
            continue
        }
        if i == 4 {
            break
        }
        print(i)
    }
}
1
3

match

match runs the first arm with an equal value, then finishes. Arms never fall through, so there's no break to forget. else is optional and must come last. Fubby has no switch.

func kind(Int day) -> String {
    match day {
        case 6, 7 {
            return "weekend"
        }
        else {
            return "weekday"
        }
    }
}

func main() {
    print(kind(7))
    print(kind(2))
    match "red" {
        case "red" { print("stop") }
        case "green" { print("go") }
    }
}
weekend
weekday
stop

Functions

Every parameter needs a type, written before its name. A return type comes after ->. With no return type, the function returns nothing. A function with a return type must return on every path, and the checker enforces that. Calls can nest up to 1000 levels.

func factorial(Int n) -> Int {
    if n <= 1 {
        return 1
    }
    return n * factorial(n - 1)
}

func average(List scores) -> Float {
    let total = 0
    for score in scores {
        total += score
    }
    return total / 1.0 / len(scores)
}

func greet(String name, String? title) {
    if title == null {
        print("Hello " + name)
    } else {
        print("Hello " + title + " " + name)
    }
}

func main() {
    print(factorial(5))
    print(average([70, 85]))
    greet("Asha", null)
    greet("Rao", "Dr")
}
120
77.5
Hello Asha
Hello Dr Rao

Functions can't be stored in variables or passed as values yet. Where a function must be named, such as a Snow route or a Bela task, you pass its name as a string.

Lists and maps

Indexes start at 0. An index outside the list, or a missing map key, is a runtime error you can catch. See the library reference for the full set of helpers.

func main() {
    let numbers = [10, 20, 30]
    print(numbers[1], len(numbers))
    numbers[0] = 15
    numbers[1] += 1
    numbers.push(40)
    print(numbers.pop(), numbers)

    let profile = {"name": "Fubby", "version": 1}
    print(profile["name"], profile.has("email"))
    profile["status"] = "preview"
    profile.set("version", 2)
    print(profile.remove("status"), profile)
    print(map_keys(profile))

    print(list_contains(numbers, 30), list_reverse(numbers))
    print(list_remove(numbers, 0), numbers)
}
20 3
40 [15, 21, 30]
Fubby false
preview {name: Fubby, version: 2}
[name, version]
true [30, 21, 15]
[21, 30] [15, 21, 30]

push, pop, set and remove change the collection itself. The list_ helpers return a new list, and leave the original alone.

Classes and objects

A class has typed fields, an optional constructor called init, and methods. You create an object by calling the class name. Members are private unless you mark them public or protected.

class BankAccount {
    private String owner
    private Float balance

    init(String owner, Float opening) {
        this.owner = owner
        this.balance = opening
    }

    public func deposit(Float amount) {
        if amount <= 0 {
            throw "deposit must be positive"
        }
        this.balance += amount
    }

    public func summary() -> String {
        return this.owner + ": " + this.balance
    }
}

func main() {
    let account = BankAccount("Asha", 100.0)
    account.deposit(25.5)
    print(account.summary())
}
Asha: 125.5

Field starting values

Field typeStarts as
Int, Float0
Boolfalse
String""
List, MapA new empty collection
Any, or anything ending in ?null
Another class, such as DogMust be set in the constructor, or creating the object fails
class Dog {
    public String name

    init(String name) {
        this.name = name
    }
}

class Owner {
    public Dog pet
    public Dog? spare
    public List toys

    init(Dog pet) {
        this.pet = pet
    }
}

func main() {
    let owner = Owner(Dog("Rex"))
    owner.toys.push("ball")
    print(owner.pet.name, owner.spare, owner.toys)
}
Rex null [ball]

Inheritance

A class can extend one parent. Replacing a parent's method requires override. super.method() calls the parent's version. A child's constructor doesn't run the parent's automatically, so it sets inherited fields itself, which needs them to be protected or public.

class Animal {
    protected String name

    init(String name) {
        this.name = name
    }

    public func sound() -> String {
        return "..."
    }

    public func speak() -> String {
        return this.name + " says " + this.sound()
    }
}

class Dog extends Animal {
    init(String name) {
        this.name = name
    }

    public override func sound() -> String {
        return "woof"
    }

    public func both() -> String {
        return super.sound() + this.sound()
    }
}

func main() {
    let dog = Dog("Rex")
    print(dog.speak())
    print(dog.both())
}
Rex says woof
...woof

Interfaces and abstract classes

An interface lists method signatures, and any class that implements it must provide them exactly. Interfaces work as types. An abstract class can't be created directly, only extended.

interface Shape {
    func area() -> Int
}

abstract class Base {
    public func label() -> String {
        return "shape"
    }
}

class Square extends Base implements Shape {
    private Int side

    init(Int side) {
        this.side = side
    }

    public func area() -> Int {
        return this.side * this.side
    }
}

func describe(Shape shape) {
    print(shape.area())
}

func main() {
    let square = Square(3)
    describe(square)
    print(square.label())
}
9
shape

Errors

throw raises an error, and try/catch handles it. catch catches every runtime error, including ones from Fubby itself such as dividing by zero. The caught value is a String.

func main() {
    try {
        throw "out of stock"
    } catch (error) {
        print(error)
    }

    let zero = 0
    try {
        print(1 / zero)
    } catch (error) {
        print(error_contains(error, "zero"))
    }
}
Fubby throw: out of stock
true

An error nobody catches stops the program, and names the file, line and column:

func main() {
    let zero = 0
    print(1 / zero)
}
fubby: app.fab:3:5: runtime error: Division by zero

Imports

import loads another .fab file, relative to the file importing it. Declarations from every imported file share one namespace, so names must be unique across them. Importing the same file twice is rejected, even through different paths, and so are import cycles.

helpers.fab
func greeting() -> String {
    return "Hello from a module"
}
main.fab
import "helpers.fab"

func main() {
    print(greeting())
}
Hello from a module

Packages use import "package:name/src/lib.fab". See Smokey.

The checker

fubby check looks for mistakes without running anything. fubby run, fubby snow serve and fubby shady test apply the same checks first, so a bad line is found even if it would rarely run.

It findsExample
Undefined names and typesprint(totl), let Banana b = 1
Wrong number of argumentsstr_upper()
Certain type mistakeslet Int n = "x", if 1 { }
Unknown or private membersaccount.balance from outside the class
Missing returnsA function declared -> Int that can finish without returning
DuplicatesTwo functions, classes, parameters or match cases with the same name or value
Name clashesA function named like a built-in, such as print
Assigning to a constantconst c = 1 followed by c = 2

The checker only reports mistakes it's sure about. When a type can't be known, such as a value from Any or a JSON file, it accepts the code, and the same rules are enforced while the program runs.

func id(Any value) -> Any {
    return value
}

func main() {
    let Int n = id("text")
}
fubby: app.fab:6:5: runtime error: Type mismatch: expected Int, got text

What the language doesn't have yet