NEW: Learn to build the incredible iOS 15 Weather app today! >>

< Back to Latest Articles

Transforming data with map()

In this article we’re going to look at the map() function, which transforms one thing into another thing. Along the way we’ll also be exploring some core concepts of functional programming, so if you read no other articles in this course at least read this one!

Watch the video here, or read the article below

Tip: The best way to try this code yourself is using a macOS Command Line Tool project, writing your code directly into main.swift.

Quick links

Transforming an array

I want to start in an unusual way: I want to write an extension on Swift’s Array type that is able to convert all elements in the array into strings.

First, we’ll write a function that does the conversion for us. This will accept a single Any parameter and return the string description of that parameter:

func anyToString(_ thing: Any) -> String {
    String(describing: thing)
}

Now we can write an extension on Array that loops over all the items in the current array, transforms them using our anyToString() function, and sends back a new array of all the transformed items:

extension Array {
    func transformed() -> [String] {
        var results = [String]()

        for item in self {
            let transformed = anyToString(item)
            results.append(transformed)
        }

        return results
    }
}

And now we can use it to convert an array of integers into an array of strings:

let numbers1 = [1, 2, 3]
let result1 = numbers1.transformed()
print(result1)

Hopefully there’s nothing there that troubles you too much, but already we’ve built something useful: we can now take any kind of Array and transform it into an array of strings.

But we can do better! What if we have a dictionary of numbers rather than an array of numbers – how can we get our transformed() method to work on that too?

Well, both Array and Dictionary conform to a protocol called Sequence, so we can make our extension work on all sequences with a tiny change – this:

