Programming in Swift · Closures and Collections | Ray Wenderlich


This is a companion discussion topic for the original entry at https://www.raywenderlich.com/5994-programming-in-swift/lessons/35

Why wouldn’t you use

names.sort(by: >)

instead of

names.sort(by: {(a, b) → Bool in
a > b
})

You definitely could use > to replace the long form closure, just like we used + in our example of reduce later in this same video!

One of our goals in this video was to show the many syntactical options of closures. None of them are technically more correct than the others, but if you can use a function that already exists, like >, that’s probably a better idea than rewriting it :]

compactMap:

25%20PM

You say that the compactMap function takes a closure that takes one element and returns an optional value.

Is it really an optional value if we know for sure that it’s a non-nil value? If it was an optional, wouldn’t we have to unwrap it first before using it or making changes to it? (Like Int(input) is transforming the possible Ints in the closure)?

flatMap:

34%20PM

You say that the flatMap function takes a parameter (consisting of a closure) and returns an array.

In turn, the closure takes one parameter and returns the same type.

Can you please explain what you mean by that?

The closure declaration here is array → [String]. Is ‘array’ here returning something?

Yes it is. The closure will return nil for the nil elements. CompactMap will then not include those nils in the array that it returns. We know that the return from compactMap is non-nil values. The closure itself has to return something for every element of the input, so it needs to return nil for the nil ones.

Here is an example, using the same closure for both map and compactMap. The closure can return nil; map will include the nils in the returned array; compactMap will not include the nils.

let userInputs = ["1", "2", "A", "3"]
let mapped = userInputs.map {input in Int(input)}
// [1, 2, nil, 3]   (mapped type is [Int?])
let compact = userInputs.compactMap {input in Int(input)}
// [1, 2, 3]   (compact type is [Int])
2 Likes

Thanks for the clarification!

For both methods in my original comment, is it unnecessary to include the closure’s parameters/return type because they are inferred given the method?

What are the types of ‘input’ and ‘array’ that have been omitted from the closures?

For the map methods, the type of the parameter will always be the type of the array elements being mapped.

For the first one, userInput is an array of String, so the parameter of the compactMap closure is a String, named input.

For the second one, arrayOfDwarfArrays is an array of arrays of String, so the parameter of the flatMap closure will be an array of String, named array.
Inside that closure, since array is an array of String, the parameter for the filter closure will be a String, named dwarf.

The second one does not actually need to define the return type of the closures, as that can be inferred from the content of the closures. It is sometimes a good idea to declare them anyway, because then the compiler can tell you if your code is returning what you think it is returning.

For the first one, you could put in the type of the return. If you try to make it Int, it will show an error. If you then make it Int?, it will accept it, thus letting you know that it is in fact an optional.

//let validInput = userInput.compactMap {input -> Int in Int(input)} // compile error
let validInput = userInput.compactMap {input -> Int? in Int(input)}
1 Like

I suppose my confusion stems from the fact that the code still works with the return type being omitted.

Does it infer that we’re returning an array of Ints because the values are being transformed into Int values in the body of the closure? Or is there something more to it?

.sorted, for instance, always compares two elements and returns a Bool. Are the map methods similar where they have that transformation element as an underlying type?

That’s it, nothing more to it.

They don’t have any particular type, it depends on what you put there for the transformation. You could return Doubles instead of Ints, for instance. Or a new String with an “*” added on the end.

Swift likes to infer type. It will determine what the type actually is anyway, and inform you with an error if it doesn’t match what you declared.

Closures in particular have been made as simple as possible. Notice that you don’t have to write “return Int(input)”, though you can. It takes it as implied that you want the expression result to be returned, because duh.

2 Likes

I really appreciate your help with this.

To sum up:

/For the map methods, the type of parameter will always be the type of the array of elements being mapped./

The .map closure takes as a parameter the values in the sequence and returns an alternative mapped value for each item (in a new array). Unless otherwise specified/inferred, the returned array will be of the same type as the initial collection.

The .compactMap closure takes as a parameter the values in the sequence and returns an optional value for each. The method then returns an array of the non-nil values in a new array.

The .flatMap closure takes as a parameter the values in the sequence (of all elements) and returns a single array. You use this method when the transformation produces a collection for each element-- for example if you have two arrays, they will be returned combined in a single array.

They don’t have any particular type, it depends on what you put there for the transformation. You could return Doubles instead of Ints, for instance. Or a new String with an “*” added on the end.

Does this also work with .flatMap? Or does this one in particular have to return the same type as the parameter?

I tried transforming Ints into Doubles, and Strings into Ints with .flatMap, but both times I got an error message saying that it didn’t match the parameter type.

It will always be specified or inferred. The output type of the map methods is determined by the type returned by the closure, not the type of the input sequence.

