25 August 2024
Borgo is a programming language that builds on Go,
borrowing some ideas from Rust and Zig while keeping compatibility with Go libraries. I came across
Borgo a week ago and I was immediately intrigued: it adds the two things I miss most in Go, which
are algebraic types and the ?
operator for concise error handling. I had to try it.
To evaluate the langauge, I decided to implement an idea I’ve been thinking about for a while -- a simplified version of Markdown -- then write up my experience. It the experiment’s a success, I might start using Borgo for some internal tooling at work.
Borgo is similar to Go, and the current implementation also compiles to Go code that you can then
compile using the regular go
command. This means Borgo programs rely on the Go runtime
(including its implementation of concurrency), standard library, and potentially third-party
libraries.
For me, two changes from Go stand out. The first one is algebraic data types, which basically means you have proper enums, and each enum variant can contain values including structs and other enums. This is the basis of Rust’s type system and also a common feature in statically-typed functional languages. (I wrote about algebraic data types back when I was learning Rust).
The other interesting change has to do with error handling. In Go, functions that can fail usually
have an error
return value (in addition to any other return values they may have),
which is nil
unless there’s an error. In Borgo, these functions instead have a return
type Result<T, E>
, where T
is the return type in case of success and
E
is the error value.
The ?
operator can be used to propagate an error to a function’s caller. For example,
this code:
let resp = http.Get("http://example.com/")?
corresponds to something like this in Go:
resp, err := http.Get("http://example.com/")
if err != nil {
return nil, err // or similar, depending on the return type
}
An interesting detail is that Borgo can automatically wrap Go functions and methods that return an
error
value so a function that returns (T, error)
for some type
T
will return Result<T, error>
instead.
To get some hands-on experience, I used Borgo to try to implement a simplified version of Markdown. The program should read in a text file and output HTML with headers, paragraphs, and so on.
You can see the resulting program here: main.brg. It only implements paragraphs and blockquotes because I got frustrated with Borgo’s limitations pretty quickly.
I first ran into trouble trying to use bufio.Scanner to read the input. Scanner provides a convenient way to read input line-by-line, but you have to call scanner.Err() at the end to check for errors:
func (s *Scanner) Err() error
Remember that I said Borgo automatically wraps methods that return (T, error)
? That
doesn’t apply here because the Err method returns just an error. So I can’t use Borgo’s error
handling. But I can’t use the Go convention (if err != nil { ... }
) either because
there’s no nil
value in Borgo.
Instead of bufio.Scanner, I tried using bufio.Reader, which is a slightly lower-level way of reading input. In Go you use it something like this:
line, err := reader.ReadString('\n')
if err == io.EOF {
// handle end of input
}
if err != nil {
// handle error
}
// use line
Borgo wraps the return value in a Result<string, error>
as expected, so I could
use the ?
operator like this:
line = reader.ReadString('\n')?
But that’s not right because it would treat the “end of input” condition as an error. It looks like
I’ll need to explicitly match the Result value and compare the error value with
io.EOF
… but it turns out io.EOF
doesn’t exist in Borgo’s version of
the standard library. I’m not sure why; in any case I decided to just use bufio.Scanner without
error handling.
Overall, it was not a success. There were some other annoying limitations, including the Borgo compiler producing quite confusing error messages for simple mistakes and the fact that the html package in the standard library isn’t available. (I needed html.EscapeString for obvious reasons.) The only benefit Borgo provided was a nice way to define an enum to represent the current state of the parser.
After I finished my little Borgo project I took another look a Borgo’s git repository. There’s been almost no changes in the last year and practically all commits before that are from one person, so it looks like Borgo is really just a proof-of-concept -- an experiment that’s run its course. Nothing wrong with that, but I’m a little annoyed at the author for not making that clear in the website and documentation. I’m also wondering what they concluded from their experiment: did it prove or disprove the concept?
For my part, I still think the basic idea could work. The Go team probably isn’t going to make big changes that break compatibility (nor should they) so a similar-but-different language that uses Go’s runtime and interoperates well with Go code could be a good alternative. Scala and Kotlin did the same thing for Java; Carbon is aiming to do something similar for C++.
It’s not an easy task, though. I think the issues I ran into with error handling show that Borgo’s design is a bit naive; anyone creating a Go-compatible language would really need to consider Go’s design and conventions in fine detail and put in the effort to come up with a language that’s elegant on its own while keeping compatibility. The same goes for the compiler and tooling: it has to match Go’s tooling in terms of speed and usability so it won’t be a step down for programmers coming from Go.