extension Array {

Should become this:

extension Sequence {

And now we can use transformed() with a dictionary like this:

let numbers2 = [1: "One", 2: "Two", 3: "Three"]
let result2 = numbers2.transformed()
print(result2)

That works great, although the output is a little odd:

["(key: 1, value: \"One\")", "(key: 2, value: \"Two\")", "(key: 3, value: \"Three\")"]

Tip: Don’t be surprised if your array is in a different order each time you run the code – dictionaries don’t have an order, so you should not expect the converted array to have any meaningful order.

We told Swift to convert each item in the dictionary into a string, so it did just that: it writes each key and value like a tuple, then places that into a string. It’s done exactly what we wanted, but what we wanted isn’t right – it might have made sense for arrays, but it looks like gibberish for dictionaries.

Fortunately, we can do even better. Rather than forcing values into strings using Swift’s default conversion, we could instead upgrade our function to allow a custom conversion – we get to decide how each item in the sequence should be converted, rather than always funneling it through the anyToString() function.

This is done by passing a function into the transformed() method. This function must accept a single element from our sequence, which Swift provides to us as Element, and it must return that element transformed into a string.

So, change this:

func transformed() -> [String] {

Into this:

func transformed(_ converter: (Element) -> String) -> [String] {

If you haven’t seen functions being passed into other functions before, the syntax can hurt your eyes a little. What this means is “there’s a function called transformed(), which accepts a parameter called “converter”. That parameter must be a function that accepts one element from our array and returns a string, and the whole transformed() function will return an array of strings”.

Inside the transformed() function we’re calling anyToString() to convert each item from our sequence into a string, but we don’t want that any more. Instead, we want to use whatever function was passed in, which means you need to change this:

let transformed = anyToString(item)

Into this:

let transformed = converter(item)

However, we’ve broken the rest of our code, because those two places we call transformed() need to pass in a converter function – they need to pass in a function that accepts one element from the array and returns a string.

For now – just to make our code compile – let’s pass in the anyToString() function we wrote earlier. This will get us back our existing behavior, but we’ll change it soon enough.

So, change these two:

let result1 = numbers.transformed()
let result2 = numbers2.transformed()

Into these:

let result1 = numbers.transformed(anyToString)
let result2 = numbers2.transformed(anyToString)

So, the transformed() function expects to be given a function that can transform one element into a string, and that’s exactly what anyToString() does so we can pass it straight in. However, like I said that gives us our old ugly behavior, so let’s do better – rather than using anyToString() for the dictionary, we could use a trailing closure to run custom code:

let result2 = numbers2.transformed { element in
    "\(element.key) = \(element.value)"
}

Now we’re getting great transformations for both arrays and dictionaries – nice! Hopefully you can see some real power building here, because we don’t have to explain to Swift how to transform an array of objects or a dictionary of objects, we just have to tell it how to transform one object and transformed() takes care of the rest.

But we can do better still: originally we said that whatever function gets passed into transformed() must accept an element from our array and return a string, but there was no real reason to restrict that only to arrays – it works just as well with sequences.

That was a nice improvement, because it makes our code work in more places. But we have a second restriction that can be removed: we’re forcing our transformation function to return a string, when really it could return anything at all. I mean, do we really care what they want to transform something into? Not really – as long as they pass in a function that converts sequence elements into the type they want, we’re happy.

Swift calls this process generics, a bit like a generic brand. For example, there are lots of specific brands of coffee out there, but if you don’t actually care you can just buy a generic coffee brand – it’s still coffee, but not sold under a proprietary name. For our function we want to work with some kind of data but we don’t specifically care what kind that is.

For transformed() we become generic we need to change two things:

  1. Everywhere String is used in the function should become NewType.
  2. Just after func transformed we need to insert <NewType>.

Here’s how the new function looks:

extension Sequence {
    func transformed<NewType>(_ converter: (Element) -> NewType) -> [NewType] {
        var results = [NewType]()

        for item in self {
            let transformed = converter(item)
            results.append(transformed)
        }

        return results
    }
}

The Element type comes from Swift itself, because when they wrote the Array type they used Element to mean “the type of items we’re storing.” This means we need to use the exact name Element to refer to that item time – we can’t use Item, Object, or similar, because Swift won’t know what that means.

However, NewType is not from Swift: it’s our name, chosen by us, and has no special meaning. So, if you wanted you could use TransformedType in place of NewType, or Converted, or Elephant. Any of them are fine, as long as you use the same name everywhere.

When we wrote func transformed<NewType> we made what’s called a generic type parameter – we said that transformed() will be used with some specific kind of data but we don’t know. By giving it a name we’re effectively defining a placeholder that we can use in our function: everywhere NewType appears it will refer to the same type.

It’s a common coding convention to name your generic type parameters with single alphabet letters starting from T, so some people might prefer a function like this one:

func transformed<T>(_ converter: (Element) -> T) -> [T] {

Again, it doesn’t matter how you name your generic type parameters as long as they are used consistently.

Anyway, with that change made we now have a lot more control: our transformation function can return anything at all, so we get to decide on a case-by-case basis what it does. For example, we might decide that for dictionaries we want to send back a tuple containing the key and value, like this:

let result2 = numbers2.transformed { element in
    (element.key, element.value)
}

Or we might decide that for our integer array we want to send back each number convert into a Double:

let result1 = numbers1.transformed { element in
    Double(element)
}

And the result is a very flexible piece of code: one that applies to any kind of Sequence, one that can return any kind of data, and one that works with any kind of transformation function. Nice!

What did we build?

Our transformed() function accepts a transformation function, and returns a new array containing each one of its items after they have been transformed. It is generic, so that we can transform any kind of array into any other kind of array, and it’s also applied on Sequence so actually it also works on sets, dictionaries, and more.

This function is so useful it comes baked right into Swift as map(), and it works almost identically to the function we built. Sure, the official Swift first is a little faster because they more optimizations in place, and it’s also more powerful because they support throwing functions too, but from the call site the two are the same – we can actually replace our current code with map() like this and get the same output:

let result1 = numbers1.map { element in
    Double(element)
}

let result2 = numbers2.map { element in
    (element.key, element.value)
}

As map() already exists, you might think I wasted your time by making you build it from scratch, but I hope you realize it’s important you see how it works to get a really thorough understanding of what’s actually happening.

Now that you’ve seen the code, you know a number of things are true:

  • All items in a sequence will be transformed; we won’t break out in the middle.
  • This means if you send in an array of 10 items, you’ll get back an array of 10 items every time.
  • All items will be converted into the same resulting type based on whatever our transformation function returns.
  • Although our transformation function won’t ever throw errors, Swift’s own map() is able to deal with a throwing transformation.

If you were super curious, you could apply some of those rules to ours to get something even more similar to Swift’s map() method, like this:

extension Sequence {
    func transformed<NewType>(_ converter: (Element) throws -> NewType) rethrows -> [NewType] {
        var results = [NewType]()
        results.reserveCapacity(self.underestimatedCount)

        for item in self {
            let transformed = try converter(item)
            results.append(transformed)
        }

        return results
    }
}

Because map() will always send back an array containing the same number of items as were in the original array, that updated function uses reserveCapacity() to get space for approximately the right number of items. It’s called underestimated count because it might return 0 – some sequences can be looped over only once because they use up their values as they go. Imagine a sequence that generated the Fibonacci sequence, for example.

That code also now marks the transformation closure with throws, which means it can throw an error not that it must throw an error. Because the transformation function can throw we have to call it using try, and because we want our caller to handle the error we need to mark all of transformed() as throws so that errors bubble up to wherever it is called.

Except we don’t – we mark transformed() as rethrows, which is a variant of throws that means “this function can throw an error only if the transformation function we pass to it can also throw errors.” This is one of Swift’s power features, but it means when we call either map() or transformed() with a non-throwing transformation function then we don’t need to use try, but when we do use a throwing transformation then we need try so we can catch any errors.

Passing functions directly

Earlier we wrote code like this:

let result1 = numbers.transformed(anyToString)

Our transformed() function expects to be given a function as its only parameter, and that function must in this case accept a single integer from our array and convert it into something else. That’s exactly what our old anyToString() function does, which means we can pass it straight in.

We also wrote code like this:

let result1 = numbers1.transformed { element in
    Double(element)
}

That works, but we can do better. You see, Double.init() is the initializer for doubles, although we don’t use it that much because Swift offers the short syntactic sugar of Double(). Double.init() is a function just like any other, and here it can receive a single integer and return back a Double – it exactly matches the format expected by both transformed() and map().

So, we can instead write this:

let result1 = numbers1.map(Double.init)

That takes each item in our array, converts it to a Double, then puts it back into a new array. In this case, it will convert our array of integers into an array of doubles in extraordinarily concise syntax.

Once your brain starts thinking in this way you realize it unlocks a remarkable amount of power, and also a remarkable amount of brevity – you can get huge amounts of work done with almost no code at all.

For example, if we had an array of doubles we could calculate the square roots of each of them by mapping directly to the sqrt() function:

let squares: [Double] = [4, 9, 16, 25, 36, 49, 64, 81, 100]
let roots = squares.map(sqrt)
print(roots)

Or for a slightly longer example, we could define a function that calculates a particular number in the Fibonacci sequence:

func fibonacci(of number: Int) -> Int {
    var first = 0
    var second = 1

    for _ in 0..<number {
        let previous = first
        first = second
        second = previous + first
    }

    return first
}

If we wanted to call that many times – e.g., to calculate the first 50 numbers, we could write this:

let first50 = (0..<50).map(fibonacci)
print(first50)

As you can see, Swift’s ranges conform to Sequence, which means we can use either map() or transformed() to repeatedly call the fibonacci(of:) function and get back a lot of data. Of course, if you really wanted to calculate that many numbers in the Fibonacci sequence you might want to consider writing a dedicated function because each number builds upon the previous one!

Swift actually has another function power feature that benefits both transformed() and map() without any further work from us: we can use key path expressions as functions. This was introduced in Swift 5.2, and it means if you can write a key path such as \X.y then you can use that in place of functions that expect an X and return y. For example, \String.count can be used in places where you want to send in a string and get back the value of its count property.

To put that into code, we could create an array of strings then use either map() or transformed() to create an array of integers containing the counts of those strings:

let names = ["Taylor", "Justin", "Adele"]
let nameCounts1 = names.map { name in
    name.count
}
print(nameCounts1)

Key path expressions as functions allow us to condense that down, because we want to pass a string in and get the count out:

let nameCounts2 = names.map(\.count)
print(nameCounts2)

That code is shorter, yes, and that’s important in the same way that using map() is shorter than manipulating the array ourselves.

But what both map() and key path expressions let us do is focus on the result we want, as opposed to the exact steps it takes to get there. So, for map() that means we say “transform this thing into that thing,” but we don’t need to create an empty array, looping over all our items, transform them one by one, then add them to the transformed array – all that process goes away so we can focus on the data we want. And for key path expressions it means we just say “give me the count” as opposed to creating our own function to do that.

Spreading map elsewhere

Regardless of what you call the function itself, when you boil map() down to its basics it takes a value out of its container, transforms it somehow, then places it back into a new container. The new container won’t always be the same type – we might convert from a dictionary into an array, for example – but you’re still putting values back into a container.

This concept matters because Swift also provides map() on other types outside of Sequence: both Optional and Result support it, and it works in exactly the same way.

At the basic level, both Optional and Result are implemented in a similar way. Here’s the definition of Optional:

enum Optional<Wrapped> {
    case none
    case some(Wrapped)
}

And here’s Result:

enum Result<Success, Failure: Error> {
    case success(Success)
    case failure(Failure)
}

As you can see, they are both enums with associated values – one for Optional, to store whatever is inside it, and two for Result, to store whatever is inside its success and failure cases. At a broader level the two are quite different because the Swift compiler has special knowledge of how Optional works so that things like if let and guard let can work, but the actual underlying types for both are just a plain old enum.

Both of these contain data somehow, and so both have a built-in map() method that extracts their data, transforms them using a function we specify, and puts them back into a container.

In the case of Optional, map will extract the value wrapped inside, transform it using a function we specify, then place it back into another Optional. If there is no value inside – if the optional is set to none – then our function does nothing at all.

To demonstrate this, we could write some code that mimics loading some saved application data into a text view. I’ll make it return an optional Data instance, so that it can return nil if there is nothing saved:

func getSavedData() -> Data? {
    Data("Saved data goes here".utf8)
}

When we load that back we might get some saved data or we might get nothing at all, but if we did get something then we need to convert it into a string so we can put it in our text view.

This is where map() comes in:

let saved = getSavedData().map { data in
    String(decoding: data, as: UTF8.self)
}

Remember, if getSavedData() returns nil then the transformation function will return nothing; it will just set saved to nil.

Now, you might be wondering what the point is here – wouldn’t it be better to use something like nil coalescing so we get a real string rather than an optional one?

Very often that is a better solution, and in fact just adding ?? "" to the end of our code gets us that result:

let saved = getSavedData().map { data in
    String(decoding: data, as: UTF8.self)
} ?? ""

However, often you don’t want to resolve the optionality at this point – you want to load the data and transform it however you want, but keep it optional for the time being. This lets you pass the optional around somewhere else in your program, then resolve the optionality at a later date when you have more information. In contrast, if you resolve the optionality now then you don’t know whether loading your saved data succeeded or failed.

Like I said earlier, Result also has a map() method, and in fact we can use it immediately – just replace your existing getSavedData() method with this:

func getSavedData() -> Result<Data, Error> {
    .success(Data("Saved data goes here".utf8))
}

We don’t actually need to change the call site at all, because it works fine exactly the same way as Optional: if saved data was loaded successfully then transform it, otherwise do nothing.

This is possible because Result actually gives us two map() methods: one that transforms its success case, and one called mapError() that transforms its failure case. That latter option is useful when you need to bridge APIs: if one API returns a FileError and another expects to work with a LoadError, mapError() lets you convert between the two while staying with the existing Result type you had before.

The laws of functors

Before we’re done, I want to add one small but important piece of information: any data type that can be mapped over using map() is called a functor, as long it abides by two laws.

First, if your map function is the identity function – a fancy word meaning “sends back exactly what it receives with nothing changed” – then the input and output of calling map() must be the same. This sounds obvious, I know: if your transform function sends back exactly what it received, then the input and output must be the same.

In code, it means this should print “true”:

let scores = [100, 80, 85]
let mappedScores = scores.map { $0 }
print(scores == mappedScores)

The second law is this: if you create a transformation function that performs two transformations on your data, then calling map() on with that function should give the same result as calling map() on the first transformation then separately on the second.

In code, that means creating some example functions such as these:

func uppercased(_ string: String) -> String {
    string.uppercased()
}

func reversed(_ str: String) -> String {
    String(str.reversed())
}

func uppercasedAndReversed(_ str: String) -> String {
    String(str.uppercased().reversed())
}

And now calling them twice – once where we map using uppercased() then map using reversed(), and a second time where we use uppercasedAndReversed():

let names1 = names.map(uppercased).map(reversed)
let names2 = names.map(uppercasedAndReversed)
print(names1 == names2)

As long as a type implements a map() function respecting those two rules, it can be a called a functor. That’s it – even though it might sound academic, there is no magic behind the name; it’s just an easy way for us to refer to this kind of functionality.

Anyway, we’ve built our own map() function so you can see exactly how it works, we’ve looked at calling it with functions and key paths, and we’ve also looked at how it works equally well in other types. Hopefully you can see why map() is so important to functional programmers!

If you liked this, you'd love Hacking with Swift+…

Here's just a sample of the other tutorials, with each one coming as an article to read and as a 4K Ultra HD video.

Find out more and subscribe here


Understanding assertions

27:33

INTERMEDIATE SWIFT

FREE: Understanding assertions

Assertions allow us to have Swift silently check the state of our program at runtime, but if you want to get them right you need to understand some intricacies. In this article I’ll walk you through the five ways we can make assertions in Swift, and provide clear advice on which to use and when.

Challenge 1: Converter

38:12

SOLUTIONS

FREE: Challenge 1: Converter

This early challenge day asks you to build a converter app that’s able to move between any two similar units, such as kilometers and miles. Let’s solve it now, then we’ll take it further, and then we’ll take it even further

How to use phantom types in Swift

24:11

ADVANCED SWIFT

FREE: How to use phantom types in Swift

Phantom types are a powerful way to give the Swift compiler extra information about our code so that it can stop us from making mistakes. In this article I’m going to explain how they work and why you’d want them, as well as providing lots of hands-on examples you can try.

Understanding generics – part 1

20:01

INTERMEDIATE SWIFT

FREE: Understanding generics – part 1

Generics are one of the most powerful features of Swift, allowing us to write code once and reuse it in many ways. In this article we’ll explore how they work, why adding constraints actually helps us write more code, and how generics help solve one of the biggest problems in Swift.

Creating a custom property wrapper using DynamicProperty

14:20

INTERMEDIATE SWIFTUI

FREE: Creating a custom property wrapper using DynamicProperty

It’s not hard to make a basic property wrapper, but if you want one that automatically updates the body property like @State you need to do some extra work. In this article I’ll show you exactly how it’s done, as we build a property wrapper capable of reading and writing documents from our app’s container.

Shadows and glows

19:50

SWIFTUI SPECIAL EFFECTS

FREE: Shadows and glows

SwiftUI gives us a modifier to make simple shadows, but if you want something more advanced such as inner shadows or glows, you need to do extra work. In this article I’ll show you how to get both those effects and more in a customizable, flexible way.

User-friendly network access

14:26

NETWORKING

FREE: User-friendly network access

Anyone can write Swift code to fetch network data, but much harder is knowing how to write code to do it respectfully. In this article we’ll look at building a considerate network stack, taking into account the user’s connection, preferences, and more.

Creating a WaveView to draw smooth waveforms

32:08

CUSTOM SWIFTUI COMPONENTS

FREE: Creating a WaveView to draw smooth waveforms

In this article I’m going to walk you through building a WaveView with SwiftUI, allowing us to create beautiful waveform-like effects to bring your user interface to life.

Using memoization to speed up slow functions

36:18

HIGH-PERFORMANCE APPS

FREE: Using memoization to speed up slow functions

In this article you’ll learn how memoization can dramatically boost the performance of slow functions, and how easy Swift makes it thanks to its generics and closures.

Functional programming in Swift: Introduction

6:52

FUNCTIONAL PROGRAMMING

FREE: Functional programming in Swift: Introduction

Before you dive in to the first article in this course, I want to give you a brief overview of our goals, how the content is structured, as well as a rough idea of what you can expect to find.

Making the most of optionals

23:07

ADVANCED SWIFT

FREE: Making the most of optionals

Swift’s optionals are implemented as simple enums, with just a little compiler magic sprinkled around as syntactic sugar. However, they do much more than people realize, and in this article I’m going to demonstrate some of their power features that can really help you write better code – and blow your mind along the way.

Ultimate Portfolio App: Introduction

14:17

ULTIMATE PORTFOLIO APP

FREE: Ultimate Portfolio App: Introduction

While I’m sure you’re keen to get started programming immediately, please give me a few minutes to outline the goals of this course and explain why it’s different from other courses I’ve written.

Making your app accessible

33:12

ULTIMATE PORTFOLIO APP

FREE: Making your app accessible

It is my firm belief that every iOS app should be usable to everyone, and putting in the work to make your app function well no matter who is using it says a lot about the kind of developer you are.

Trees

31:55

DATA STRUCTURES

FREE: Trees

Trees are an extraordinarily simple, extraordinarily useful data type, and in this article we’ll make a complete tree data type using Swift in just a few minutes. But rather than just stop there, we’re going to do something quite beautiful that I hope will blow your mind while teaching you something useful.

Guess the Flag

16:29

SOLUTIONS

Guess the Flag

This challenge asks you to add some important features to Guess the Flag, including a score tracker and a limited number of questions. Let’s solve those now, and tackle a bonus problem at the end just for fun…

Rendering a pie chart

25:37

RENDERING CHARTS IN SWIFTUI

Rendering a pie chart

Pie charts are a classic way of showing divided data visually, and they represent interesting challenges around sizing and angles. In this article we’ll build a complete pie chart view from scratch using SwiftUI, ensuring it works using animation, and also modify it to support donut-style charts too.

Bringing MVVM into our SwiftUI project, part 2

43:49

ULTIMATE PORTFOLIO APP

Bringing MVVM into our SwiftUI project, part 2

In this article we’re going to continue with our move towards MVVM, this time converting another view that works well, but also looking at code that works less well so you can get a better idea of how SwiftUI and MVVM really work.

Testing development data

29:48

ULTIMATE PORTFOLIO APP

Testing development data

In previous tests we relied upon our sample data creating 5 projects and 50 items, but that isn’t set in stone right now – it’s an implementation detail, meaning that it’s a behavior that happens to be the case but isn’t explicitly guaranteed. This is a common cause of bugs, so in this article we’re going to write tests for our development code, and along the way discover and resolve some interesting quirks…

Testing extensions

26:04

ULTIMATE PORTFOLIO APP

Testing extensions

Our project has three extensions that add functionality to Apple’s own code, and we need to treat these carefully – we don’t own them, so their behavior might change at any point in the future. So, in this article we’re going to write tests for the extensions we created, making sure they work correctly every time.

Testing Core Data

26:18

ULTIMATE PORTFOLIO APP

Testing Core Data

Our app relies extensively on user data, so if there’s one part of it that absolutely must be bullet proof it’s our Core Data stack. In this article we’ll explore writing tests for our data, including projects, items, and awards.

 
Unknown user

You are not logged in

Log in or create account
 

Link copied to your pasteboard.