Swift: Using String Ranges The Functional Way

A few weeks ago, I wrote about How To Find A Substring In Range of a Swift String. At the time, it seemed very odd why Ranges were so hard to work with in Swift, but at least I found a way to work around them…

However, as I’ve been learning a lot more about Functional Programming in the past few weeks, I’ve come to realize that Ranges are created this way to help guide us to use them in a more specific way – particularly the more functional way.

One of the very first thing I learned from watching the edX FP101x lectures was the concept of head and tail. In fact, this is mentioned 5 minutes into the very first lecture, and the concept comes up over and over again! The idea is very simple. The head of a list is it’s first element, and the tail is an list of all elements except the first one (so basically it drops the first element from the list).

head[1,2,3,4,5]
// 1

tail[1,2,3,4,5]
// [2,3,4,5]

In functional programming, instead of using the typical for-loops to solve problems, the use of head, tail, and recursion is favored to deal with elements in a list one at a time.

So let’s say you want to get the first x number of characters from a word. Without thinking of Swift Ranges (and they’re hard to work with anyway!), you might initially approach it something like this:

func getSubstringUpToIndex(index: Int,
    fromString str: String) -> String
{
    var substring = ""
    
    for (i, letter) in enumerate(str) {
        
        substring.append(letter)
        
        if i == index - 1 {
            break
        }
    }
    
    return substring
}

getSubstringUpToIndex(5, fromString: "Hello, NatashaTheRobot")
// Hello

Now let’s do the same thing the functional way – with head, tail, and recursion:

func getSubstringUpToIndex(index: Int,
    fromString str: String) -> String
{
    let (head, tail) = (str[str.startIndex], dropFirst(str))
    
    if index == 1 {
        return String(head)
    }
    
    return String(head) +
        getSubstringUpToIndex(index - 1, fromString: tail)
}

getSubstringUpToIndex(5, fromString: "Hello, NatashaTheRobot")
// Hello

So hopefully now it makes sense why Swift Ranges have a startIndex and endIndex available with no easy way to go in between!

Of course in this case you can solve this problem the way I mentioned in my initial post about Swift Ranges, but in solving it this way I’m hoping you (and I!) can start thinking a lot more in this functional way of going through the list one at a time in favor of for loops. After all, this focus on first and last (with no easy way to go in between) is apparent in many other Swift APIs…

Enjoy the article? Join over 14,500+ Swift developers and enthusiasts who get my weekly updates.

  • sdrpa

    Great post, found it while learning haskell and trying to implement some of the haskell functions in swift using pure functions

    getSubstringUpToIndex(index: Int, fromString str: String) -> String will throw fatal error: Can’t form a Character from an empty String if empty string is passed as param, while haskell: take 3 [] will return just empty array

    Here’s variation which will return empty string for out of bounds params:

    func take(n: Int, str: String) -> String {
    if n >= countElements(str) {
    return String()
    }
    let head = String(str[str.startIndex])
    return head + take(n + 1, dropFirst(str))
    }

    • sdrpa

      The above is wrong 🙁 Still fighting with recursion. Corrected:

      func take(n: Int, str: String) -> String {
      if n >= countElements(str) { return str }
      return take(n, dropLast(str))
      }

  • Pingback: Swift: functional subscripting — Erica Sadun()