Skip to content

Go interop

Lyth has no foreign-function layer: Lyth code is Go code once compiled, so every Go package works directly, with its exact Go names and types, and every Lyth package is an ordinary Go package that Go code can import. This page shows how Go APIs look from Lyth and how Lyth APIs look from Go. The design is in interop, and the tested status of each construct is in the interop matrix.

import "fmt"
import "net/url"
import "strings"
import "crypto/sha256" as sha
func main() {
u := url.Parse("https://example.com/a?b=1") catch { return }
fmt.Println(u.Host, u.Query().Get("b"))
user, domain, found := strings.Cut("ada@example.com", "@")
println(user, domain, found)
println(len(sha.Sum256([]byte("lyth"))))
}
import (
sha "crypto/sha256"
"fmt"
"net/url"
"strings"
)
func main() {
u, err := url.Parse("https://example.com/a?b=1")
if err != nil {
return
}
fmt.Println(u.Host, u.Query().Get("b"))
user, domain, found := strings.Cut("ada@example.com", "@")
fmt.Println(user, domain, found)
fmt.Println(len(sha.Sum256([]byte("lyth"))))
}
example.com 1
ada example.com true
32

Imports are Go import paths, resolved by the go command against your go.mod, and the qualifier is the package’s declared name. as renames it. Go identifiers are used exactly as Go spells them (u.Host, strings.Cut): Lyth never translates names in that direction, and a wrong case gets a suggestion (strings.cut → “Go names are used verbatim: write strings.Cut”). Multiple results are declared as in Go: a, b, c := ....

A Go function whose last result is error is fallible in Lyth: ? propagates the error and catch handles it, so url.Parse(s)? is the everyday form. When you want the raw pair, destructure it: u, err := url.Parse(s). Ignoring the error entirely is a compile error, except for the conventional cases errcheck also exempts (writes to os.Stdout, strings.Builder, bytes.Buffer, and fmt.Print*).

import "fmt"
import "net/http"
import "strings"
import "time"
func main() {
srv := &http.Server(Addr = ":8080", ReadHeaderTimeout = 5 * time.Second)
srv.IdleTimeout = time.Minute
fmt.Println(srv.Addr, srv.ReadHeaderTimeout, srv.IdleTimeout)
sb := strings.Builder()
sb.WriteString("built ")
sb.WriteString("in Lyth")
println(sb.String(), sb.Len())
}
import (
"fmt"
"net/http"
"strings"
"time"
)
func main() {
srv := &http.Server{Addr: ":8080", ReadHeaderTimeout: 5 * time.Second}
srv.IdleTimeout = time.Minute
fmt.Println(srv.Addr, srv.ReadHeaderTimeout, srv.IdleTimeout)
sb := strings.Builder{}
sb.WriteString("built ")
sb.WriteString("in Lyth")
fmt.Println(sb.String(), sb.Len())
}
:8080 5s 1m0s
built in Lyth 13

Go structs are constructed with named arguments only, since Go field order is not part of an API. Exported fields are read and written directly. Methods with pointer receivers work on locals (Lyth takes the address, as Go does); on a read-only value such as a parameter they are an error (E0504), because the method could modify it.

import "fmt"
import "sort"
struct Temperature {
Celsius: float64
}
func Temperature.String(): string = "${Celsius}°C"
struct NotFound {
Key: string
}
func *NotFound.Error(): string = "not found: ${Key}"
struct byLength : sort.Interface {
Words: []string
}
func byLength.Len(): int = len(Words)
func byLength.Less(i: int, j: int): bool = len(Words[i]) < len(Words[j])
func byLength.Swap(i: int, j: int) {
Words[i], Words[j] = Words[j], Words[i]
}
func lookup(key: string): int throws {
throw &NotFound(Key = key)
}
func main() {
fmt.Println(Temperature(Celsius = 21.5))
words := ["banana", "fig", "kiwi"]
sort.Sort(byLength(Words = words))
println(words)
lookup("answer") catch (e: *NotFound) {
println("missing", e.Key)
} catch {
println("lookup failed")
}
}
import (
"errors"
"fmt"
"sort"
"strconv"
)
type Temperature struct {
Celsius float64 `json:"celsius"`
}
func (t Temperature) String() string { return strconv.FormatFloat(t.Celsius, 'g', -1, 64) + "°C" }
type NotFound struct {
Key string `json:"key"`
}
func (n *NotFound) Error() string { return "not found: " + n.Key }
type byLength struct {
Words []string `json:"words"`
}
var _ sort.Interface = byLength{}
func (b byLength) Len() int { return len(b.Words) }
func (b byLength) Less(i int, j int) bool { return len(b.Words[i]) < len(b.Words[j]) }
func (b byLength) Swap(i int, j int) {
b.Words[i], b.Words[j] = b.Words[j], b.Words[i]
}
func lookup(key string) (int, error) {
return 0, &NotFound{Key: key}
}
func main() {
fmt.Println(Temperature{Celsius: 21.5})
words := []string{"banana", "fig", "kiwi"}
sort.Sort(byLength{Words: words})
fmt.Println(words)
if _, err := lookup("answer"); err != nil {
if e, ok := errors.AsType[*NotFound](err); ok {
fmt.Println("missing", e.Key)
} else {
fmt.Println("lookup failed")
}
}
}
21.5°C
[fig kiwi banana]
missing answer

