Skip to content

Latest commit

 

History

History
416 lines (312 loc) · 6.43 KB

File metadata and controls

416 lines (312 loc) · 6.43 KB

Coming from Go

Quick reference for Go developers. For full details, see reference.md

Every section below is a Go pattern with a Lisette equivalent.

Variables

x := 5          // mutable
x = 6           // mutated
var y int       // mutable, zero-initialized
let x = 5       // immutable
x = 6           // error: mutation disallowed

let mut y = 5   // mutable
y = 6           // mutated

let z: int      // error: must initialize

Variables are immutable by default. Use let mut for mutable bindings.

Functions

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.

Lambdas

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.

Enums

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.

Pattern matching

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.

Error handling

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.

nil safety

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>>.

Pointers

x := 42
p := &x
fmt.Println(*p)
let x = 42
let p = &x
fmt.Println(p.*)

Dereference with postfix .*

Structs

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.

Methods

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.

Interfaces

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.

Collections

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>.

Loops

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.

Concurrency

ch := make(chan int)
go func() {
    ch <- 42
}()
v := <-ch
let 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).

Imports

import "fmt"
import "net/http"
import "go:fmt"
import "go:net/http"

To import Go packages into a Lisette project, prefix with go:

Strings

name := "world"
msg := fmt.Sprintf("Hello, %s!", name)
let name = "world"
let msg = f"Hello, {name}!"

Format strings use f"..." with {expr} interpolation.

String access

To index into a string:

s[i] // byte
s.rune_at(i)              // rune
s.byte_at(i)              // byte

To slice a string:

s[a:b] // bytes
s.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)
}

Pipeline operator

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

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)

Unused code

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`