AI + Clojure Functions in YAML
Yesterday we learned that all YS YAML input compiles to Clojure (Lisp) before being evaluated by a native binary Clojure interpreter runtime.
Does this mean that you could write Lisp functions in your YAML data files? And then call them on your data?
Of course it does!
Dynamic port numbers🔗
Let's imagine we have this config.yaml
file:
server:
host: localhost
port: 8080
ssl: true
database:
driver: postgres
host: db.example.com
port: 5432
name: myapp
credentials:
username: admin
password: s3cr3t
Now let's say we'd rather not need to specify the port
values.
We'd like the port to be automatically added depending on certain conditions.
Here's a simplistic solution using a YS function:
!YS-v0
defn auto-port(mapping):
assoc mapping 'port': 1234
--- !data
server::
auto-port::
host: localhost
ssl: true
database::
auto-port::
driver: postgres
host: db.example.com
name: myapp
credentials:
username: admin
password: s3cr3t
So we wrote a little YS function that takes a mapping and adds a port
key with
a value of 1234
.
Not very exciting or useful, but it's a start.
Next we put a function call to auto-port
in front of the server
and
database
mappings.
This wasn't great because we had to indent those sections in order to call the
function.
Let's see if it works:
$ ys -Y config.yaml
server:
host: localhost
ssl: true
port: 1234
database:
driver: postgres
host: db.example.com
name: myapp
credentials:
username: admin
password: s3cr3t
port: 1234
Nice. It does work. It added the port to the end of each mapping, but that's OK (for now).
Let's keep going on this one.
AI Generated YS🔗
Using a function to generate 1234
was silly.
Let's say we want the auto-port
function to return a random port number in the
range of 8000-8999 if the host
value is localhost
.
If the driver
value is postgres
, we'll use port 5432.
Otherwise we'll return 0 (for unknown).
Since we are new to YS and super busy, we'll use AI to generate the function.
Here's the prompt:
Write a YS function that takes a mapping argument and returns the mapping with a new pair added:
port: <number>
. If the mapping has ahost
key and the value islocalhost
, use a random port number in the range of 8000-8999. If the mapping has adriver
key and the value ispostgres
, use port 5432. Otherwise use port 0 (for unknown).
Here's the AI generated function:
!YS-v0
defn auto-port(mapping):
if mapping.get('host') == 'localhost':
return mapping.assoc('port' rand-int(8000, 8999))
elif mapping.get('driver') == 'postgres':
return mapping.assoc('port' 5432)
Let's see if it works:
$ ys -e "
defn auto-port(mapping):
if mapping.get('host') == 'localhost':
return mapping.assoc('port' rand-int(8000, 8999))
elif mapping.get('driver') == 'postgres':
return mapping.assoc('port' 5432)
autoport::
foo: 42"
Error: Could not resolve symbol: return
Nope! Amazingly this generated code was actually valid YS syntax, because it compiled. Unfortunately this isn't how you write that in YS.
Note
Use ys -ce
in the command above to see the (broken) Clojure/Lisp code
that was generated.
The overall problem is that YS is a new language and there isn't enough of it in the wild yet for AI to train on.
Using the !clj
tag🔗
Clojure on the other hand is approaching 20 years old and has a lot of code out there.
Let's see what we get with this prompt:
Write a Clojure function that takes a mapping argument and returns the mapping with a new pair added:
port: <number>
. If the mapping has ahost
key and the value islocalhost
, use a random port number in the range of 8000-8999. If the mapping has adriver
key and the value ispostgres
, use port 5432. Otherwise use port 0 (for unknown).
Here's the function:
(defn auto-port [mapping]
(if (= (get mapping "host") "localhost")
(assoc mapping "port" (+ 8000 (rand-int 1000)))
(if (= (get mapping "driver") "postgres")
(assoc mapping "port" 5432)
(assoc mapping "port" 0))))
The point of this post was to use Clojure functions in YAML. How do we do that?
YS has a !clj
tag that allows us to write raw Clojure code as strings in our
YAML files.
Let's put this all together in our config.yaml
file:
!YS-v0:
=>: !clj |
(defn auto-port [mapping]
(if (= (get mapping "host") "localhost")
(assoc mapping "port" (+ 8000 (rand-int 1000)))
(if (= (get mapping "driver") "postgres")
(assoc mapping "port" 5432)
(assoc mapping "port" 0))))
server: !:auto-port
host: localhost
port:
ssl: true
database: !:auto-port
driver: postgres
host: db.example.com
port:
name: myapp
credentials:
username: admin
password: s3cr3t
Before we try this out, let's look at what we did here.
- We start in data mode with the
!YS-v0
tag. - We use the
!clj
tag to define some code in raw Clojure. - Since we need a YAML pair here we use the dummy
=>
key. - We use a YAML
|
literal style scalar for the multi-line string. - We use the
!:<fn>
YS calling syntax to call theauto-port
function.- This saves us from indenting the mapping data.
- We put an empty
port
pair to keep the desired position for the port pair.
Now, let's load this:
$ ys -Y config.yaml
server:
host: localhost
port: 8643
ssl: true
database:
driver: postgres
host: db.example.com
port: 5432
name: myapp
credentials:
username: admin
password: s3cr3t
Perfect!
We were able to use AI to get something we wanted with minimal effort.
Back to the Future🔗
Assuming Clojure is the past and YS is the future...
Let's go Back to the Future!
Can we convert this Clojure code into YS?
If you have the time in your busy day, then absolutely!
Later on I'll show you a lot about how to go from Clojure back to YS, but for today I'll just do it for you:
Here's a direct translation:
!YS-v0
defn auto-port(mapping):
if mapping.host == 'localhost':
assoc mapping "port": 8000 + rand-int(1000)
if mapping.driver == 'postgres':
assoc mapping 'port': 5432
assoc mapping 'port': 0
--- !data
server: !:auto-port
host: localhost
port:
ssl: true
database: !:auto-port
driver: postgres
host: db.example.com
port:
name: myapp
credentials:
username: admin
password: s3cr3t
and:
$ ys -Y config.yaml
server:
host: localhost
port: 8151
ssl: true
database:
driver: postgres
host: db.example.com
port: 5432
name: myapp
credentials:
username: admin
password: s3cr3t
Voila!
Going all the way🔗
I know this post is already long, but the OCD in me wants to go all the way to how I'd probably do this for real.
Let's move this helper function into a separate YS module file called
helpers.ys
.
Also the AI didn't write the best idiomatic Clojure code. Let's tighten up the YS ported code a bit.
Here's config.yaml
:
!YS-v0:
:use helpers: :all
server: !:auto-port
host: localhost
port:
ssl: true
database: !:auto-port
driver: postgres
host: db.example.com
port:
name: myapp
credentials:
username: admin
password: s3cr3t
and helpers.ys
:
!YS-v0
ns: helpers
defn auto-port(mapping):
assoc mapping 'port':
cond:
mapping.host == 'localhost':
8000 + rand-int(1000)
mapping.driver == 'postgres': 5432
else: 0
and:
$ ys -Y config.yaml
server:
host: localhost
port: 8913
ssl: true
database:
driver: postgres
host: db.example.com
port: 5432
name: myapp
credentials:
username: admin
password: s3cr3t
Excellent!
Conclusion🔗
That was probably the longest post of the Summer of YS series so far.
But like I was telling you yesterday... Now that you know that YS is Clojure in disguise, the sky is the limit for the cool stuff I can show you.
If you are enjoying these posts, the please add a "Reaction" below.
And if you have comments or questions, just ask in the comment section there. I hope this post raised a lot of interesting possibilities in your mind. If that's the case, you must surely have questions!
Don't be shy!
YS is a new language. Don't be afraid to ask questions. If you don't get something, then it's almost certain that others don't get it either.
Just ask!!!