Learning YAMLScript from Clojure
YAMLScript works by translating code written in YAMLScript to Clojure code, which is then evaluated.
A good way to learn YAMLScript is to convert existing Clojure programs to YAMLScript.
This tutorial will guide you through the process of converting various Clojure programs to idiomatic YAMLScript a step at a time.
For each Clojure program, we will:
- Start with a working Clojure program
- Refactor some parts of the program to YAMLScript
- Repeat until YAMLScript is idiomatic
- Have working YAMLScript code every step of the way
If you don't know Clojure, that's okay. We're starting simple and you can learn 2 languages for the price of one.
Let's get started!
Hello World
Since we are obligated as programmers to start with a "Hello, World!" program, let's look at the simplest hello-world in Clojure:
(println "Hello, World!")
It turns out the this program is already valid YAMLScript!
$ ys -e '(println "Hello, World!")'
Hello, World!
Let's save it to a file called program.ys
and run it.
(println "Hello, World!")
$ ys program.ys
$
Dang. Nothing happened.
Let's compile it to Clojure to see what's going on.
$ ys -c program.ys
"(println \"Hello, World!\")"
Ah! It compiled to a string, because we forgot to add !yamlscript/v0
to the top of the file. All YAML files are valid YAMLScript files. They won't evaluate any code unless you explicitly tell them to.
!yamlscript/v0
(println "Hello, World!")
$ ys program.ys
Hello, World!
There we go!
Let's make it idiomatic now.
!yamlscript/v0
say: 'Hello, World!'
Look mom, no parentheses!
We turned the YAML scalar into a single pair mapping, and we changed the double-quoted string to a single-quoted string.
Single-quoted strings are preferred in YAMLScript unless you need interpolation or special escaped characters.
We also changed the println
function to the say
function, because who has time to type println
when you just want to say
something?!?!
Let's compile it back to Clojure to be honest with ourselves.
$ ys -c program.ys
(say "Hello, World!")
We got our Clojure parentheses and double-quoted string back. Clojure doesn't use single quotes for strings.
We still have say
instead of println
, but that's okay because ys -c
compiles to Clojure code intended to be run by a YAMLScript runtime, and say
is part of the YAMLScript standard library.
Hello 2.0
Let's make our little program a little more interesting.
(defn hello
([name] (println (str "Hello, " name "!")))
([] (hello "World")))
(hello)
(hello "YAMLScript")
We've defined a function hello
that takes an optional name
argument. If name
is not provided, it defaults to "World"
.
Let's convert this to YAMLScript, but change as little as possible.
!yamlscript/v0
=>: !clj |
(defn hello
([name] (println (str "Hello, " name "!")))
([] (hello "World")))
(hello)
(hello "YAMLScript")
Hmm. We added 2 lines to the top and then indented the Clojure code. Does that work?
$ ys program.ys
Hello, World!
Hello, YAMLScript!
Apparently it does!
We already know about the first line. The =>
is a special key in YAMLScript, when you need to write a YAML key/value pair, but you only have a value. The compiler simply removes the =>
and uses the value as the expression.
What did here is keep the entire Clojure code string intact using a YAML literal scalar (think heredocs) and then use the !clj
tag to tell the YAMLScript compiler to treat the string as Clojure code.
The !clj
tag is a way to write Clojure things that YAMLScript does not yet support. But it can also be a good first step to converting Clojure code to YAMLScript.
Let's keep going by leaving the function defn alone but playing with the function calls.
!yamlscript/v0
=>: !clj |
(defn hello
([name] (println (str "Hello, " name "!")))
([] (hello "World")))
hello:
hello: 'YAMLScript'
We made the 2 calls to hello
into YAML mapping pairs. The first one has no value, and that's valid in YAMLScript when a function has no arguments.
Now let's convert the function defn to YAMLScript.
!yamlscript/v0
defn hello:
(name): (println (str "Hello, " name "!"))
(): (hello "World")
hello:
hello: 'YAMLScript'
That's how you write a multi-arity function in YAMLScript. It's advanced stuff and you already learned it during hello-world! Take a moment! You deserve it!
!yamlscript/v0
defn hello(name='world'):
(println (str "Hello, " name "!"))
hello:
hello: 'YAMLScript'
Hey! What happened to our multi-arity accomplishment? We don't need it here in YAMLScript, because YAMLScript has support for default function arguments.
Let's finish up with a little interpolation.
!yamlscript/v0
defn hello(name='world'):
say: "Hello, $name!"
hello:
hello: 'YAMLScript'
That's come idiomatic YAMLScript if I've ever seen it!
FizzBuzz
Let's continue our journey of refactoring cliche coding conundrums with the classic FizzBuzz.
Here's a working Clojure implementation I found at Rosetta Code.
(defn fizzbuzz [start finish]
(map (fn [n]
(cond
(zero? (mod n 15)) "FizzBuzz"
(zero? (mod n 3)) "Fizz"
(zero? (mod n 5)) "Buzz"
:else n))
(range start finish)))
(doseq [x (fizzbuzz 1 101)]
(println x))
We'll skip the !clj
step this time and start by making this a top level YAML mapping.
!yamlscript/v0
defn fizzbuzz(start finish):
(map (fn [n]
(cond
(zero? (mod n 15)) "FizzBuzz"
(zero? (mod n 3)) "Fizz"
(zero? (mod n 5)) "Buzz"
:else n))
(range start finish))
doseq [x (fizzbuzz 1 101)]:
(println x)
It works! Trust me! Don't do that! Run it yourself! But it works! Trust me!
All we did was turn the top level expressions into YAML mapping pairs, by removing the outer parentheses and adding a colon in the middle.
We also changed the defn args to use parens instead of square brackets.
Let's make more expressions into pairs now.
!yamlscript/v0
defn fizzbuzz(start finish):
map:
fn(n):
cond:
(zero? (mod n 15)): "FizzBuzz"
(zero? (mod n 3)): "Fizz"
(zero? (mod n 5)): "Buzz"
else: n
range: start finish
doseq [x (fizzbuzz 1 101)]:
println: x
We also changed :else
to else
because YAMLScript likes it that way.
Does it work? You betcha! Are we done? Heck no!
!yamlscript/v0
defn fizzbuzz(start finish):
map _ (start .. finish):
fn(n):
cond:
(zero? (mod n 15)): 'FizzBuzz'
(zero? (mod n 3)): 'Fizz'
(zero? (mod n 5)): 'Buzz'
else: n
doseq [x (fizzbuzz 1 100)]:
println: x
Ok, hmm. We moved the range up to the top of the map call but put a _
right before it. And it's not a range call anymore, it's some operator expression.
..
is the rng
operator in YAMLScript and the end is inclusive so we didn't need to say 101 when we meant 100.
The _
is a placeholder for the pair value to go. In Clojure, many functions take a function as the first argument. If we need to actually define a big function there it would be nicer if we could do it last. In these cases _
is our friend.
Double quotes to single. What's next?
!yamlscript/v0
defn fizzbuzz(start finish):
map _ (start .. finish):
fn(n):
cond:
(mod n 15).!: 'FizzBuzz'
(mod n 3).!: 'Fizz'
(mod n 5).!: 'Buzz'
else: n
doseq x (fizzbuzz 1 100):
say: x
We replaced the zero?
calls with a .!
(short for .falsey?()
) call. We also removed the square brackets from the doseq
call because YAMLScript is cool like that.
Getting better...
In YAMLScript, if you define a function called main
it will be called automatically when the program is run.
Let's try that.
!yamlscript/v0
defn main(start=1 finish=100):
each x (start .. finish):
say:
fizzbuzz: x
defn- fizzbuzz(n):
cond:
(mod n 15).!: 'FizzBuzz'
(mod n 3).!: 'Fizz'
(mod n 5).!: 'Buzz'
else: n
We moved the heavy lifting code into a private function called fizzbuzz
and simply call it from the main
function for every number in our range.
We also made it so we can pass in the start and finish values as arguments:
$ ys program.ys 35 42
Buzz
Fizz
37
38
Fizz
Buzz
41
Fizz
Pretty sweet!
Let's tidy up this code a bit more, and come down from our fizzy buzz.
!yamlscript/v0
defn main(start=1 finish=100):
each x (start .. finish): x.fizzbuzz().say()
defn- fizzbuzz(n):
cond:
(n % 15).!: 'FizzBuzz'
(n % 3).!: 'Fizz'
(n % 5).!: 'Buzz'
else: n
We made the body of main
into a single pair by chaining the x
, fizzbuzz()
, and say()
together. I wouldn't say this is idiomatic YAMLScript, it's a little less readable imho, but sometimes we got to show off a little.
This is probably how I'd write that part:
each x (start .. finish):
say: fizzbuzz(x)
See how fizzbuzz
comes before the paren, not inside it? We call that a YeS expression and it's definitely idiomatic!
Finally we replaced the mod
calls with the %
operator. To be fair, %
is the rem
operator and %%
is the mod
operator in but with positive numbers they do the same thing.
I'm fizzed out! Let's move on!
To Be Continued...
We'll continue this journey soon. I promise.
I have lots more Idiomatic YAMLScript to show you.
Stay tuned!