A Lyth type implements a Go interface the Go way, by having its methods with the interface’s exact names (String, Error, ServeHTTP). fmt.Stringer, error, sort.Interface, http.Handler, and io.Writer are implemented this way throughout the examples and tests. Declaring the interface, as in struct byLength : sort.Interface, is optional: it makes the compiler report a missing or mistyped method at the struct, and generates Go’s var _ sort.Interface = byLength{}. Lyth error types work with errors.Is, errors.AsType, and typed catch clauses, which see through wrapping.

import "cmp"
import "errors"
import "io/fs"
import "os"
import "slices"
struct Item {
Name: string
Price: int
}
func main() {
items := [Item(Name = "tea", Price = 4), Item(Name = "cake", Price = 7), Item(Name = "jam", Price = 2)]
slices.SortFunc(items, { a, b -> cmp.Compare(a.Price, b.Price) })
println(items[0].Name, slices.IndexFunc(items, { it.Name == "cake" }))
println(slices.Max([3, 9, 4]), slices.Contains(["a", "b"], "b"))
os.Remove("/no/such/file") catch (err) {
pe, ok := errors.AsType[*fs.PathError](err)
println(ok, pe.Op, pe.Path)
}
}
import (
"cmp"
"errors"
"fmt"
"io/fs"
"os"
"slices"
)
type Item struct {
Name string `json:"name"`
Price int `json:"price"`
}
func main() {
items := []Item{{Name: "tea", Price: 4}, {Name: "cake", Price: 7}, {Name: "jam", Price: 2}}
slices.SortFunc(items, func(a Item, b Item) int {
return cmp.Compare(a.Price, b.Price)
})
fmt.Println(items[0].Name, slices.IndexFunc(items, func(it Item) bool {
return it.Name == "cake"
}))
fmt.Println(slices.Max([]int{3, 9, 4}), slices.Contains([]string{"a", "b"}, "b"))
if err := os.Remove("/no/such/file"); err != nil {
pe, ok := errors.AsType[*fs.PathError](err)
fmt.Println(ok, pe.Op, pe.Path)
}
}
jam 2
9 true
true remove /no/such/file

Type arguments of generic Go functions are inferred from the arguments, including a lambda’s parameter types (a and b are Item above). When they cannot be inferred, give them all explicitly in square brackets, as in Go: errors.AsType[*NotFound](https://github.com/lyth-lang/lyth/tree/main/docs/guide/err) calls a generic function, and pgx.RowToStructByName[User] is a generic function value. Generic functions and types declared in Lyth are Go generics too, so Go code calls them as pkg.MaxOf(1, 2) or names them as pkg.Page[User], and a Lyth extension function is an ordinary Go function taking the receiver first.

import "fmt"
import "net/http"
import "net/http/httptest"
import "strings"
import "unicode"
typealias Middleware = (http.Handler) -> http.Handler
func withHeader(name: string, value: string): Middleware = { next ->
http.HandlerFunc({ w, r ->
w.Header().Set(name, value)
next.ServeHTTP(w, r)
})
}
func main() {
println(strings.Map({ r -> unicode.ToUpper(r) }, "shout"))
app := withHeader("X-Powered-By", "Lyth")(http.HandlerFunc({ w, r ->
fmt.Fprint(w, "ok") catch {}
}))
rec := httptest.NewRecorder()
app.ServeHTTP(rec, httptest.NewRequest("GET", "/", null))
println(rec.Header().Get("X-Powered-By"), rec.Body.String())
}
import (
"fmt"
"net/http"
"net/http/httptest"
"strings"
"unicode"
)
type Middleware = func(http.Handler) http.Handler
func withHeader(name string, value string) func(http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set(name, value)
next.ServeHTTP(w, r)
})
}
}
func main() {
fmt.Println(strings.Map(func(r rune) rune {
return unicode.ToUpper(r)
}, "shout"))
app := withHeader("X-Powered-By", "Lyth")(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if _, err := fmt.Fprint(w, "ok"); err != nil {
}
}))
rec := httptest.NewRecorder()
app.ServeHTTP(rec, httptest.NewRequest("GET", "/", nil))
fmt.Println(rec.Header().Get("X-Powered-By"), rec.Body.String())
}
SHOUT
Lyth ok

