Skip to content

YS on the Go

Yesterday I hinted at the idea of YS hosted on Go.

This doesn't mean that YS would be rewritten in Go or that it wouldn't compile to Clojure. The Lisp is still essential to YS.

Where I was going was the possibility of creating a Clojure hosted on Go, which I've been thinking about for a while.

Go is the backbone of technologies like Kubernetes, where YS wants to provide a more powerful YAML experience.

As I was getting ready for bed, the word "Glojure" popped into my head.

What a perfect name for a Go hosted Clojure!

Glojure🔗

I had to see if someone had already thought of this.

A quick search revealed that there were actually 2 projects on GitHub called Glojure!

One of them External link looked quite promising, so I decided to take a look at it after I had slept.

This morning I was able to get a YS program running on Glojure!

Let's take a look at it in action, and then I'll explain how it works (and also how it doesn't).

The program is a YS implementation of the famous fizzbuzz External link algorithm.

In this example we'll just print the first 16 numbers instead of the normal 100.

$ ys -ce '
say =: println
defn rng(a b): range(a b:inc)
defn or?(a b): a:empty?.if(b a)

doseq x (1 .. 16): !:say
  or?:
    str:
      zero?(x % 3).when("Fizz")
      zero?(x % 5).when("Buzz")
    =>: x
' | glj
#'user/say
#'user/rng
#'user/or?
1
2
Fizz
4
Buzz
Fizz
7
8
Fizz
Buzz
11
Fizz
13
14
FizzBuzz
16
nil

It works!

Right?

Glojure Basics🔗

If you have a recent version of Go installed, glj (the Glojure binary) is easy to install and run:

$ go install github.com/glojurelang/glojure/cmd/glj@latest
$ echo '(println "Hello, World!")' | glj
Hello, World!
nil

The nil was unexpected in this context. The Clojure command clj does this:

$ clj -M -e '(println "Hello, World!")'
Hello, World!

But those are not quite the same. Glojure doesn't support the -e flag currently.

I feel like glj is starting a REPL session, and then evaluating the code. In that case, printing nil is expected.

I'm not fully up to speed on how Glojure works, but it doesn't matter for what I'm showing you today.

A more fair comparison would be:

$ glj <<<'(println "Hello, World!")'
Hello, World!
nil
$ clj <<<'(println "Hello, World!")'
Clojure 1.12.1
user=> Hello, World!
nil
user=>
$

So yep, they are both starting a REPL session, evaluating the code (which prints Hello, World! and returns nil (which the REPL correctly shows)), and then exiting.

How YS Works with Glojure🔗

With normal YS you'd do this:

$ ys -e '
each x (1 .. 16): !:say
  or?:
    str:
      zero?(x % 3).when("Fizz")
      zero?(x % 5).when("Buzz")
    =>: x
'
1
2
Fizz
4
Buzz
Fizz
7
8
Fizz
Buzz
11
Fizz
13
14
FizzBuzz
16

The ys -c flag compiles the YS code to Clojure, so let's pipe it to glj:

$ ys -c -e '
each x (1 .. 16): !:say
  or?:
    str:
      zero?(x % 3).when("Fizz")
      zero?(x % 5).when("Buzz")
    =>: x
' | glj
unable to resolve symbol: each

Well, Clojure (thus Glojure) doesn't have a each function. True. each is part of the ys::std library.

It's really just an alternate form for doseq. Let's try it with doseq:

$ ys -c -e '
doseq x (1 .. 16): !:say
  or?:
    str:
      zero?(x % 3).when("Fizz")
      zero?(x % 5).when("Buzz")
    =>: x
' | glj
unable to resolve symbol: rng

Hmmm, rng isn't even used.

It's time to look at the Clojure code that ys -c generates:

$ ys -c -e '
doseq x (1 .. 16): !:say
  or?:
    str:
      zero?(x % 3).when("Fizz")
      zero?(x % 5).when("Buzz")
    =>: x
'
(doseq
 [x (rng 1 16)]
 (say
  (or?
   (str
    (when (zero? (rem x 3)) "Fizz")
    (when (zero? (rem x 5)) "Buzz"))
   x)))

OK! There's rng. It looks like .. compiles to rng.

Note

The ys::std/rng is an alternate form for the standard Clojure range function. It includes the upper bound, which is often what you want.

There's also or? and say. Those aren't in Clojure/Glojure either.

It looks like we need a way to include the ys::std library in the Glojure runtime environment.

Well that's gonna take some thought and effort.

But we only need 4 missing functions for this example: each, rng, or? and say.

Let's see if we can just define them quickly in our YS 1-liner:

$ ys -ce '
each =: doseq
defn rng(a b): range(a b:inc)
defn or?(a b): a:empty?.if(b a)
say =: println

each x (1 .. 16): !:say
  or?:
    str:
      zero?(x % 3).when("Fizz")
      zero?(x % 5).when("Buzz")
    =>: x
' | glj
can't take value of a macro: #'glojure.core/doseq
#'user/rng
#'user/or?
#'user/say
unable to resolve symbol: x

It doesn't like us renaming doseq to each.

Note

I tried for quite a while to get this to work. That part's not in the cards for today. We'll just use doseq for now.

$ ys -ce '
defn rng(a b): range(a b:inc)
defn or?(a b): a:empty?.if(b a)
say =: println

doseq x (1 .. 16): !:say
  or?:
    str:
      zero?(x % 3).when("Fizz")
      zero?(x % 5).when("Buzz")
    =>: x
' | glj
#'user/say
#'user/rng
#'user/or?
1
2
Fizz
4
Buzz
Fizz
7
8
Fizz
Buzz
11
Fizz
13
14
FizzBuzz
16
nil

It works!

Just like it did at the start of the blog post. 😄

We know why it prints nil at the end. The first 3 lines (#'user/say etc) are just the return values of the functions we defined.

Conclusion🔗

I'm impressed with how well Glojure works even though it's still in its early days.

It feels very promising and I'm definitely going to push harder on getting it to be a runtime option for YS!

Comments