It should. Here is an example, with an array of Int as input. The closure produces an array of Doubles. With map, you end up with an array of arrays of Doubles. With flatMap, and the same closure, you get a single array of Doubles.

let intArray = [1, 2, 3]
let mapArray = intArray.map {item in [Double(item), Double(item) / 2]}
print(mapArray)

[[1.0, 0.5], [2.0, 1.0], [3.0, 1.5]]

let flatMapArray = intArray.flatMap {item in [Double(item), Double(item) / 2]}
print(flatMapArray)

[1.0, 0.5, 2.0, 1.0, 3.0, 1.5]

1 Like

It’s hit and miss for me, but I think it may have more to do with me not being very skilled with writing code yet. It’s usually when the input is an array of arrays…

For example, this works:

45%20PM

But not this:

And neither does this:

I’m not too sure how to make it work… Adding square braces didn’t help (error saying there’s no subscript available).

numbers is an array of two elements, each of which is an array of Int. So it will be passing an array of Int into the closure. As the compiler error indicates, you can’t use Double() on an array of Int.

Same thing. numberStrings is an array of two elements, each of which is an array of String, so it will be passing an array of String to the closure. You can’t use Int() on an array of String.

In both cases, once you grasp that the parameter passed into the closure is an array, you could put another map call inside the closure. That inner map would now get individual Ints or Strings, so its closure could use Double() or Int().

The first one works because it is passing each of the two arrays of Int into the closure, and the closure just returns the passed in array. flatMap gets a total of two arrays returned, and joins them into one. For the other two, the closure to flatMap should also be returning an array, so flatMap can join them. As Picard would say, your task is to make it so.

2 Likes

Hey Catie, thanks for your help. Is there an official answer for the extra credit challenge you mention at the very end of the video? It asks us to use the flatMap function with longer syntax. I think im almost there, but my compiler is not yet compiling. Let me know if you have an answer the question in the video I can use to reference! thanks

Hi! I actually didn’t write an official solution for that bit of extra credit.

If you’d like to post what you’ve got already, I’d be happy to take a look. :]

sure, here is my shorthand way of writing the second closure:

32%20PM

Here are two tidbits for ya.

  1. A shortened version of dwarvesAfterM:
let dwarvesAfterM = arrayOfDwarfArrays.flatMap {
  $0.filter { $0 > "M" }
}.sorted()
  1. And a possible solution for the bonus challenge, using dwarvesAfterM as an example problem:
// `flatMap` as a `for` loop - I've leave this one as a bonus challenge!
var dwarvesAfterM2: [String] = []
for dwarfArray in arrayOfDwarfArrays {
  dwarvesAfterM2 += dwarfArray.filter { $0 > "M" }
  dwarvesAfterM2.sort()
}

Hi Catie.

I’m a long time C/C++ & Obj-C programmer, and I’ll preface this with the fact that I utterly loathe Swift. I can’t believe this was ‘designed’.

One of the issues I’m struggling with is Closures and how ludicrous it is that you can just keep removing syntax and reducing something vaguely readable into something that isn’t - and Apple say this is easy and friendly and readable! Ha!!! I also can’t abide K&R so I always have to refactor into Allman to more clearly show scope.

In your Closures lesson you demonstrate this, but I am sticking to the full syntax as it’s marginally more readable, but it’s lead me to have a question:

I’ve written this code, and it works in the simulator BUT despite it working I still get the red exclamation point on the line:

let ns = getNetString()     <- ! Use of unresolved identifier 'getNewString'

Here’s the full code running in a Playground:

let multiply: (Int, Int) -> Int =
{
    (a: Int, b: Int) -> Int in 
    
    let s = "Arrghh!!! Swift!!!"
    let ns = getNewString()      
    
    if a < b
    {
        print(a, b, s, ns)
    }
    else
    {
        print(b, a, s, ns)
    }

    print ("Result of \(a) * \(b) = \(a * b)")
    
    return a * b
}

func getNewString() -> String
{
    return "We hates it we does"
}

multiply(4, 2)

Result in console window:

**2 4 Arrghh!!! Swift!!! We hates it we does**

**Result of 4 * 2 = 8**

So, why is it working when the editor seems to think it shouldn’t? And is this kind of thing allowed? i.e. calling other methods within an object from inside a closure. Or is this only working because the called method is returning a fixed string i.e. constant and therefore not run-time dependant?

Why did they design it so there are so many ways to write it? It’s just making it harder to read other peoples code and quickly read and understand the code itself?!

Hi Anne Marie,

That’s an interesting playground bug! Playgrounds are meant to run from top to bottom, so to get rid of the error just move getNewString above multiply. I’m really not sure why/how it’s managing to run anyway :[

Dealing with the 5,000 ways to write a closure can definitely be frustrating! It got easier to read the shorthand syntax(es) with time and practice, but I often prefer to be more explicit.

1 Like