February 2, 2024

Haskellbrain: Week 3 of 48 in 24

Well, I’m quite late with this week’s challenges. Again, I was only going for the silver medal of doing any three languages, but I didn’t have a lot of time to spare for the challenges last week, and I also spent more time than I should have tweaking my haskell solution in particular.

This week’s exercise is fizzbuzz, but slightly different.

JavaScript

First up, here’s my javascript solution.

export const convert = (num) => {
  let s =  [[3, "Pling"], [5,"Plang"], [7,"Plong"]]
      .map(([x,y]) => num % x === 0 ? y : "")
      .join("");
  return s === "" ? num.toString() : s;
};

The basic idea is to take a list of pairs (a divisor and a string) and map that pair to the string if $n$ is divisible by the divisor, or map to the empty string otherwise. We then join those strings together. The final wrinkle is to check if the string we have left over is is still empty, and if it is, return the number as a string, rather than the resultant string.

It’s kind of terse, but none of the components are that mysterious. We’ve got the ternary operator p ? x : y which evaluates to x if p is true and y otherwise. So z = p ? x : y is just a more compact way of doing:

let z;
if (p) {
    z = x;
} else {
    z = y;
}

We’ve got map which takes an array and puts each element through the provided function: a.map(f) is just a more compact way of doing:

let z = []
for (let x of a) {
    z.push(f(x));
}

Finally, we’ve got an anonymous function which is just a quick way of defining a function that isn’t worth writing out using function. So instead of writing ([x,y]) => num % x === 0 ? y : "" as the argument to map, we could have defined the function first:

function f([x,y]) {
    return num % x === 0 ? y : "";
}

num, here, is the input to convert and if we’re defining this function inside convert we can effectively treat num as a constant.

It took a little while (and a little complaining on social media) to get the argument to this function right. But that’s a topic for another day.

Clojure

Next up we have Clojure.

(ns raindrops)
(defn p [d inString]
  (fn [[accString n]]
    (if (= 0 (mod n d)) 
      [(str accString inString) n] 
      [accString n]
      )
    )
  )
(def plong (p 7 "Plong"))
(def plang (p 5 "Plang"))
(def pling (p 3 "Pling"))
(defn convert [n] ;; <- arglist goes here
   (def plingString (first (plong (plang (pling ["" n])))))
  (if (= 0 (count plingString)) (str n) plingString)
  )

So the first thing I’m doing here is creating a function that creates functions. It takes two arguments, a divisor d and a string inString, and it outputs a function that takes a list of a string and a number [accString n]. The output function’s number argument n is the input to convert and the string is the accumulation of all the “Pling"s etc that have been found so far accString. The function checks if the number n is divisible d, and if it is, appends inString to accString and returns the list of that string, plus the n that was the input. So the input and output types for this function are the same. I then create functions pling, plang, and plong which check for the relevant divisibility property. Each of these outputs something that can be consume as the input to the others, so I can then just compose them. Each function passes the input to convert through to its output so that when they’re composed, each function knows what number they’re converting. So (plong (plang (pling ["" 21]))) becomes:

(plong (plang ["Pling" 21]))
(plong ["Pling" 21])
["PlingPlong" 21]

Then all we need to do is take the first element of this pair, and check if it’s "". If not, no problem, output the string. If it is, then output the number as a string instead. There’s probably a better way to check for an empty string, but I was short of time.

Haskell

Both of these solutions rely on using "" as an indicator that none of 3, 5 or 7 divide the input number. This is fine, but you can imagine that maybe you want to add a rule that says “If the number is divisible by 11 and nothing else, output the empty string”. Now you could easily add [11, ""] to the list in the javascript solution, but convert(11) would output 11, rather than "" because it’s checking for the empty string. You could get around this, but it’s probably going to be a little awkward. The same is true for the clojure approach. For my haskell solution, I decided to try to handle this possibility a little more gracefully using Maybe.

module Raindrops (convert) where
import Data.Maybe

convert :: Int -> String
convert n = if null plingList then show n else concat plingList
  where plingList :: [String]
        plingList = catMaybes $ map ($ n) plingFunctions
        plingFunctions :: [Int -> Maybe String]
        plingFunctions = map singleConvert [(3,"Pling"), (5,"Plang"), (7,"Plong")]

singleConvert :: (Int, String) -> Int -> Maybe String
singleConvert (d,s) n
  | mod n d == 0 = Just s
  | otherwise = Nothing

Haskell has this useful type Maybe String where the value is either a string wrapped in Just or the value is Nothing. So singleConvert takes a pair of (d,s) and another value n and outputs Nothing if n is not evenly divisible by d, and Just s if it is. This will be useful later. Bear with me.

So we have defined this function singleConvert that takes (d,s) and n as its two parameters. But we’re actually going to be using this for creating functions that take an Int as input and output a Maybe String. So we are going to partially apply singleConvert to get those functions. This is a neat, if initially mind-bending, feature of haskell. If you have a function f :: Int -> Int -> Int we read that as f takes two arguments (both Ints) and outputs an Int. But we could instead read this as f takes one argument, an Int and outputs a function from Ints to Ints. Both of these things are true! (If anything, the latter is more true). So that’s what we do. We map singleConvert over a list of pairs (d,s) to produce a list of functions called plingFunctions. The type of the functions in this list is Int -> Maybe String. Now we use another related kind of mind-bending haskell-ism: map ($ n) plingFunctions applies each function in plingFunctions to input n. f $ n in haskell means apply function f to argument n, and is basically the same as f n. But, using $ means we can use $ n as a function, the function that applies n to its input (which has to be a function that takes n’s type as its input’).

An intermediate step in the javascript approach was a list that looked like, say, ["Pling", "", "Plong"]. This is the output of the map step. We then join those together, relying on the fact that "" is the identity for string concatenation, meaning that s + "" == s. So any "" entries are effectively ignored. We get to a similar intermediate step in this haskell solution here, where we get something that looks like: [Just "Pling", Nothing, Just "Plong"]. That’s what we get from mapping the “apply n to it” function over our list of functions plingFunctions. Then we need to get only the Just elements, extract them from their Just contexts and concatenate them together. That’s precisely what catMaybes does; we go from a list of Maybe Strings to a list of the contents of all the Just elements. Our above example becomes ["Pling", "Plong"]. And we’re nearly done. If the list is empty – null plingList – then just output the number – show n turns an integer to a string – otherwise smoosh the members of the list together – that’s what concat does – and return that. The neat thing about this is that if we added (11, "") to the inputs, it would work as expected. Because this haskell solution can distinguish between Nothing and Just "".

© Seamus Bradley 2021–3

Powered by Hugo & Kiss.