Fun FridaYS — Rosetta Code
It's Friday and I feel like having some fun. With YS, of course.
Rosetta Code
is a super fun site
that has over 1000 programming tasks that people solve in nearly 1000
programming languages (including YS
).
If you've never heard of it, you should check it out.
Let's solve a task in YS that hasn't been solved yet!
Rosetta Code Data🔗
Years ago I wrote a hack to scrape the Rosetta Code site and save all the
code into a Git repo
that you can
clone right now.
It currently has 121578 programs in it!!
Just now I wrote a little YS program
to pick a
random Clojure program that has between 10 and 20 lines of code.
Today we are going to take a random Clojure program and backport it to YS.
This is a really good way to learn YS in depth.
When we're done we'll also publish the new solution back to the Rosetta Code.
Let's get started!
The Set Up🔗
This part is simple. Just clone Rosetta Code Data and run the random-clojure-task script.
$ git clone https://github.com/acmeism/RosettaCodeData
$ cd RosettaCodeData
$ ys <(curl -s https://gist.githubusercontent.com/ingydotnet/23f597ea05f4b54c0c5bc1100a692ab8/raw/random-clojure-task)
File: Lang/Clojure/Sparkline-in-unicode/sparkline-in-unicode.clj
From: http://rosettacode.org/wiki/Sparkline_in_unicode#Clojure
(defn sparkline [nums]
(let [sparks "▁▂▃▄▅▆▇█"
high (apply max nums)
low (apply min nums)
spread (- high low)
quantize #(Math/round (* 7.0 (/ (- % low) spread)))]
(apply str (map #(nth sparks (quantize %)) nums))))
(defn spark [line]
(if line
(let [nums (read-string (str "[" line "]"))]
(println (sparkline nums))
(recur (read-line)))))
(spark (read-line))
That looks like a fun one!
We should be able to make this work in YS, with very little effort.
Remember the !clj
tag for raw Clojure code?
Let's write the initial YS program: sparkline-in-unicode.ys
.
!YS-v0
=>: !clj |
(defn sparkline [nums]
(let [sparks "▁▂▃▄▅▆▇█"
high (apply max nums)
low (apply min nums)
spread (- high low)
quantize #(Math/round (* 7.0 (/ (- % low) spread)))]
(apply str (map #(nth sparks (quantize %)) nums))))
(defn spark [line]
(if line
(let [nums (read-string (str "[" line "]"))]
(println (sparkline nums))
(recur (read-line)))))
(spark (read-line))
Lets run it like they do in the Rosetta Code url above
:
$ ys sparkline-in-unicode.ys <<<$'1 2 3 4 5 6 7 8 7 6 5 4 3 2 1\n1.5, 0.5 3.5, 2.5 5.5, 4.5 7.5, 6.5'
▁▂▃▄▅▆▇█▇▆▅▄▃▂▁
▂▁▄▃▆▅█▇
Very cool!
YS Refactoring🔗
Let's refactor this Clojure code step by step towards YS.
I like the YS examples on Rosetta Code to all have a main
function.
This lets them all be run in a consistent way.
Let's add a main
function to our YS program.
Let's do it by getting out of the clj block and adding main
in YS code:
!YS-v0
=>: !clj |
(defn sparkline [nums]
(let [sparks "▁▂▃▄▅▆▇█"
high (apply max nums)
low (apply min nums)
spread (- high low)
quantize #(Math/round (* 7.0 (/ (- % low) spread)))]
(apply str (map #(nth sparks (quantize %)) nums))))
(defn spark [line]
(if line
(let [nums (read-string (str "[" line "]"))]
(println (sparkline nums))
(recur (read-line)))))
defn main():
spark: read-line()
Run it:
$ ys sparkline-in-unicode.ys <<<$'1 2 3 4 5 6 7 8 7 6 5 4 3 2 1\n1.5, 0.5 3.5, 2.5 5.5, 4.5 7.5, 6.5'
▁▂▃▄▅▆▇█▇▆▅▄▃▂▁
▂▁▄▃▆▅█▇
Now let's use defn
for all the functions, but we'll keep the function bodies
in Clojure.
We can also move main
to the top, which I prefer when possible.
Note
In Clojure, if you want to call a function before it's defined, you need to
use (declare <name>)
before the call.
YS does this automatically for you, so you can define functions in the order
that you prefer.
!YS-v0
defn main():
spark: read-line()
defn sparkline(nums): !clj |
(let [sparks "▁▂▃▄▅▆▇█"
high (apply max nums)
low (apply min nums)
spread (- high low)
quantize #(Math/round (* 7.0 (/ (- % low) spread)))]
(apply str (map #(nth sparks (quantize %)) nums)))
defn spark(line): !clj |
(if line
(let [nums (read-string (str "[" line "]"))]
(println (sparkline nums))
(recur (read-line))))
We used YS to start the function definitions, but we kept the function bodies in
Clojure by using the !clj
tag.
Run it:
$ ys sparkline-in-unicode.ys <<<$'1 2 3 4 5 6 7 8 7 6 5 4 3 2 1\n1.5, 0.5 3.5, 2.5 5.5, 4.5 7.5, 6.5'
▁▂▃▄▅▆▇█▇▆▅▄▃▂▁
▂▁▄▃▆▅█▇
Let's lose the lets🔗
The let
function in Clojure is a =:
assignment in YS.
YS-v0
defn main():
spark: read-line()
defn sparkline(nums):
sparks =: "▁▂▃▄▅▆▇█"
high =: (apply max nums)
low =: (apply min nums)
spread =: (- high low)
quantize =: !clj |
#(Math/round (* 7.0 (/ (- % low) spread)))
=>: !clj |
(apply str (map #(nth sparks (quantize %)) nums))
defn spark(line):
when line:
nums =: (read-string (str "[" line "]"))
println: (sparkline nums)
recur: (read-line)
Now it's starting to look like YS!
I did leave a couple of !clj
expressions in there.
They define Clojure anonymous functions.
In clojure, you can use #(...)
to define an anonymous function and in YS you
use \(...)
.
In Clojure, %
is short for %1
(the first argument).
In YS, %
is a modulo operator, so we use %1
instead.
YS also lets you use _
for %1
in anonymous functions.
Let's convert the anonymous functions and get rid of the !clj
tags.
!YS-v0
defn main():
spark: read-line()
defn sparkline(nums):
sparks =: "▁▂▃▄▅▆▇█"
high =: (apply max nums)
low =: (apply min nums)
spread =: (- high low)
quantize =:
\(Math/round (* 7.0 (/ (- _ low) spread)))
apply str: (map \(nth sparks (quantize _)) nums)
defn spark(line):
when line:
nums =: (read-string (str "[" line "]"))
println: (sparkline nums)
recur: (read-line)
Time to run it:
$ ys sparkline-in-unicode.ys <<<$'1 2 3 4 5 6 7 8 7 6 5 4 3 2 1\n1.5, 0.5 3.5, 2.5 5.5, 4.5 7.5, 6.5'
▁▂▃▄▅▆▇█▇▆▅▄▃▂▁
▂▁▄▃▆▅█▇
Still working!
More Idiomatic YS🔗
Let's make this more idiomatic YS.
!YS-v0
defn main():
spark: read-line()
defn spark(line):
when line:
nums =: read-string((str '[' line ']'))
say: sparkline(nums)
recur: read-line()
defn sparkline(nums):
sparks =: '▁▂▃▄▅▆▇█'
high =: max(nums*)
low =: min(nums*)
spread =: high - low
quantize =:
\(round(7.0 * ((_ - low) / spread)))
apply str: nums.map(\(nth sparks quantize(_)))
We did a few things here:
- Reordered the function definitions to be top down
- Replaced
(x y z)
forms withx(y z)
- Replaced
(+ x y)
forms withx + y
- Replaced
println
withsay
- Replaced
(apply x y)
withx(y*)
- Changed
Math/round
toround
because YS makes all theMath
functions available without the namespace prefix
Run it:
$ ys sparkline-in-unicode.ys <<<$'1 2 3 4 5 6 7 8 7 6 5 4 3 2 1\n1.5, 0.5 3.5, 2.5 5.5, 4.5 7.5, 6.5'
▁▂▃▄▅▆▇█▇▆▅▄▃▂▁
▂▁▄▃▆▅█▇
More Code Improvements🔗
Just because we found an example of Clojure code on the Rosetta Code site, doesn't mean it's the best way to solve the problem.
I see several things that we could do better here.
!YS-v0
defn main():
spark: read-line()
defn spark(line):
when line:
nums =: line:words.map(N)
say: sparkline(nums)
recur: read-line()
defn sparkline(nums):
sparks =: '▁▂▃▄▅▆▇█'
high =: max(nums*)
low =: min(nums*)
spread =: high - low
quantize =:
\(round(sparks.#.-- * ((_ - low) / spread)))
apply str: nums.map(\(nth sparks quantize(_)))
The nums =: read-string((str '[' line ']'))
line is a silly way to split a
line into a list of numbers.
It's reformatting the line to be a Clojure vector syntax string and then calling
read-string
(Clojure's form of eval
) on it.
Let's just split the string into words and convert them to numbers.
We also have a hard-coded 7.0
.
I assume it's the length of the sparks
string minus one.
We can use .#
to get the length of the string and .--
to subtract one.
Run it:
$ ys sparkline-in-unicode.ys <<<$'1 2 3 4 5 6 7 8 7 6 5 4 3 2 1\n1.5 0.5 3.5 2.5 5.5 4.5 7.5 6.5'
▁▂▃▄▅▆▇█▇▆▅▄▃▂▁
▂▁▄▃▆▅█▇
Note that I removed the commas (they're WS anyway) from the input so that
words
would return the right thing.
Final Touches🔗
Let's do one more thing.
I don't think this code needs to read from stdin.
It's causing us to use read-line
and recur
in the spark
function.
Let's pass in the data as a text string to the program.
!YS-v0
defn main(input):
each line input:lines:
say: line:words.map(N):sparkline
defn sparkline(nums):
sparks =: '▁▂▃▄▅▆▇█'
high =: max(nums*)
low =: min(nums*)
spread =: high - low
quantize =:
\(round(sparks.#.-- * ((_ - low) / spread)))
apply str: nums.map(\(nth sparks quantize(_)))
Run with:
$ ys sparkline-in-unicode.ys $'1 2 3 4 5 6 7 8 7 6 5 4 3 2 1\n1.5 0.5 3.5 2.5 5.5 4.5 7.5 6.5'
▁▂▃▄▅▆▇█▇▆▅▄▃▂▁
▂▁▄▃▆▅█▇
We changed main
to expect a multi-line string as input.
Now we don't even need the extra spark
function.
That's pretty nice!
I just posted it to the Rosetta Code site
!
From whence we came🔗
Since YS compiles to Clojure, let's see what the Clojure version of our YS code looks like.
$ ys -c sparkline-in-unicode.ys | zprint
(declare sparkline)
(defn main
[input]
(each [line (lines input)] (say (sparkline (+map (words line) N)))))
(defn sparkline
[nums]
(let [sparks "▁▂▃▄▅▆▇█"
high (apply max nums)
low (apply min nums)
spread (sub+ high low)
quantize (fn [& [_1]]
(round (mul+ (ys.std/dec+ (clojure.core/count sparks))
(div+ (sub+ _1 low) spread))))]
(apply str (+map nums (fn [& [_1]] (nth sparks (quantize _1)))))))
(apply main ARGS)
There you go!
I hope you enjoyed this little journey.
You probably saw a lot of new YS things. I didn't have time to explain each one, but that's what this Summer is for.
Let me know what you think in the comments below!