Quick reference for Go developers. For full details, see reference.md
Every section below is a Go pattern with a Lisette equivalent.
x := 5 // mutable
x = 6 // mutated
var y int // mutable, zero-initializedlet x = 5 // immutable
x = 6 // error: mutation disallowed
let mut y = 5 // mutable
y = 6 // mutated
let z: int // error: must initializeVariables are immutable by default. Use let mut for mutable bindings.
func add(a int, b int) int {
return a + b
}fn add(a: int, b: int) -> int {
a + b
}The last expression is the return value. Use return for early exits.
double := func(x int) int {
return x * 2
}
nums = filter(nums, func(x int) bool {
return x > 0
})let double = |x: int| x * 2
let nums = nums.filter(|x| x > 0)Parameters go between | pipes. Types can be inferred.
Go has no equivalent. Lisette enums can carry data:
enum Result<T, E> {
Ok(T),
Err(E),
}
match r {
Ok(value) => process(value),
Err(e) => handle(e),
}This is how Option and Result work. Lisette has no nil type.
switch dir {
case "north", "south":
fmt.Println("vertical")
case "east", "west":
fmt.Println("horizontal")
default:
fmt.Println("unknown")
}match dir {
"north" | "south" => fmt.Println("vertical"),
"east" | "west" => fmt.Println("horizontal"),
_ => fmt.Println("unknown"),
}Lisette enforces exhaustiveness in match statements.
result, err := doSomething()
if err != nil {
return nil, err
}
process(result)let result = do_something()?
process(result)The ? operator unwraps Ok or returns early with Err. Functions returning (T, error) in Go become Result<T, error> in Lisette.
var user *User // pointer can be `nil`
if user != nil {
fmt.Println(user.Name)
}let user = get_user(id) // `Option<Ref<User>>`
if let Some(u) = user {
fmt.Println(u.name)
}Ref<T> is guaranteed non-nil. Nilable pointers become Option<Ref<T>>.
x := 42
p := &x
fmt.Println(*p)let x = 42
let p = &x
fmt.Println(p.*)Dereference with postfix .*
type User struct {
Name string
email string // unexported
}
u := User{Name: "Alice", email: "a@b.com"}struct User {
pub name: string,
email: string, // private
}
let u = User { name: "Alice", email: "a@b.com" }Fields are private by default. Use pub to export.
func (r Rectangle) Area() float64 {
return r.Width * r.Height
}
func (r *Rectangle) Scale(factor float64) {
r.Width *= factor
r.Height *= factor
}impl Rectangle {
fn area(self) -> float64 {
self.width * self.height
}
fn scale(self: Ref<Rectangle>, factor: float64) {
self.width *= factor
self.height *= factor
}
}Methods live in impl blocks. Use self for value receiver, self: Ref<T> for pointer receiver.
type Reader interface {
Read(p []byte) (n int, err error)
}
type ReadWriter interface {
Reader
Writer
}interface Reader {
fn Read(self, p: Slice<byte>) -> Result<int, error>
}
interface ReadWriter {
embed Reader
embed Writer
}Lisette uses structural typing for interfaces, like Go.
nums := []int{1, 2, 3}
nums = append(nums, 4)
ages := make(map[string]int)
ages["Alice"] = 20
age, ok := ages["Bob"]let nums = [1, 2, 3]
let nums = nums.append(4)
let mut ages = Map.new<string, int>()
ages["Alice"] = 20
let age = ages.get("Bob") // Option<int>Lisette offers Slice<T> and Map<K, V>. Map access returns Option<V>.
for i := 0; i < 10; i++ {
fmt.Println(i)
}
for _, item := range items {
process(item)
}
for {
if done() { break }
}for i in 0..10 {
fmt.Println(i)
}
for item in items {
process(item)
}
loop {
if done() { break }
}No C-style for. Use ranges (0..10) or loop for infinite loops.
ch := make(chan int)
go func() {
ch <- 42
}()
v := <-chlet ch = Channel.new<int>()
task { ch.send(42) }
let v = ch.receive() // Option<int>task instead of go, methods instead of operators. receive returns Option<int>. send returns bool (false if the channel was closed).
import "fmt"
import "net/http"import "go:fmt"
import "go:net/http"To import Go packages into a Lisette project, prefix with go:
name := "world"
msg := fmt.Sprintf("Hello, %s!", name)let name = "world"
let msg = f"Hello, {name}!"Format strings use f"..." with {expr} interpolation.
To index into a string:
s[i] // bytes.rune_at(i) // rune
s.byte_at(i) // byteTo slice a string:
s[a:b] // bytess.substring(a..b) // string (rune-indexed)
s.bytes()[a..b] // Slice<byte>
s.runes()[a..b] // Slice<rune>To iterate over a string:
for _, r := range s {
fmt.Println(r)
}for r in s.runes() {
fmt.Println(r)
}
for b in s.bytes() {
fmt.Println(b)
}result := strings.ReplaceAll(strings.ToLower(strings.TrimSpace(" Hello World ")), " ", "-")let result = " Hello World "
|> strings.TrimSpace
|> strings.ToLower
|> strings.ReplaceAll(" ", "-")|> passes the left side as the first argument to the right side. Reads top-to-bottom instead of inside-out.
Named numeric types like time.Duration work as in Go, with the conversion spelled as T rather than T(x):
multiplier := 100
delay := time.Millisecond * time.Duration(multiplier)let multiplier = 100
let delay = time.Millisecond * (multiplier as time.Duration)In Go, unused variables and imports blocks compilation. In Lisette, they are warnings that allow compilation and can be cleaned up later.
▲ Unused variable
╭─[example.lis:5:7]
4 │ fn main() {
5 │ let x = 42
· ┬
· ╰── never used
6 │ fmt.Println("hello")
╰────
help: Use this variable or prefix it with an underscore: `_x`