Gleam: Raindrops

Problem on Exercism

pub fn convert(number: Int) -> String {
}

First Iteration: Pattern-matching

To start, I just wanted to solve the problem and decided to use pattern-matching.

pub fn convert(number: Int) -> String {
    case number % 3, number % 5, number % 7 {
        0, 0, 0 -> "PlingPlangPlong"
        0, 0, _ -> "PlingPlang"
        0, _, 0 -> "PlingPlong"
        _, 0, 0 -> "PlangPlong"
        0, _, _ -> "Pling"
        _, 0, _ -> "Plang"
        _, _, 0 -> "Plong"
        _, _, _ -> int.to_string(number)
    }
}

While we have three values to divide by, there's only two states for either resulting value that we care for: remainder and no remainder. If there is no remainder, we have a combination of strings to return, based on which values do not result in a remainder. If there were more rules or more complex strings to return, this method is very inefficient, it's hard-coded.

Second Iteration: Piping

Since the previous solution relied on accounting for every required possibility, I wanted to solve it, such that only the remainder state matters, not the combination thereof.

pub fn convert(number: Int) -> String {
    let append_or_not = fn(rem: Int, res: String, s: String) {
        case rem {
            0 -> res <> s
            _ -> res
        }
    }

    let result = append_or_not(number % 3, "", "Pling")
    |> append_or_not(number % 5, _, "Plang")
    |> append_or_not(number % 7, _, "Plong")

    case result {
        "" -> int.to_string(number)
        _ -> result
    }
}

For this, the function append_or_not receives a remainder, current result, and the string to append if there is no remainder. So the input is piped through this function three times for each of the rules, but while the order does not matter here, to add a new rule would require adding a new line and it would break if the rule is not related to remainders, or multiples of a number.

Third Iteration: Tuples

I wanted a solution that doesn't require changing the code to work. In the last two iterations, the first would require changing the entire case block, while the second would require a new line of code addressing the new condition. Looking at others' solutions, I saw the tuples.

const sounds = [
    #(3, "Pling"),
    #(5, "Plang"),
    #(7, "Plong")
]

fn sfx(n: Int, s: #(Int, String)) {
    case n % s.0 {
        0 -> s.1
        _ -> ""
    }
}

pub fn convert(number: Int) -> String {
    let result = list.map(sounds, sfx(number, _))
    |> string.concat
    case result {
        "" -> int.to_string(number)
        _ -> result
    }
}

I didn't have to declare the sfx function outside of convert here, doing as I did in my second iteration.

Here, the rules are stored as a list of tuples, where the first value of the tuple is the divisor and the second value is the sound effect to append. So convert takes this list and iterates over it with the function sfx, and since it's mapping, a new list is produced with the results of dividing the number by each divisor and returning the corresponding string. Then it's just a matter of concatenating the strings.