Intro
This documentation is structured so that it can be read from top to bottom if you like. Each chapter builds upon what has been introduced previously. We assume the reader is familiar with the overall syntax of Lua and has some basic understanding of programming in general. Any Lua code provided in these examples is handwritten and shouldn’t be assumed as the generated Lua result of Fuse code compilation. They are just there for the comparison and don’t represent the generated Lua code.
Hello World
After installing the Fuse you have 2 options to use it, either using fuse
as your Lua interpreter or compiling your code into pure Lua code with the help of the fusec
command.
So let’s create a file called hello.fuse
, Now let’s write a little hello world program.
print("Hello, World!")
Now go to the directory containing your file and run the following command.
fusec hello.fuse --target 5.1
After running this command you can find a new file beside hello.fuse
called hello.lua
which now you can execute with any Lua 5.1 interpreter.
Alternatively, you can use this command to execute your fuse file directly.
fuse hello.fuse
Congratulations you just wrote and ran your first Fuse code!
Variables
Fuse is a gradually typed language, Historicly languages with strong type systems impose a lot of boilerplate code on the developers. In Fuse, like most modern typed languages you usually don’t need to annotate anything to get the benefits of the type system. This happens thanks to the type inference, The Type of all these variables is determined by their initialization:
let pi = 3.1415
let c = 299792458
let e = 2.7182
let name = "Ada Lovelace"
let pi = 3.14
let do_major = ["do", "re", "mi", "fa", "sol", "la", "si", "do"]
let player = {
health: 70,
score: 248.3,
spells: [
"Fire Breath",
"Ice Blast",
"Ice Wall",
"Master Elements(Passive)",
],
}
All variables in Fuse by default are immutable
, This means that after initializing its value we no longer can change it. But what about when we want to modify a variable? We have 2 ways to acheive this.
One is simply define a new immutable variable with the same name.
let genre = "Jazz"
let genre = genre.lowercase()
This way we would essentially shadow the old variable with a new variable simulating a variable mutation.
But this won’t work in all cases, Sometimes all we need is plain old mutable variables. We can mark a variable as mutable
by adding the mut
keyword in the variable declaration.
let mut sum = 0
for i in 1..100 do
sum += i
end
Types
Even tho we don’t have to explicitly define the types in the examples above, underneath all of them are getting the correct type with type inference. In Fuse, we annotate types after the variable’s name. This style of type annotation lets us push every variable-specific part to the right and keep the left-hand side of our variable definitions cleaner.
Here are some variable definitions with explicit type annotations.
let name: string = "Sam"
let max_health: = 100
let mut score: number | string = 30
score = "Max"
As you can see variables can have one or more types, and variables with more than one type in their definition can be used with multiple value types; But reading their value as a specific type needs runtime checks and casting to prevent undesired behaviors.
let score: number | string = 30
let score_number: number = score -- Error, Won't compile since may result in type error!
let score_number: number = score as number -- Ok, Since we are explicitly casting to a number.
Functions
Fuse keeps the original syntax of the Lua language with one exception functions like all values are defined in the local scope by default.
Here is a function that will take two number
and will return a number
:
function sum(a: number, b: number) -> number
return a + b
end
In addition to that you can also use the fn
keyword instead of the longer version of it.
fn sum(a: number, b: number) -> number
return a + b
end
For functions with a single expression body, you can omit the end
keyword and use the =>
sign to assign the return value of the function.
fn sum(a: number, b: number) -> number => a + b
Conditional Expressions
if num > 0 then
print("Positive")
elseif num < 0 then
print("Negative")
else
print("Zero")
end
In Fuse if
also can be used as an expression.
let max = if a > b then a else b end
For Loop
let fruits = ["apple", "orange", "kiwi", "banana"]
for fruit in fruits do
print(fruit)
end
See For Loop
While Loop
let numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
let mut index = 1
while index < #numbers do
print("Number at index ${index} is ${numbers[index]}")
index += 1
end
Repeat Loop
repeat
works similarly to a while
loop but it will always execute the code block before checking the condition of the loop.
let num = 1
repeat
print("num: ${num}")
num += 1
until num == 10
assert(num == 10)
Infinite Loop
A repeat
block without any condition will act as an infinite loop.
repeat
print("This will print forever!")
end
You can also achieve the same thing with a while
loop like this:
while true do
print("This will print forever!")
end
Match Expression
fn handle_request(req: Request) -> string
match req when
{ status: 200 } then req.body end
{ status } if status >= 400 and status < 500 then "User Error" end
{ status } if status >= 500 and status < 600 then "Server Error" end
{ status: 900 } then "Unknown Error" end
else "Unknown Error" end
end
end
Comments
-- This is a single-line comment
--- This is a documentation comment used to document variables,
--- functions, structs, traits, and libraries. Tools and text editors
--- may treat these comments differently.
Right now Fuse doesn’t support multiline comments. we may add them back in the future but only if we see a real demand for them.
Optional and nil
Fuse itself is a nil/null
safe language, We do not let any nil values be passed around. Instead we can use an Optional
type to represent a value that may be nil
, You may also know Optional
types as Maybe
and/or Option
and other languages.
This function returns a User
if it exists otherwise it would return nothing.
fn get_user(id: number) -> Optional<User>
if user_exists(id) then
Some(fetch_user(id))
else
None
end
end
Note: There is actually a nil
value exists in Fuse for compatibility with Lua, But there is no direct interaction with this value unless we are in an unsafe
block. It will be explored in the Interfacing with Lua.
We can also add a question mark(?
) to the end of our type to represent the same thing.
fn get_user(id: number) -> User? -- result in Optional<User>
if user_exists(id) then
Some(fetch_user(id))
else
None
end
end
When using an Optional
value, We have to first unwrap
the said value.
fn post_login_hooks(data: LoginData)
let option = get_user(data.uid)
if option.is_ok() then
let user = option.unwrap()
print("Hello, ${user.display_name}")
else
print("User Not Found.")
end
end
Or using pattern-matching
let message = match option when
Some(user) then "Hello, ${user.display_name}" end
None then "User Not Found." end
end
print(message)
Imports
To access exposed modules from other libraries use the import
instruction.
-- Importing from our project.
import MyModule from "relative/path/to/module"
import { func } from "relative/path/to/another-module"
-- Importing something from fuse standard library
import io from "@fuse:io"
import { HashMap } from "@fuse:collections"
-- Importing from packages.
import ThirdPartyPackage from "package:third-party-package"
-- Importing libraries from the Lua path.
import name from "@lua:path/to/lib"
Note: As a conversion in Fuse we name files and directories with kebab-case
but there is no restriction imposed on it by the compiler, For modules it is also possible to name them using kebab-case
or snake_case
, Similar to Rust’s cargo
, Fuse would resolve my-module
and my_module
to the same module.
Collections
Lua comes with a really smart design for implementing both objects and arrays all with the same piece of code. By doing so they have made the interpreter insanely small and portable, For a compiled language like fuse this isn’t a goal anymore; After all Fuse will compile into vanilla Lua and won’t need anything other than a viable Lua interpreter to run.
Because of these differences in the workflow of Fuse and Lua, We can introduce additional data structures with Zero Cost Abstraction
. Some of these new data structures are our collections
module which comes with multiple useful tools in addition to the table
, Things such as, Array
, List
, Map
, and Set
among others(see Fuse Standard Library).
import { Array, List, Map, Set } from "@fuse:collections"
let array: Array<number> = [1, 2, 3]
let list: List<number> = [1, 2, 3]
let map: Map<string, number> = { "A": 1, "Blue", 42, "Jay": 9 }
let set: Set<string> = { "Just", "The", "Unique", "Entries" }
Struct
Lua’s table is the living embodiment of “when you only have a hammer everything is a nail”. While this strategy is good keeping the interpreter small won’t help with the code readability.
Fuse comes with an explicit syntax to define structures which will use the tables under the hood but will make the definition and implementation much more contained.
Here is a simple struct
.
struct Book
name: string
author: string
pages: number
end
impl Book
pub fn new(name: string, author: string, pages: number) -> Self
return Self { name, author, pages }
end
end
Trait
In Fuse we value composition
over inheritance
, One of the most common tools for providing language-level composition support is trait
s.
Traits are used to share code between structs. They are in concept similar to interface
s. Structs and Tables can implement traits; Traits cannot be instantiated and therefore have no fields.
trait Weapon
fn fire(self)
fn reload(self, magazine: Magazine) -> boolean
end
struct Riffle
bullets: number
end
impl Riffle
const MAX_BULLETS: number = 30
pub fn new() -> Self
return Self { bullets: MAX_BULLETS }
end
end
impl Weapon for Riffle
pub fn fire(self)
if self.bullets > 0 then
print("bang!")
self.bullets -= 1
else
print("empty!")
end
end
pub fn reload(self, magazine Magazine) -> boolean
print("reloading...")
self.bullets = Self::MAX_BULLETS
return true
end
end
- Previous
- Next