1. Passing functions into your own functions
When you first started programming, a function seemed like some kind of button: you pressed it, and it ran. But in Swift (as in many modern languages), a function can also be treated as an ordinary value, almost like a number or a string. This dramatically increases code expressiveness: you start passing “rules” to places where you used to copy repetitive loops and if statements.
Imagine that we have a list of books, and sometimes we search by word, sometimes we show only short titles, sometimes we print them “nicely,” and sometimes “as is.” If you write a new loop every time, the code grows like a snowman in January. But if we pass a function that says “how to process an element,” we write one universal template and plug in different rules.
Up to this point, you mostly passed closures into ready-made methods from the standard library: map, filter, compactMap, reduce. Now we will take a step toward maturity: we will write our own function that accepts a closure and calls it. Only a little more advanced. This is the moment when you start to feel that the standard library is not magic, but a set of very smart, yet perfectly repeatable ideas.
Function type in Swift: (A) -> B
In short, a function type in Swift looks like this: “takes A, returns B,” and is written as (A) -> B. This is not a comment or a hint for a human — it is a real type that the compiler checks just as strictly as Int or String.
Below is a small table to help your brain get used to the idea that a “function” can live in a variable and be passed as an argument:
| What it is | Example value | Type |
|---|---|---|
| “Take a number, return a number” | |
|
| “Take a string, return Bool” | |
|
| “Take two numbers, return a number” | |
|
Pay attention: the + operator can also be treated as a value of the required type, if the context expects a function “two Int → Int”. That is exactly why this works: reduce(0, +) (you have already seen this). And a little later we will do a similar trick in our own functions.
Basic pattern: function parameter and its call
The most basic pattern looks like this: a function has a function parameter, and inside we call it as usual — with parentheses. Here is a tiny example of “apply a rule to a value”:
import Foundation
func apply(_ x: Int, using transform: (Int) -> Int) -> Int {
transform(x)
}
let doubled = apply(10, using: { $0 * 2 })
print(doubled) // 20
Here transform is a parameter of type (Int) -> Int. It is no worse than an Int parameter; its “value” is just different: not a number, but a rule.
Now let us make the example closer to real life: “count the number of elements that match a condition.” We are intentionally writing this manually, even though an array has filter/count, because right now we are learning the mechanics of passing a rule.
import Foundation
func countBooks(_ books: [String], where predicate: (String) -> Bool) -> Int {
var total = 0
for title in books {
if predicate(title) { total += 1 }
}
return total
}
The main thing here is to see that predicate(title) looks completely normal: predicate is a “variable that contains a function,” and we call it.
Pass vs. call: a common confusion
At this point almost all beginners make the same mistake: they confuse passing a function with calling a function. Visually the difference is small — the presence of () — but in meaning it is “pass the rule” versus “execute it right now.” It is like bringing a recipe into the kitchen or cooking borscht immediately (and then wondering why the kitchen is occupied).
Let us look at the contrast. Suppose we have a function:
import Foundation
func isShortTitle(_ s: String) -> Bool {
s.count <= 10
}
Then passing it as a value means writing it without parentheses:
import Foundation
let books = ["Swift", "The Swift Programming Language", "Dune"]
let shortCount = countBooks(books, where: isShortTitle)
print(shortCount) // 1
But you cannot write it like this (or you will not get what you expect), because isShortTitle() is a call, and it needs an argument:
import Foundation
countBooks(books, where: isShortTitle()) // ❌ this is not allowed: you are trying to call the function
It helps to keep a simple formula in mind: a function name without () is a value, a function name with () is an action.
3. Improving code readability
When functions that take functions appear in code, types start to look “scarier”: (String) -> Bool, (String) -> String, (Int, Int) -> Int and so on. This is normal, but sometimes you want to make the code friendlier for humans.
typealias for closure types
For that, we use typealias — simply a “type alias.”
Important: this is not a new entity, not a class, and not a struct. It is literally a “second name” for a type, so that signatures are easier to read.
import Foundation
typealias BookPredicate = (String) -> Bool
typealias BookFormatter = (String) -> String
func printBooks(_ books: [String], format: BookFormatter) {
for title in books {
print(format(title))
}
}
Now the signatures start to read almost like plain English: “print books, formatting them with the rule BookFormatter.”
When a closure is better, and when a named function is better
At this stage it is easy to swing to an extreme: either write everything as closures (and end up with a “forest of curly braces”), or, on the contrary, spawn named functions for every little thing. A healthy style is somewhere in the middle: a small one-time rule is convenient to keep near the place where it is used, while a meaningful rule is better to name.
For example, output formatting is often one-time:
import Foundation
printBooks(library) { title in
"\"\(title)\" (\(title.count) chars)"
}
But predicates like “the book is short,” “the book contains a word,” “the book starts with a letter” are usually better extracted and named, because they are already part of the mini-app’s business logic, not a printing detail.
import Foundation
func startsWithLetter(_ letter: Character) -> (String) -> Bool {
let l = String(letter).lowercased()
return { normalize($0).hasPrefix(l) }
}
Readability is the main criterion. If, a week later, you open the file and understand the code without decoding “what the author meant,” then you did everything right.
4. Mini book catalog: filtering and formatting
To keep the examples from being “in a vacuum,” let us continue developing our educational command-line application. For now it is simple: there is a list of book titles (strings), and we can print and filter them. Later the course will make the project more complex, but right now our goal is to learn how to pass rules as functions.
Let us start with the data and a couple of utilities:
import Foundation
let library = [
"Swift",
"Clean Code",
"The Pragmatic Programmer",
"Dune",
"1984"
]
func normalize(_ s: String) -> String {
s.trimmingCharacters(in: .whitespacesAndNewlines).lowercased()
}
normalize is an ordinary function. But right now it is also a “value” for us, which can be passed into map or into our own helper.
Now let us create two universal functions: one filters an array of books by a rule, the other prints books with formatting. These are the two most common operations in command-line programs: select the data and present it nicely.
First — filtering:
import Foundation
typealias BookPredicate = (String) -> Bool
func filterBooks(_ books: [String], where predicate: BookPredicate) -> [String] {
var result: [String] = []
for title in books {
if predicate(title) { result.append(title) }
}
return result
}
And now — printing with formatting:
import Foundation
typealias BookFormatter = (String) -> String
func printBooks(_ books: [String], format: BookFormatter) {
for title in books {
print(format(title))
}
}
And this is the reason for all of it. We can plug in different rules without rewriting loops:
import Foundation
func isShort(_ title: String) -> Bool { title.count <= 6 }
let shortOnes = filterBooks(library, where: isShort)
printBooks(shortOnes) { "- \($0)" }
// - Swift
// - Dune
// - 1984
Pay attention to the style: isShort is a named function (easy to reuse), while the output formatting is written as a closure “in place,” because it is small and one-time.
5. Functions that build rules
Once you accept the idea that “a function is a value,” very elegant techniques start working: operators can be passed as functions, initializers too, and you can also write functions that return new “rules.”
Operators as values
Let us make our own function to “calculate something from two numbers” and pass + and * into it:
import Foundation
func combine(_ a: Int, _ b: Int, using op: (Int, Int) -> Int) -> Int {
op(a, b)
}
print(combine(3, 4, using: +)) // 7
print(combine(3, 4, using: *)) // 12
This is the same principle as reduce(0, +), only now we created the “slot” ourselves where an operator can be plugged in.
Initializers as values
Now about Int.init. Suppose the user entered book ratings as strings, and we want to turn them into numbers. Int.init has the right shape for compactMap: “string → Int?.”
import Foundation
let rawRatings = ["5", "x", "4", "10", "nope"]
let ratings = rawRatings.compactMap(Int.init)
print(ratings) // [5, 4, 10]
The idea is simple: you do not write { Int($0) }, but instead pass a ready-made rule for “how to try to make an Int.”
Returning a function from a function
Sometimes it is more convenient not only to pass functions into functions, but also to write a function that creates and returns a new rule, for example, “search for books containing a word.” It is like making a cookie cutter: you made the “star” shape once, and then you can stamp out as many cookies as you want.
Let us build a “predicate constructor” for substring search:
import Foundation
func makeTitleContainsPredicate(_ query: String) -> (String) -> Bool {
let q = normalize(query)
return { title in
normalize(title).contains(q)
}
}
Now let us use it:
import Foundation
let containsSwift = makeTitleContainsPredicate("swift")
let found = filterBooks(library, where: containsSwift)
printBooks(found) { "FOUND: \($0)" }
// FOUND: Swift
// FOUND: The Swift Programming Language (if it were in the list)
A quiet kind of magic happens here without mysticism: the inner function (the closure) “remembers” q. You have already seen this in the topic about capturing variables in closures; now it has become a practical tool.
6. Typical mistakes
When functions start traveling through code as ordinary values, mistakes become a little more “philosophical”: the program compiles, but you passed the wrong thing, called it in the wrong place, or made reading too complicated. That is normal; everyone goes through this. The main thing is to learn how to recognize these situations quickly by their symptoms and fix them without pain.
Mistake #1: the function is accidentally called instead of being passed.
The most common case is writing where: isShort() instead of where: isShort. Parentheses turn a “rule” into an “action,” and an action needs arguments. If the compiler complains about "missing argument" or "cannot convert value … to expected argument type", check first whether () got attached by accident.
Mistake #2: the type of the passed function does not match.
For example, you want to pass a (String) -> Bool rule into a filter, but you have a (String) -> Int function. In everyday terms, that sounds like “I brought a hammer to a place that needed a screwdriver.” The fix is simple: look at the expected parameter type and compare what your function “takes” and what it “returns.”
Mistake #3: the closure becomes too large and turns into a mini novel.
If a passed closure contains 10–15 lines, several if/else statements, and variables, that is almost always a signal to extract the logic into a named function. It is easier to read, easier to reuse, and easier to debug — and also easier to explain to classmates what you actually wrote.
Mistake #4: capturing variables in a closure is used unconsciously.
A closure can “remember” external values (as in the example with q), and that is convenient. But if you start mutating an external var inside the rule, the code becomes less predictable. We are not discussing all the consequences deeply yet at this stage of the course, but a useful habit already now is to try to keep “rules” for filter/map/reduce about calculation, not about changing the outside world.
Mistake #5: overcomplicating your function’s interface.
Sometimes a beginner gets so inspired that they create a function with three closures, two arrays, and five typealias declarations — and then cannot even call it themselves. Start with the minimum: one array + one rule. Once that is clear and convenient, add the next level, but only if it is really needed for readability.
GO TO FULL VERSION