UPGRADE YOUR SKILLS: Learn advanced Swift and SwiftUI on Hacking with Swift+! >>

< 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


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.

Ultimate Portfolio App: Introduction

11:03

ULTIMATE PORTFOLIO APP

FREE: Ultimate Portfolio App: Introduction

UPDATED: 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 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.

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.

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.

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.

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.

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.

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.

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.

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.

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.

Interview questions: Introduction

3:54

INTERVIEW QUESTIONS

FREE: Interview questions: Introduction

Getting ready for a job interview is tough work, so I’ve prepared a whole bunch of common questions and answers to help give you a jump start. But before you get into them, let me explain the plan in more detail…

Testing extensions

28:23

ULTIMATE PORTFOLIO APP

Testing extensions

UPDATED: Our project has various extensions that help make the rest of our code easier, and we need to treat these carefully – small changes to these can have huge effects across the rest of our project. So, in this article we’re going to write tests for the extensions we created, making sure they work correctly every time.

Codable networking with Combine

16:18

NETWORKING

Codable networking with Combine

So much of our job is about downloading JSON data, decoding it using Codable, then presenting it – it’s a core skill. But it’s common to see folks rely on huge libraries such as Alamofire, or get mixed up with URLSession. So, in this article we’ll look at how to rewrite common networking code using Combine, then add some generics to make it truly flexible.

Upgrade your Mac

1:36:29

LIVE STREAMS

Upgrade your Mac

In this stream we're going to build a SwiftUI and SwiftData app that monitors how long Xcode takes to build your projects, then uses that to calculate how much time and money you would save by upgrading to a newer Mac.

Cleaning up CloudKit, part 2

31:09

ULTIMATE PORTFOLIO APP

Cleaning up CloudKit, part 2

The second part of cleaning up CloudKit involves tackling error handling head on, and along the way I’ll show you a useful trick for making this process easier. I’ve said it before, but it bears repeating that getting error handling right is the key to a great CloudKit app!

What are the advantages and disadvantages of SwiftUI compared to UIKit?

2:27

INTERVIEW QUESTIONS

What are the advantages and disadvantages of SwiftUI compared to UIKit?

This is another example of a question where exclusively picking one over the other makes you look inexperienced – software development is often fuzzy, and acknowledging that complexity is a mark of experience rather than a weakness.

Advanced string interpolation, part two

19:32

ADVANCED SWIFT

Advanced string interpolation, part two

In part one of this tutorial we looked at how to customize string interpolations on a type-by-type basis, giving you more control over how your code works. In this second part we’ll look at a second powerful use for interpolation: building whole types from scratch.

Creating a day/night cycle

53:37

REMAKING APPS

Creating a day/night cycle

One of the most beautiful parts of the Weather app is the way it smoothly transitions between day and night – it doesn’t just go from black to blue, but instead mimics both sunrise and sunset, smoothly animating between the two. In this tutorial we’re going to recreate that same effect in our own app.

 
Unknown user

You are not logged in

Log in or create account
 

Link copied to your pasteboard.