Evolving a String Extension using Functional Programming in Swift

Evolving a String Extension using Functional Programming tutorial

Hallo tovenaars en heksen, Leo here. The topic today is making a String Extension using Functional Programming in Swift.

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 having to figure it out. Functional programming is a great tool to make your code more expressive and more assertive.

Let’s code! But first…

 

Painting of The Day

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’s work. The painting I choose was “Most Beautiful and most wonderful” by 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-fertilization between his work and between art, design, writing, lecturing, music, and theatre.

You can see more of his work on 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 – String Extension using Functional Programming in Swift

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 an 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 we have:

  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 you add an implicitly String to it, if isn’t nil we can safely force unwrap it to append the digit.

And you can test it with:

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

print(a1)
print(a2) 

Resulting in:

First example of string functional programming in Swift

 

May the Functional Power be with you

But we can do it with functional programming.

The first approach I made 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 an [Int] integer array with the compactMap. This compactMap is important because it already filters 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 wraps the result of the mark in an Optional<String> 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 nil anymore. As shown below:

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

print(a1)
print(a2)

Resulting in:

Second result of string functional programming in Swift image

That led to a problem where I could have an optional value with an empty string in it.

 

Solving the Optional Empty String problem

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 an Optional<String>. 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 are 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<String>). So we can use a ternary operator to check if the optionalString is nil, if yes, it just returns a String the compiler will magically wrap it for us and if isn’t nil just append the char to the Optional<String>.

I think that was the best solution I could get.

You can still make it with just a reduce function 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.

And this is the end!

 

Summary – String Extension using Functional Programming

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

That’s all my people, I hope you liked reading this article as much as I enjoyed writing it. If you want to support this blog you can Buy Me a Coffee or leave a comment saying hello. You can also sponsor posts and I’m open to freelance writing! You can reach me on LinkedIn or Twitter and send me an e-mail through the contact page.

Thanks for reading and… That’s all folks.

Credits: image

Share this post:

Related posts

Sponsor