1

Say I have a dictionary of type [String : String] which I want to transform to type [String : URL]. I can use map or flatMap to transform the dictionary, but due to the failable URL(string:) initializer, my values are optional:

let source = ["google" : "http://google.com", "twitter" : "http://twitter.com"]

let result = source.flatMap { ($0, URL(string: $1)) }

This returns a value of type [(String, URL?)] and not [String : URL]. Is there a one-liner to transform this dictionary with a single method? My first thought was something like:

source.filter { $1 != nil }.flatMap { ($0, URL(string: $1)!) }

But I don't need to check if the value is nil (values will never return nil on a dictionary concrete values), I need to check if the return value of URL(string:) is nil.

I could use filter to remove the nil values, but this doesn't change the return type:

source.flatMap { ($0, URL(string: $1)) }.filter { $1 != nil }
JAL
  • 41,701
  • 23
  • 172
  • 300
  • 1
    declare your empty result `var result: [String: URL] = [:]` and then use forEach to add the key values to it `source.forEach{ guard let value = URL(string: $0.value) else { return } result[$0.key] = value }` this way you just iterate over it once – Leo Dabus Feb 08 '17 at 01:16
  • 2
    AFAIK, there are no standard library mapping functions that return a dictionary (the real problem is the [lack of higher-kinded types](https://github.com/apple/swift/blob/master/docs/GenericsManifesto.md#higher-kinded-types)) – you'll either have to implement your own, or just use a trusty old for loop. – Hamish Feb 08 '17 at 01:17
  • 3
    @LeoDabus A `nil` value means that it doesn't exist for a given key, so you could totally just say `source.forEach{ result[$0.key] = URL(string: $0.value) }`. – Hamish Feb 08 '17 at 01:18
  • Thanks, both of you. I was afraid that the only current way to do this was to use a loop and an empty result. – JAL Feb 08 '17 at 01:20
  • Related: [What's the cleanest way of applying map() to a dictionary in Swift?](http://stackoverflow.com/q/24116271/2976878) – Hamish Feb 08 '17 at 01:31
  • @Hamish I've read through that, didn't find it as helpful as I had hoped. Happy to entertain it as a dupe if you feel it is too closely related. – JAL Feb 08 '17 at 01:33
  • @JAL I don't think it's *quite* a dupe, as you're also looking to filter out key-value pairs, but I thought it close enough to be worth linking to. – Hamish Feb 08 '17 at 01:41
  • @JAL `extension Dictionary where Key: CustomStringConvertible, Value: CustomStringConvertible { var linksToURL: [String: URL] { var result: [String: URL] = [:] forEach{ result[$0.key.description] = URL(string:$0.value.description) } return result } }` – Leo Dabus Feb 08 '17 at 01:52
  • It will become nicer with the implementation of https://github.com/apple/swift-evolution/blob/master/proposals/0100-add-sequence-based-init-and-merge-to-dictionary.md – Martin R Mar 08 '17 at 18:51

4 Answers4

1

You need to make sure you're returning tuples with only non-optional values, and since optional values themselves support flatMap you can use that to make the tuple optional as opposed to the individual value inside of it:

let source = [
    "google": "http://google.com",
    "twitter": "http://twitter.com",
    "bad": "",
]
var dict = [String: URL]()
source.flatMap { k, v in URL(string: v).flatMap { (k, $0) } }.forEach { dict[$0.0] = $0.1 }

But since we've already expanded out the dictionary creation (I don't think there's a built-in way to create a dict from an array), you might as well do this:

var dict = [String: URL]()
source.forEach { if let u = URL(string: $1) { dict[$0] = u } }
Blixt
  • 49,547
  • 13
  • 120
  • 153
  • This is on the right track, but still requires an outside dictionary to be initialized and referenced using a `for` loop (like discussed in the comments). I guess Swift doesn't support a pure solution with `map` at this time. – JAL Feb 08 '17 at 19:52
  • @JAL Since you can't pass in an array to the dictionary constructor, you can't do better than this without extension methods. The good news is, the literal implementation is less code and arguably more readable than any functional approach. :) – Blixt Feb 08 '17 at 20:20
0

Here are a few solutions:

//: Playground - noun: a place where people can play

import Foundation

let source = ["google": "http://google.com", "twitter": "http://twitter.com", "bad": ""]

//: The first solution takes advantage of the fact that flatMap, map and filter can all be implemented in terms of reduce.
extension Dictionary {
    /// An immutable version of update. Returns a new dictionary containing self's values and the key/value passed in.
    func updatedValue(_ value: Value, forKey key: Key) -> Dictionary<Key, Value> {
        var result = self
        result[key] = value
        return result
    }
}

let result = source.reduce([String: URL]()) { result, item in
    guard let url = URL(string: item.value) else { return result }
    return result.updatedValue(url, forKey: item.key)
}
print(result)


//: This soultion uses a custom Dictionary initializer that consums the Key/Value tuple.
extension Dictionary {
    // construct a dictionary from an array of key/value pairs.
    init(items: [(key: Key, value: Value)]) {
        self.init()
        for item in items {
            self[item.key] = item.value
        }
    }
}

let items = source
    .map { ($0, URL(string: $1)) } // convert the values into URL?s
    .filter { $1 != nil } // filter out the ones that didn't convert
    .map { ($0, $1!) } // force unwrap the ones that did.
let result2 = Dictionary(items: items)
print(result2)


//: This solution also uses the above initializer. Since unwrapping optional values is likely a common thing to do, this solution provides a method that takes care of the unwrapping.
protocol OptionalType {
    associatedtype Wrapped
    var asOptional : Wrapped? { get }
}

extension Optional : OptionalType {
    var asOptional : Wrapped? {
        return self
    }
}

extension Dictionary where Value: OptionalType {
    // Flatten [Key: Optional<Type>] to [Key: Type]
    func flattenValues() -> Dictionary<Key, Value.Wrapped> {
        let items = self.filter { $1.asOptional != nil }.map { ($0, $1.asOptional!) }
        return Dictionary<Key, Value.Wrapped>(items: items)
    }
}

let result3 = Dictionary(items: source.map { ($0, URL(string: $1)) }).flattenValues()
print(result3)
Daniel T.
  • 32,821
  • 6
  • 50
  • 72
0

Daniel T's last solution is quite nice if you want to write it in a more functional style. I'd do it a bit differently with the primary difference being a method to turn a tuple of optionals into an optional tuple. I find that to be a generally useful transform, especially combined with flatMap.

let source = ["google" : "http://google.com", "twitter" : "http://twitter.com", "fail" : ""]

// Dictionary from array of (key, value) tuples.  This really ought to be built it
extension Dictionary {
    public init(_ array: [Element]) {
        self.init()
        array.forEach { self[$0.key] = $0.value }
    }
}

//Turn a tuple of optionals into an optional tuple. Note will coerce non-optionals so works on (A, B?) or (A?, B)  Usefull to have variants for 2,3,4 tuples.
func raiseOptionality<A,B>(_ tuple:(A?, B?)) -> (A, B)? {
    guard let a = tuple.0, let b = tuple.1 else { return nil }
    return (a,b)
}

let result = Dictionary(source.flatMap { raiseOptionality(($0, URL(string: $1))) } )
Brian K
  • 51
  • 3
0

Easy as pie if you just want a good, known URL in place of the bad ones.

Use


let source = ["google" : "http://google.com", "twitter" : "http://twitter.com", "bad": ""]

let defaultURL = URL(string: "http://www.google.com")! // or whatever you want for your default URL

let result = source.flatMap { ($0, URL(string: $1) ?? defaultURL) }
Bill Waggoner
  • 452
  • 3
  • 11
  • Not sure if you read my question, I'm not looking to provide a default value, I'm looking to flatten out nil values. I could use a default value and filter key-value pairs that match out, but I don't want the default to potentially match one of the valid URLs. – JAL Feb 10 '17 at 20:32