Why Swift Sucks as a Functional Language

Published: (March 8, 2026 at 01:09 PM EDT)
8 min read
Source: Dev.to

Source: Dev.to

Introduction

Some months ago, I became one of the many victims of a dreaded massive layoff.
Due to personal issues — among them caring for my father after he fell ill with COVID — I’m still searching for a job.

In this search, I’ve encountered a trend that’s relatively new to me: the rise of coding tests. I studied data structures and algorithms during my degree, but that was more years ago than I’m comfortable admitting. Years of just storing data in databases and retrieving it to display on a screen haven’t exactly kept those skills sharp.

In my last set of interviews, I failed more often than I’d like to admit. Thankfully, I came across a kind team lead (yes, they exist!) who gave me valuable feedback on my performance. With his guidance, I’ve developed a plan to succeed in future interviews. But that’s a topic for another post.


The Problem

One of the coding problems I encountered was to find a local maxima in an array of integers.

  • A local maxima is a value greater than its neighbours, considering only one neighbour for the first and last elements.
  • The input array had an additional constraint: you wouldn’t find any repeated values.

The key insight needed to properly solve this problem (which I failed to find without a bit of help due to a severe case of rustiness in some parts of my brain) is this:

If you select any value which isn’t a local maxima, you are guaranteed to find one on the side of a neighbour that’s greater than the current value.

Example

let values = [5, 15, 1, 6, 8, 9, 7]

If we pick the middle position (6), this isn’t a local maxima, since 6

Int? {
    guard numbers.count > 1 else { return numbers.first }

    var left = 0
    var right = numbers.count - 1

    while left < right {
        let mid = (left + right) / 2

        if (mid == 0 || numbers[mid] > numbers[mid - 1]) &&
           (mid == numbers.count - 1 || numbers[mid] > numbers[mid + 1]) {
            return numbers[mid]
        }

        // If left neighbour is greater, search the left half
        if mid > 0 && numbers[mid - 1] > numbers[mid] {
            right = mid - 1
        } else {
            // Otherwise, search the right half
            left = mid + 1
        }
    }

    return nil
}

Functional Scala Solution

import scala.annotation.tailrec
import scala.collection.IndexedSeqView

def findAnyLocalMaxima(numbers: Array[Int]): Option[Int] =
  extension (numbers: IndexedSeqView[Int])
    def hasLocalMaximaAt(index: Int) =
      numbers(index) > numbers(index - 1) && numbers(index) > numbers(index + 1)

    def leftHalf = numbers.slice(0, numbers.length / 2)
    def rightHalf = numbers.slice(numbers.length / 2 + 1, numbers.length)

  @tailrec
  def findAnyLocalMaximaInView(numbers: IndexedSeqView[Int]): Option[Int] =
    numbers.length match
      case length if length > 2 =>
        val middleIndex = length / 2
        if numbers.hasLocalMaximaAt(middleIndex) then
          Some(numbers(middleIndex))
        else if numbers(middleIndex - 1) > numbers(middleIndex) then
          findAnyLocalMaximaInView(numbers.leftHalf)
        else
          findAnyLocalMaximaInView(numbers.rightHalf)

      case 2 => Some(math.max(numbers(0), numbers(1)))
      case 1 => Some(numbers(0))
      case 0 => None
  end findAnyLocalMaximaInView

  findAnyLocalMaximaInView(numbers.view)
end findAnyLocalMaxima

Ok, I admit it, I got a bit fancy with that extension to make the main code even more readable.


Functional Programming Thoughts

Functional programming emphasises referential transparency — the ability to replace a function call with its result without changing the program’s behaviour, as long as you use pure functions. This aligns with recursive approaches, where you rewrite the problem into smaller cases to reach a base case. As a result, functional programming favours recursive calls instead of loops, because that way you express much better the intent of your code.

If you inspect the above functional code, the algorithm’s intent is crystal clear:

  1. Check if the middle value is a local maxima.
  2. If not, recurse into the appropriate half.

That intention is totally lost in the imperative version, which relies on side effects to adjust indices to inspect the data you are interested in. You even require comments explaining the logic!


The Problem with Going Fully Functional in Swift

In two words: stack overflows!

I bet you have faced at some point a stack overflow exception. This happens because whenever you invoke a new function you push a frame on the stack containing information about that call. A naïve recursive implementation of the divide‑and‑conquer algorithm can therefore blow the stack for large inputs, whereas the iterative Swift version safely stays within the stack limits.


