1. Why functions are needed
If you write a program without functions, copy-paste inevitably appears: the same logic for reading a number, checking a range, calculating a discount, and printing the result gets repeated in different places. At first, it seems harmless (“so what, just 6 more lines”), and then you change the output format — and end up searching for those 6 lines throughout the file. A function is a way to give a name to a repeated action or calculation and use it as many times as needed.
You can think of a function as a “mini machine”: you put input data inside, the machine does the work, and either returns a result or simply performs an action (for example, prints something). Important idea: a function is a contract. It promises: “if you give me this input data, I will do this.”
Below is the smallest example of an action function:
func sayHello() {
print("Hello! I am a function 🙂")
}
sayHello() // Hello! I am a function 🙂
Notice: first we declared the function (with func), and then we called it (with sayHello()).
2. Function skeleton: func, name, parameters, body
When people see a function declaration for the first time, it looks like “too many symbols around a simple action.” But in fact, everything there is logical: Swift forces us to explicitly describe what the function takes and what it returns. That is the contract the compiler checks for you (and that is one of the reasons Swift likes types so much).
A function signature (that is, the “header”) is usually read like this:
func name(parameters) -> resultType { body }
Let’s visualize the parts of a function in a table — it is easier to remember that way:
| Part | Example | Purpose |
|---|---|---|
| Keyword | |
Tells Swift: “this is a function” |
| Name | |
So you can call the function by name |
| Parameters | |
Input data for the function |
| Result | |
Result type (what the function promises to return) |
| Body | |
Code that runs when the function is called |
The smallest example of “a function computes something and returns it”:
func square(x: Int) -> Int {
return x * x
}
let v = square(x: 6)
print(v) // 36
What matters here is that square does not print the result. It returns it. Printing is a separate step. This makes functions more universal: today you print, tomorrow you compare, the day after tomorrow you store the result in a variable.
3. Parameters and arguments: do not confuse “inside” and “outside”
Parameters and arguments are similar words, which is why the brain tries to “save memory” and treat them as the same thing. But the difference is simple and very practical: a parameter lives in the function declaration, while an argument lives at the call site. In other words, a parameter is like a “slot,” and an argument is the specific value you pass into that slot.
Let’s look at a small example where a function builds a greeting string:
func greeting(name: String) -> String {
return "Hello, \(name)!"
}
let text = greeting(name: "Anya")
print(text) // Hello, Anya!
In the declaration, name is a parameter. At the call site, "Anya" is an argument.
A function can take multiple parameters. Then it starts to resemble a “formula” with several inputs:
func rectangleArea(width: Int, height: Int) -> Int {
return width * height
}
let area = rectangleArea(width: 4, height: 7)
print(area) // 28
Notice how pleasant the call is to read: width: 4, height: 7. Even if you forget the order, the code still “explains itself.”
4. Return value and return
The return value is what many functions are written for in the first place. And here it is important to understand two rules. First: if the signature contains -> Type, then the function must return a value of that type. Second: a value can only be returned with return.
The simplest example: a function determines the sign of a number and returns a string:
func signDescription(value: Int) -> String {
if value > 0 {
return "positive"
} else if value < 0 {
return "negative"
} else {
return "zero"
}
}
print(signDescription(value: 10)) // positive
print(signDescription(value: 0)) // zero
Notice the discipline: every branch of if/else if/else returns a String. If you forget return in even one branch, the compiler will say: “You promised a String; where is it?”
Another important detail: return not only returns a value, but also immediately ends the function’s execution. Everything written after return in that branch will not run.
5. Void: a function “does” something, but returns nothing
In real programs, a huge number of functions do not return anything “useful”: they print, read input, write to a file, show a screen, send a request. At our current level, the clearest example is printing: print() also returns nothing; it just performs an action.
If a function does not return a result, we say it returns Void. In Swift, Void is an alias for the empty tuple () (that is, “empty value”). Usually, -> Void is not written in the signature: the absence of an arrow already means “I return nothing.”
Example of a function that prints a header:
func printHeader(title: String) {
print("=== \(title) ===")
}
printHeader(title: "My program") // === My program ===
Sometimes return appears in Void functions too — but not to “return a result,” rather to exit early. It is like a “stop word” inside a function.
Example:
func printIfPositive(value: Int) {
if value <= 0 {
return
}
print("The number is positive")
}
printIfPositive(value: -3) // (will print nothing)
printIfPositive(value: 5) // The number is positive
This is a very useful technique: instead of deep nesting with if, you can “exit earlier” and keep the main execution path more direct.
6. Function contract: return and immutable parameters
All branches must return a value
Errors with return are one of the most common reasons a beginner looks at the compiler and thinks: “It is nitpicking.” But the compiler is not nitpicking; it is guarding your contract. If a function promises Int, then it must return Int in every scenario.
For example, you cannot write it like this (shown as a counterexample, ❌ does not compile):
func badSign(value: Int) -> String {
if value > 0 {
return "positive"
}
// if value <= 0 — what should be returned? the compiler will not let you build the project
}
The correct version is to always provide an “else”:
func goodSign(value: Int) -> String {
if value > 0 {
return "positive"
} else {
return "not positive"
}
}
print(goodSign(value: 0)) // not positive
Yes, sometimes this looks “redundant.” But then anyone who calls your function knows for sure: there will always be a result, and it will be of the right type.
Parameters inside a function behave like let
Beginners often try to “reuse a parameter as a working variable”: add something to it, trim a string, change a discount. And then Swift says: “not allowed.” The reason is simple: a parameter is input, and it is better to treat it as a constant. The function received a value — and works with it without replacing the input itself.
If you need to change a value, create a local copy with var. It may look boring, but it is very clear.
Example:
func withPrefix(text: String, prefix: String) -> String {
var result = text
result = prefix + result
return result
}
print(withPrefix(text: "world", prefix: "hello ")) // hello world
Here we do not touch text. We create result and work with it. This is a discipline that later saves you from unexpected effects: when the input data “suddenly changed” and nobody knows why.
7. Example: DiscountBuddy console app
Now we will assemble a small program that we will keep developing in the lectures later in the day. The idea is simple: a “discount calculator.” The user enters a price and a discount percentage, and the program calculates the final price and prints the result. Our goal is not math (that part is boring), but to see how functions turn a long piece of code into neat blocks.
Greeting
First, let’s create a greeting function:
func printWelcome() {
print("DiscountBuddy v0.1")
print("Let's calculate the price with a discount.")
}
printWelcome()
// DiscountBuddy v0.1
// Let's calculate the price with a discount.
Reading a number
Now let’s move number input into a separate function. For now we will be as straightforward as possible: if the user enters nonsense, we will get 0. This is not ideal validation, but for the current stage of the course it is fine:
func readInt(prompt: String) -> Int {
print(prompt, terminator: " ")
let line = readLine() ?? ""
return Int(line) ?? 0
}
let x = readInt(prompt: "Enter a number:")
print("You entered \(x)")
Calculating the discounted price
And finally, the discounted price calculation function — it returns a value, it does not print:
func discountedPrice(price: Int, discountPercent: Int) -> Int {
let discount = price * discountPercent / 100
return price - discount
}
let finalPrice = discountedPrice(price: 1000, discountPercent: 15)
print(finalPrice) // 850
Stitching the scenario together in top-level code
Now let’s put this into one coherent chunk of top-level code that reads like a “scenario,” not a “mess”:
func printWelcome() {
print("DiscountBuddy v0.1")
}
func readInt(prompt: String) -> Int {
print(prompt, terminator: " ")
let line = readLine() ?? ""
return Int(line) ?? 0
}
func discountedPrice(price: Int, discountPercent: Int) -> Int {
let discount = price * discountPercent / 100
return price - discount
}
printWelcome()
let price = readInt(prompt: "Enter the price (euros):")
let percent = readInt(prompt: "Enter the discount (%):")
let result = discountedPrice(price: price, discountPercent: percent)
print("Final price: \(result) euros")
Notice that the top-level part now reads almost like plain text: “greet → ask for price → ask for discount → calculate → print.” That is exactly why we love functions: they turn code into a clear scenario.
How to think about functions: input → function → output
When you are just starting out, it is very easy to start seeing functions as magic: “called it — something happened somewhere.” To avoid that, it helps to keep a simple model in mind: a function is a data transformer, sometimes with a side effect (for example, printing). That means it has input, process, and (possibly) output.
A small flowchart helps reinforce this:
flowchart LR
A[Arguments at call site] --> B[Function body]
B --> C[return value]
B --> D["side effect: print()"]
In our DiscountBuddy, it looks like this: readInt(prompt:) takes a prompt string, prints it (a side effect), reads input, and then returns an Int. And discountedPrice(...) does not print anything at all: it just returns a number. This separation is a very strong habit: “computation functions” are easier to inspect by eye, and they usually break less often.
8. Typical mistakes
Mistake #1: the function promises to return a value, but return is not in every branch.
This is classic: you wrote -> Int, made if value > 0 { return 1 }, and then forgot else. The compiler in this case is your best friend: it does not let you release a program where, in one scenario, “the result is undefined.” The fix is a habit: if a function returns a value, mentally ask yourself, “what will be returned in all other cases?”
Mistake #2: mixing computation and output without need.
A beginner often does this: the function both calculates and prints. Then it turns out you want to use the result elsewhere without printing it — and you have to rewrite things. At this level of the course, it is very useful to separate: “calculate and return” — one function, “display nicely to the user” — another function or top-level code.
Mistake #3: trying to modify a parameter directly.
Inside a function, a parameter behaves like a constant, and Swift will not let you write something like value = value + 1. This is not a “language nitpick,” but protection against confusion: input should remain input. If you need to “tweak” a value, create a local var temp = value and modify that.
Mistake #4: wrong expectations from Void functions.
Sometimes you want to do let x = printWelcome() and expect x to be a string or something useful. But Void means: the function returns nothing; it only performs an action. If you need a value, then the function should return a type (for example, String) and contain return inside.
Mistake #5: functions that are too long and try to do everything.
In the excitement over functions, it is easy to make one huge process() of 200 lines that reads, validates, calculates, and prints. Formally, it is a function, but in essence it is the same long sheet of code, only inside curly braces. A good rule to start with: if you cannot explain what a function does in 10 seconds, and its name ends up being something like doEverythingAndMore(), then it is time to split it into smaller steps.
GO TO FULL VERSION