Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Using flatMap with dictionaries produces tuples?

I have this pretty basic and straight forward mapping of an object into dictionary. I am using and parsing dictionary on the top level. One of its fields is an array of other dictionaries. To set them I use flatMap which seems an appropriate method but since the object inside is not nullable this method suddenly returns tuples (at least it seems that way) instead of dictionaries.

I created a minimum example that can be pasted into a new project pretty much anywhere any of your execution occurs which should give better detail then any description:

func testFlatMap() -> Data? {

    class MyObject {
        let a: String
        let b: Int
        init(a: String, b: Int) { self.a = a; self.b = b }

        func dictionary() -> [String: Any] {
            var dictionary: [String: Any] = [String: Any]()
            dictionary["a"] = a
            dictionary["b"] = b
            return dictionary
        }
    }

    let objects: [MyObject] = [
        MyObject(a: "first", b: 1),
        MyObject(a: "second", b: 2),
        MyObject(a: "third", b: 3)
    ]

    var dictionary: [String: Any] = [String: Any]()
    dictionary["objects"] = objects.flatMap { $0.dictionary() }
    dictionary["objects2"] = objects.map { $0.dictionary() }

    print("Type of first array: " + String(describing: type(of: dictionary["objects"]!)))
    print("Type of first element: " + String(describing: type(of: (dictionary["objects"] as! [Any]).first!)))

    print("Type of second array: " + String(describing: type(of: dictionary["objects2"]!)))
    print("Type of second element: " + String(describing: type(of: (dictionary["objects2"] as! [Any]).first!)))

    return try? JSONSerialization.data(withJSONObject: dictionary, options: [])
}
_ = testFlatMap()

So this code crashes saying

'NSInvalidArgumentException', reason: 'Invalid type in JSON write (_SwiftValue)'

(Using do-catch makes no difference which is the first WTH but let's leave that for now)

So let's look at what the log said:

Type of first array: Array<(key: String, value: Any)>
Type of first element: (key: String, value: Any)
Type of second array: Array<Dictionary<String, Any>>
Type of second element: Dictionary<String, Any>

The second one is what we expect but the first just has tuples in there. Is this natural, intentional?

Before you go all out on "why use flatMap for non-optional values" let me explain that func dictionary() -> [String: Any] used to be func dictionary() -> [String: Any]? because it skipped items that were missing some data.

So only adding that little ? at the end of that method will change the output to:

Type of first array: Array<Dictionary<String, Any>>
Type of first element: Dictionary<String, Any>
Type of second array: Array<Optional<Dictionary<String, Any>>>
Type of second element: Optional<Dictionary<String, Any>>

which means the first solution is the correct one. And on change there are no warnings, no nothing. The app will suddenly just start crashing.

The question at the end is obviously "How to avoid this?" without too much work. Using dictionary["objects"] = objects.flatMap { $0.dictionary() } as [[String: Any]] seems to do the trick to preserve one-liner. But using this seems very silly.

like image 262
Matic Oblak Avatar asked Mar 27 '18 13:03

Matic Oblak


Video Answer


1 Answers

I see that your question boils down to map vs. flatMap. map is the more consistent one and it works as expected here so I won't delve into it.

Now on to flatMap: your problem is one of the reasons that SE-0187 was proposed. flatMap has 3 overloads:

Sequence.flatMap<S>(_: (Element) -> S) -> [S.Element] where S : Sequence
Optional.flatMap<U>(_: (Wrapped) -> U?) -> U?
Sequence.flatMap<U>(_: (Element) -> U?) -> [U]
  • When your dictionary() function returns a non-optional, it uses the first overload. Since a dictionary is a sequence of key-value tuples, an array of tuple is what you get.

  • When your dictionary() function returns an optional, it uses the third overload, which essentially filters out the nils. The situation can be very confusing so SE-0187 was proposed (and accepted) to rename this overload to compactMap.

Starting from Swift 4.1 (Xcode 9.3, currently in beta), you can use compactMap:

// Assuming dictionary() returns [String: Any]?
dictionary["objects"] = objects.compactMap { $0.dictionary() }

For Swift < 4.1, the solution you provided is the only one that works, because it gives the compiler a hint to go with the third overload:

dictionary["objects"] = objects.flatMap { $0.dictionary() } as [[String: Any]]
like image 78
Code Different Avatar answered Oct 30 '22 10:10

Code Different