Conclusion

Both solutions solve the same problem, but they illustrate a classic trade‑off:

AspectImperative SwiftFunctional Scala
ReadabilityRequires comments to convey intentIntent expressed directly by recursion
PerformanceO(log n) time, O(1) spaceO(log n) time, O(log n) space (call stack)
SafetyNo risk of stack overflowPotential stack overflow for very large arrays
Language FitNatural for Swift’s mutable styleNatural for Scala’s functional style

If you need a guaranteed‑no‑stack‑overflow solution in a functional language, you can always fall back to an explicit stack or tail‑recursion optimisation (when the compiler supports it). Conversely, if you prefer the elegance of recursion and your inputs are modest, the functional version is a beautiful demonstration of the algorithm’s core insight.

Stack Overflow and Tail‑Call Optimisation

When a function calls itself recursively, each call needs a new stack frame.
If you nest too many calls, the stack runs out of space and you get the dreaded stack overflow exception.

Why Scala Doesn’t Have This Problem

Scala is a “proper” functional language in this respect.
Functional programming relies heavily on recursion instead of loops, so tail‑call optimisation (TCO) is a must.

  • With TCO, if the last operation of a function is a call to itself, the current stack frame is reused instead of creating a new one.
  • The @tailrec annotation (see findAnyLocalMaximaInView) tells the Scala compiler to apply this optimisation, allowing potentially infinite recursive calls without risking a stack overflow.

Swift and Tail Calls

Swift sometimes optimises tail calls, but the behaviour is not guaranteed.

  • Debug builds (-Onone) often omit TCO, leading to stack overflows.
  • Even with optimisation flags -Osize or -O, there is no guarantee that tail calls will be optimised.
    • The Swift documentation’s Writing High‑Performance Swift Code article does not mention TCO.
    • An old Swift Forum proposal (which referenced Scala’s @tailrec) was closed without implementation.

Bottom line: Using tail recursion in Swift is a gamble—sometimes it works, sometimes it doesn’t. If you rely on it, you’re taking a risk.


Space‑Complexity Concerns

Coding tests evaluate both time and space complexity:

  • How much extra space does your algorithm need?
  • How does that space grow with the input size? (constant, linear, logarithmic, quadratic?)

If a functional approach uses excessive extra space, it may be less attractive than an imperative, mutable solution.

Immutability and Intermediate Structures

Functional programming typically works with immutable data structures, transforming them through pipelines.
Each transformation can create intermediate structures that are discarded after use.

For divide‑and‑conquer recursion:

  1. The input is split into roughly half‑size chunks until a base case is reached.
  2. The solution is then built back up from the bottom.

If we naïvely allocate a new array for each split, the total extra space becomes O(n) (linear in the input size).


How Scala and Swift Avoid Excessive Allocation

Swift – ArraySlice

Swift’s Array operations like prefix(_:) or subscript with a Range return ArraySlice (or Slice).
These are views that wrap the original collection:

  • No additional memory is allocated for the elements.
  • Access is O(1) in both time and space.

Scala – Views

Scala provides a similar mechanism via views:

val view = numbers.view   // creates a view over the underlying collection
  • Operations on a view (e.g., IndexedSeqView) are performed without allocating new structures.
  • You can slice these views indefinitely while staying O(1) in space.

TL;DR

  • The title may be click‑bait, but the discussion highlights why Swift still lags behind as a “real” functional language.
  • If you’ve read this far, you probably agree that the functional approach:
    • Expresses the algorithm more clearly, or
    • Shows that Swift’s tail‑recursion optimisation is unreliable.

Takeaways

  1. Scala: @tailrec + guaranteed TCO → safe recursive algorithms.
  2. Swift: Tail‑call optimisation is optional → use with caution.
  3. Both languages provide zero‑copy views (ArraySlice / view) to keep space complexity linear or better.

If you know more about Swift’s tail‑recursion optimisation, drop a comment—I’d love to hear about it!

0 views
Back to Blog

Related posts

Read more »

Sir Tony Hoare has died

Annonce Jonathan Bowen m'a appris le décès de Tony Hoare jeudi 5 mars. Tony Hoare – Wikipediahttps://en.wikipedia.org/wiki/Tony_Hoare Œuvres de Tony Hoare - Da...