Composing a function of binary arity

July 10, 2017

Way back when, also known as 28th June 2017, I went to a London Clojurians coding dojo meetup. Wherein one of the ideas floated for small dojo projects was to code some cyphers, inspired by Wonderland Clojure katas.

Very well. My group started with a simple ROT. At which point I got an urge to split the character traversal algorithm from the actual operation performed on characters, hereafter referred to as f, because we're in Clojure-land and the Clojure way is to be wonderfully terse when naming function arguments. (Whereas I can be wonderfully terse about the benefits of having readable code. But since I don't have a drink at hand, let's leave the rant for some other day and continue.)

Anyway! Your quest, should you choose to accept it, is this: convert each character into an integer, pass it to an arbitrary function, then convert the result back into a character.

Great! This works:

; This is Klipse, so we're in ClojureScript land,
; in which there isn't a "character" type
; so inbuilt (int) behaves differently than in Clojure.
; This re-definition fixes the problem.
(defn int [c] (.charCodeAt c))

(defn convert [f s]
  (apply str
         (map (comp char f int) s)))

(defn dumb-rot13 [c]
  (let [offset (int \a)]
    (-> c (- offset) (+ 13) (rem 26) (+ offset))))

(convert dumb-rot13 "puvpxra")

Let's level up, shall we?

The new task is to convert each character into an integer, pass it to an arbitrary function along with its index in the original string, and convert the result back into a character. Perhaps we only want to encode every other character.

f would now be a function of two arguments, aka binary arity, so we would use map-indexed. The problem is that char and int should only operate on the value, not on the index.

So this doesn't work:

(defn convert [f s]
  (apply str
         (map-indexed (comp char f int) s)))

I can do it with threading. Except as it happens, I'm trying to get more practice with comping. But I will write out the threading version anyway (this is what I went with during the dojo) because, as it turns out...

(defn convert-indexed [f s]
    (map int)
    ; 0-indexed, where 0 is considered even
    (map-indexed f)
    (map char)
    (apply str)))

(defn rot13-odd [i c]
  (if (odd? i) (dumb-rot13 c) c))

(convert-indexed rot13-odd "puvpxra")'s easy to convert the above into exactly what I was looking for.

In short, you do not comp the functions directly, but you use transducers based off appropriate maps, in the same order as in the threading macro above. Which thus converts the binary arity function f into a unary arity transducing function (map-indexed f) accepting a collection. Yay!

(defn convert-compd [f s]
      (map int)
      (map-indexed f)
      (map char))

(convert-compd rot13-odd "puvpxra")

Props to Nathell for kicking the idea around with me until I got to the final form :)

For the curious: why am I practicing comping? For performance reasons. Compared to a threading macro, comp'd transducers do not create a ton of temporary collections for holding intermediary results.

Tags: clojure hack