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
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
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!