Lambdas are Go closures and can be passed wherever Go expects a function. A lambda converted to a named Go function type, such as http.HandlerFunc, takes its parameter types from that type. typealias names a function type without creating a new one, which suits handler and middleware types. A Go callback type that returns an error (fs.WalkDirFunc) accepts a lambda that uses ?, because Go has no throws and a trailing error is how it says a function can fail. A function type written in Lyth declares that with throws; (int) -> error? is an ordinary function type whose lambda returns the error as its value.

Go does not say whether a pointer, interface, map, or function may be nil, so values that come from Go have platform types, written T! in diagnostics. Lyth lets you use them as non-null, and does not insert checks: a nil dereference panics as it would in Go, with the .lyth line in the stack trace. Declare a local of type T? for a Go result (u: *url.URL? = f()) when you want Lyth to make you check it.

In the other direction, a nullable Lyth value may be passed to a Go parameter in its Go representation: a string? goes in as *string (nil when null), which is what database/sql, pgx, and encoding/json expect for optional values. See ADR 0017.

Lyth’s own interface types work differently. When a nullable pointer or optional value becomes an error?, any?, or other nullable interface in Lyth code, null becomes a nil interface, never Go’s non-nil interface holding a nil pointer, and an optional value goes in unboxed (types).

import "errors"
struct Account {
Owner: string
Balance: int64
pin: string
}
var ErrInsufficient = errors.New("insufficient funds")
func Open(owner: string): *Account = &Account(Owner = owner, Balance = 0, pin = "0000")
func *Account.Withdraw(amount: int64): int64 throws {
if amount > Balance {
throw ErrInsufficient
}
Balance -= amount
return Balance
}
import "errors"
type Account struct {
Owner string `json:"owner"`
Balance int64 `json:"balance"`
pin string
}
var ErrInsufficient = errors.New("insufficient funds")
func Open(owner string) *Account { return &Account{Owner: owner, Balance: 0, pin: "0000"} }
func (a *Account) Withdraw(amount int64) (int64, error) {
if amount > a.Balance {
return 0, ErrInsufficient
}
a.Balance -= amount
return a.Balance, nil
}

The Go tab is the API Go code sees: Open, Account with exported Owner and Balance fields and their json tags, an unexported pin, and Withdraw(amount int64) (int64, error). Capitalized names are exported, as in Go, and every Lyth name is used unchanged as the Go name, so ID and UserID are spelled the way Go code expects. An enum Kind { Round } is a Go type Kind int with constants KindRound and a String method, and a sealed interface is an interface Go code can use but, because of its unexported marker method, not implement. Go code calls Lyth packages like any other Go package. The REST API example tests its Lyth store from a plain Go test file (testing).

Dependencies are ordinary Go modules in go.mod:

Terminal window
go get github.com/jackc/pgx/v5
lyth mod tidy

Use lyth mod tidy instead of go mod tidy: the go command cannot see imports inside .lyth files and would remove modules only Lyth code uses. The REST API example uses pgx this way.

  • A package cannot mix Lyth and Go source files yet, apart from _test.go files. Put Go code in its own package and import it.
  • Iterating over Go iterator functions (for x in seq with an iter.Seq) is not implemented yet; collect one with slices.Collect or maps.Collect first. Channels are: Go’s <- is spelled ch.send(v) and ch.receive() (<-ctx.Done() is ctx.Done().receive()), and select has when-style branches.