AGL (AnotherGoLang)
Description
AGL is a language that compiles to Go.
It uses Go's syntax, in fact its lexer/parser is a fork of the original Go implementation, with a few modifications
The main differences are:
- Functions return only a single value. This makes it possible to use types like
Option[T]andResult[T], and to support automatic error propagation via an operator. - To make returning multiple values easy, a Tuple type has been introduced. For example:
Result[(u8, string, bool)] - AGL can be used as a scripting language
Notable change: number types are int i8 i16 i32 i64 uint u8 u16 u32 u64 f32 f64
features
- Tuple / Enum / Set
- Immutable variables/structs by default
- Error propagation operators (
?for Option[T] /!for Result[T]) - Concise anonymous function with type inferred arguments (
other := someArr.Filter({ $0 % 2 == 0 })) - Array built-in Map/Reduce/Filter/Find/Sum methods
- Operator overloading
- Compile down to Go code
- VSCode extension & LSP (language server protocol)
- SourceMap
- Shell "shebang" support
Community
- Discord server https://discord.gg/68FJ767Yna
How to use
go build // Build the "agl" executable ./agl main.agl // Output Go code in stdout ./agl run main.agl // Run the code directly and output the result in stdout ./agl build main.agl // Create a main.go file
Error propagation
Result propagation
package main func getInt() int! { // `int!` means the function return a `Result[int]` return Ok(42) } func intermediate() int! { num := getInt()! // Propagate 'Err' value to the caller return Ok(num + 1) } func main() { num := intermediate()! // crash on 'Err' value print(num) }
Option propagation
package main func maybeInt() int? { // `int?` means the the function return an `Option[int]` return Some(42) } func intermediate() int? { num := maybeInt()? // Propagate 'None' value to the caller return Some(num + 1) } func main() { num := intermediate()? // crash on 'None' value print(num) }
Propagation chaining
package main type Person struct { Name string } func (p Person) MaybeSelf() Person? { return Some(p) } func main() { bob := Person{Name: "bob"} bob.MaybeSelf()?.MaybeSelf()?.MaybeSelf()? }
If let Some(val) := ... { to use a Option[T]/Result[T] value safely
This pattern works with any of Ok|Err|Some
package main func maybeInt() int? { Some(42) } // Implicit return when a single expression is present func main() { if let Some(num) := maybeInt() { print(num) } }
Guard
This pattern works with any of Ok|Err|Some
package main func maybeInt() int? { Some(42) } // Implicit return when a single expression is present func main() { guard let Some(num) := maybeInt() else { return } // can use guard to unwrap a value guard num < 100 else { return } // can use guard as a reverse if assert(num == 42) }
Match
package main func getInt() int! { Ok(42) } func maybeInt() int? { Some(42) } func main() { match getInt() { case Ok(num): print("Num:", num) case Err(err): print("Error:", err) } match maybeInt() { case Some(num): print("Num:", num) case None: print("No value") } }
or_break/or_continue
or_break/or_continue will break/continue on a None/Err value
package main import "time" func test(i int) int? { if i >= 2 { return None } return Some(i) } func main() { for i in 0..10 { res := test(i) or_break // `res` has type `int` print(res) // will print the value `0` and `1` time.Sleep(time.Second) } }
Short anonymous function (type inferred)
Arguments are mapped into $0|$1...
In this example, x becomes $0 when using the short form.
| Lang | Expression |
|---|---|
| Go | utils.Filter(arr, func(x int) bool { return x % 2 == 0 }) |
| AGL | arr.Filter({ $0 % 2 == 0 }) |
Since the function is expected to return something and there is only one expression, it will be returned automatically.
package main type Person struct { Name string Age int } func main() { arr := []int{1, 2, 3, 4, 5} sum := arr.Filter({ $0 % 2 == 0 }).Map({ $0 + 1 }).Sum() assert(sum == 8) p1 := Person{Name: "foo", Age: 18} p2 := Person{Name: "bar", Age: 19} people := []Person{p1, p2} names := people.Map({ $0.Name }).Joined(", ") sumAge := people.Map({ $0.Age }).Sum() assert(names == "foo, bar") assert(sumAge == 37) }
You can also name the arguments using the following syntax:
names := people.Map(|person| { person.Name }).Joined(", ")
If a single statement is present in the lambda body, the curly brace can be omitted:
names := people.Map(|person| person.Name).Joined(", ")
Enum (sum type)
package main type IpAddr enum { V4(u8, u8, u8, u8) V6(string) } func main() { home := IpAddr.V4(127, 0, 0, 1) // match branches must be exhaustive match home { case .V4(a, b, c, d): print(a, b, c, d) case .V6(addr): print(addr) } // if-let can be used to pattern match if let IpAddr.V4(a, b, c, d) := home { print(a, b, c, d) } // guard-let can also be used to pattern match guard let IpAddr.V4(a, b, c, d) := home else { return } print(a, b, c, d) }
Destructuring
package main type IpAddr enum { v4(u8, u8, u8, u8) v6(string) } func main() { // enum values can be destructured addr1 := IpAddr.v4(127, 0, 0, 1) (a, b, c, d) := addr1 // tuple can be destructured tuple := (1, "hello", true) (e, f, g) := tuple print(a, b, c, d, e, f, g) }
For-in
package main func main() { for el in []int{1, 2, 3} { print(el) } for el in set[int]{1, 2, 3} { print(el) } for (k, v) in map[string]u8{"a": 1, "b": 2, "c": 3} { print(k, v) } for (a, b, c) in [](int, string, bool){(1, "foo", true), (2, "bar", false)} { print(a, b, c) } for el in 0..3 { // 0 1 2 print(el) } for el in 0..=3 { // 0 1 2 3 print(el) } for el in (0..3).Rev() { // 2 1 0 print(el) } for c in "hello" { print(string(c)) } for (i, c) in "hello".Enumerated() { print(i, string(c)) } for el in []int{1, 2, 3} where el % 2 == 0 { // for-in can have an optional where clause print(el) } }
Template string
package main func main() { name := "bob" age := 42 print(t"{name} is {age} years old") // bob is 42 years old }
Operator overloading
package main type Person struct { Name string Age int } func (p Person) == (other Person) bool { return p.Age == other.Age } func main() { p1 := Person{Name: "foo", Age: 42} p2 := Person{Name: "bar", Age: 42} assert(p1 == p2) // People of the same age are obviously equal! }
Extend built-in types
package main func (v agl.Vec[T]) Even() []T { out := make([]T, 0) for _, el := range v { if el % 2 == 0 { out = append(out, el) } } return out } func main() { arr := []int{1, 2, 3, 4, 5, 6, 7, 8} res := arr.Even().Filter({ $0 <= 6 }).Map({ $0 + 1 }) // ^^^^^^ new method available print(res) // [3 5 7] }
Methods can have generic type parameters
func (v agl.Vec[T]) MyMap[R any](clb func(T) R) []R { mut out := make([]R, 0) for _, el := range v { out = append(out, clb(el)) } return out }
You can also extend for a specific type of vector
func (v agl.Vec[string]) MyJoined(sep string) string { return strings.Join(v, sep) }
assert & precondition
assert and assertEq will not be part of the release code.
precondition will be part of both debug and release code.
package main func main() { a := 42 assert(a == 42) assert(a == 42, "some message") assertEq(a, 42) // assertEq gives a better error message containing the values at runtime assertEq(a, 42, "some message") precondition(a == 42) precondition(a == 42, "some message") }
ExpressibleByStringLiteral
package main type Point struct { X, Y int } // Point implements ExpressibleByStringLiteral func (Point) FromStringLit(s string) Point { (p1, p2) := s.Cut(":")? return Point{X: p1.Int()?, Y: p2.Int()?} } func PrintPoint(p Point) { print(p.X, p.Y) } func main() { PrintPoint("1:2") // a string literal can be used to create a Point PrintPoint(Point{X: 1, Y: 2}) }
Nil-Coalescing operator (??)
package main type Container struct { Val int } /* a := c1 ?? &Container{Val: 42} // is equivalent to --> a := c1 if a == nil { a = &Container{Val: 42} } */ func main() { var c1 *Container c2 := &Container{Val: 1} a := c1 ?? &Container{Val: 42} print(a.Val) // 42 b := c2 ?? &Container{Val: 42} print(b.Val) // 1 }
Nil-Conditional assignment operator (?.)
package main type Container struct { mut Val int } /* c1?.Val = 42 // is equivalent to --> if c1 != nil { c1.Val = 42 } */ func main() { var mut c1 *Container mut c2 := &Container{} c1?.Val = 42 c2?.Val = 42 print(c1) // <nil> print(c2) // &{42} }
Using Go libraries
package main import "agl/os" func main() { os.WriteFile("test.txt", []byte("test"), 0755)! by := os.ReadFile("test.txt")! print(string(by)) }
Here is a guide on how to use external libraries
And here is a simple example
Making http request
package main import ( "agl/net/http" "agl/io" ) func main() { req := http.NewRequest(http.MethodGet, "https://google.com", None)! c := http.Client{} resp := c.Do(req)! defer resp.Body.Close() by := io.ReadAll(resp.Body)! print(string(by)) }
Scripting
#!/usr/bin/env agl run package main func main() { print("Hello AGL!") }
$ chmod +x hello.agl
$ ./hello.agl
Hello AGL!