Skip to content

Fun FridaYS — Rosetta Code

It's Friday and I feel like having some fun. With YS, of course.

Rosetta Code External link is a super fun site that has over 1000 programming tasks that people solve in nearly 1000 programming languages (including YS External link ). 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 External link that you can clone right now. It currently has 121578 programs in it!!

Just now I wrote a little YS program External link 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 External link :

$ 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 with x(y z)
  • Replaced (+ x y) forms with x + y
  • Replaced println with say
  • Replaced (apply x y) with x(y*)
  • Changed Math/round to round because YS makes all the Math 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 External link !

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!

Comments