Fork me on GitHub
#clojuredesign-podcast
<
2020-09-02
>
Stefan07:09:48

Hi all! I’m listening to #76 at the moment, and you just said something like: if you’re missing some function in core, it’s probably because you don’t know about an idiom that you need to learn. I have a question 🙂 What about unfold ? I’ve used it a few times through flatland.useful, but is there an idiom that I’m missing that allows me to easily do it with core functions? Please note that unfold has termination built into it, which is an important feature. If you want an example, you could imagine a function that takes a euro amount of change that needs to be handed out by a shop employee, and returns the list of coins that makes up the change. So it’s the opposite of reduce, in the way that it takes a single value and creates a list of values from it.

nate15:09:57

Very interesting. I've never used unfold before. Can you give a few other examples of how it would be used?

Stefan19:09:05

Well, until recently my FP was mostly “playing around”, but I used it on Exercism for converting a number (single value) into roman numeral representation (list of characters):

(ns roman-numerals
  (:require [flatland.useful.seq :refer [unfold]]))

(def values {1000 "M"
             900 "CM"
             500 "D"
             400 "CD"
             100 "C"
             90 "XC"
             50 "L"
             40 "XL"
             10 "X"
             9 "IX"
             5 "V"
             4 "IV"
             1 "I"})

(defn lookupLE [v m]
  (let [ks (reverse (sort (keys m)))
        highest (first (filter #(<= % v) ks))]
    [highest (values highest)]))

(defn select-greatest [n]
  (when (> n 0)
    (let [[val numeral] (lookupLE n values)]
      [numeral (- n val)])))

(defn numerals [n]
  (apply str (unfold select-greatest n)))
And for the ISBN verifier:
(ns isbn-verifier
  (:require [flatland.useful.seq :refer [unfold]]))

(defn isbn-digit? [c]
  (or (= \X c)
      (java.lang.Character/isDigit c)))

(defn isbn->digits [isbn]
  (letfn [(characters-to-numbers [s]
            (when-let [char (first s)]
              (if (= \X char)
                [10 (next s)]
                [(java.lang.Character/digit char 10) (next s)])))]
    (->> isbn
         (filter isbn-digit?)
         (unfold characters-to-numbers))))

(defn mod-11 [n] (mod n 11))

(defn isbn? [isbn]
  (let [digits (isbn->digits isbn)]
    (and (= 10 (count digits))
         (not-any? #(= 10 %) (butlast digits))
         (= 0 (->> digits
                   (map * (range 10 0 -1))
                   (reduce +)
                   (mod-11))))))
No real-world examples yet, but I’m pretty sure they will arise…

genekim18:07:36

Interesting! I had a bunch of interactions with some folks on exactly this on Twitter: https://twitter.com/RealGeneKim/status/1412277787679891459 In short, calling an API with “next-token” semantics is amenable to unfold. Which apparently may be added to clojure.core — how long this is taking and the thought being put into this is a fascinating glimpse into what it takes to get into core. 🙂

lodin14:09:27

@stefan.van.den.oord I have used iterate, take-while, and map, using a tuple holding the result and state. But yeah, that is too long to not be a function.

👍 3