Evolving a String Extension using Functional Programming in Swift

Subscribe to my newsletter and never miss my upcoming articles

Hallo tovenaars en heksen, Leo here.

Today we will explore some curious things that you can do to make your life easier with functional programming. The goal is to make your code describe what is it doing instead of have to figure it out. Functional programming is a great tool to make your code more expressive and more assertive.

Let's code!

The Painting

The majority of the paintings here I choose from dead artists so we can celebrate the past painters but today we are appreciating a living painter work. The painting I choose was a "Most Beautiful and most wonderful" from Richard Whincop. He lives and works in Chichester, UK. Richard is Inspired by the Renaissance ideal of an artist who can move freely between disciplines, and there is a rich cross-fertilisation between in his work between art, design, writing, lecturing, music and theatre.

You can see more of his work in his Instagram. We all could send him a Hello to appreciate his work, it's very rare the opportunity to talk to the artist himself.

And I choose this painting because it portrays Charles Darwin. And the topic of this post is about evolution so...

The Problem

You have to make a String extension that returns the string with only numbers or nil if there's no number at all.

This problem is quite trivial when you don't use functional programming. You can solve using one for and a ternary-if like the example below:

extension String {

    func onlyNumbers() -> String? {

        var onlyNumberString: String? = nil

        for char in self where Int("\(char)") != nil { // mark 1
            onlyNumberString == nil ? ( onlyNumberString = "\(char)") : (onlyNumberString! += "\(char)") //mark 2
        }

        return onlyNumberString

    }
}

Breaking down the algorithm this:

And you can test it with:

  1. On mark 1 the for is using the where keyword to filter only chars that can be converted to Int.
  2. On mark two we first check if the var is Nil or not. If is you add a implicitly String to it, if isn't nil we can safe force unwrap it to append the digit.
let a1 = "23a23(as 1111 ㊈asss 03238".onlyNumbers()
let a2 = "a".onlyNumbers()

print(a1)
print(a2)

Screen Shot 2021-06-03 at 16.42.12.png

May the Functional Power be with you

But we can do it with functional programming.

The first approach I mande was this:

extension String {

    func onlyNumbers() -> String? {
        let onlyNumberString = self.compactMap {Int("\($0)")}.map {"\($0)"}.joined() // mark 1
        return onlyNumberString.isEmpty ? nil : onlyNumberString // mark 2
    }

}

This is pretty easy to read.

On mark 1 you first are making a [Int] integer array with the compactMap. This compactMap is important because it already filter the characters that aren't convertible to Int excluding the nil ones. After that I just map the integer array to an array of Strings with the second map and finally use Joined to glue everything into a single String. The difference between Map and CompactMap operators is that compactMap doesn't return nil values.

The mark 2 I check if the result of mark 1 is empty to return nil and if isn't return it.

If you pay attention Swift wrap the result of the mark in a Optional automatically in the end. Really cool right?

But... I wasn't satisfied with that because I don't want to have a variable just to keep the value of mappings.

So discussing with my dear friend Tales, he suggested that I could use the filter operator to do the magic. And yeah kinda worked...:

extension String {

    func onlyNumbers() -> String? {

        filter {Int("\($0)") != nil}

    }
}

The only catch was it didn't return a nil anymore. As shown below:

let a1 = "23a23(as 1111 ㊈asss 03238a".onlyNumbers()
let a2 = "a".onlyNumbers()

print(a1)
print(a2)

Screen Shot 2021-06-03 at 17.05.19.png

And I didn't like the design of this API. I don't want an empty String Optional, I want a nil value here.

To fix this "problem" I had an idea. With the filter result we could reduce it to a Optional. Let's try that....

extension String {

    func onlyNumbers() -> String? {

        filter {Int("\($0)") != nil} // mark 1
        .reduce(nil, { optionalString, char in
            optionalString == nil ? String(char) : optionalString?.appending(String(char))        
    }
}

Yes! Finally! The algorithm is this:

  1. First on mark 1 we filter everything to be an array of Integers. This is very handy as it doesn't change the type like when we were doing with the mapping operations.
  2. This is where the magic happens and there's too many things happening here. The function first parameter is the initial value of what we are reducing. The magic Swift compiler can infer that the instruction below returns two types the String and the nil because the func returns a String? (Optional) . So we can use a ternary operator to check if the optionalString is nil, if is just return a String that compiler will magically wrap it for us and if isn't nil just append the char to the Optional.

I think this is the best solution I can get. You can still make it with just a reduce like the example below:

extension String {

    func onlyNumbers() -> String? {

        reduce(nil, { optionalString, char in
            return String(char).range(of: "\\d", options: .regularExpression) != nil ?
                (optionalString == nil ? String(char) : optionalString?.appending(String(char))) :
                optionalString

        })
    }
}

But we will get an error that everyone will eventually get:

error: the compiler is unable to type-check this expression in reasonable time; try breaking up the expression into distinct sub-expressions

And you will need to refactor to this:

extension String {

    func onlyNumbers() -> String? {

        reduce(nil, { optionalString, char in
            let isDigit = String(char).range(of: "\\d", options: .regularExpression) != nil
            return isDigit ?
                (optionalString == nil ? String(char) : optionalString?.appending(String(char))) :
                optionalString

        })
    }
}

But again I don't think this would be the best solution for the case. I think the solution with the filter + reduce it's the middle between too deep into FP and too shallow.

Conclusion

Today we could study some functional programming API with a very simple problem. Using filters, mapping, compactMap and reduce. I hope you enjoy and I'm excited to hear from you what can be improved in this solution? There's an easier way to do this?

That's all my people, I hope you liked this as I liked writing. If you want to support this blog you can Buy Me a Coffee or just leave a comment saying hello.

Thanks for the reading and... That's all folks.

credit: image

Interested in reading more such articles from Leonardo Maia Pugliese?

Support the author by donating an amount of your choice.

Comments (3)

Alessandro Martin's photo

What about

var digits: String {
    String(unicodeScalars.filter(CharacterSet.decimalDigits.contains))
}
Leonardo Maia Pugliese's photo

Works pretty well, I'll just change to "String?" and add a isEmpty check in the end to make it optional but this is awesome solution! Very very clever my friend.

Alessandro Martin's photo

Leonardo Maia Pugliese My pleasure! I'm glad you like it!