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.