Difference between string array with quotations and without? - swift

UserDefaults.standard.set(["a", "b"], forKey: "xxx")
if let def = UserDefaults.standard.array(forKey: "xxx") as? [String] {
print(def) // ["a", "b"]
}
if let def = UserDefaults.standard.array(forKey: "xxx") {
print(def) // [a, b]
}
In the second example, when you don't explicitly cast the array to [String] (leaving it as [Any]), the print console produces an array without quotation marks which suggests they aren't strings. What is happening here?

If you print out their types, you'll find that the second def is an array of NSTaggedPointerStrings. NSTaggedPointerString is a private subclass of NSString, and NSStrings print out to the console without the double quotes when inside an array.
UserDefaults.standard.set(["a", "b"], forKey: "xxx")
if let def = UserDefaults.standard.array(forKey: "xxx") as? [String] {
print(def) // ["a", "b"]
print(type(of: def)) // Array<String>
for element in def {
print(element, "is a", type(of: element)) // a is a String
// b is a String
}
}
if let def = UserDefaults.standard.array(forKey: "xxx") {
print(def) // [a, b]
print(type(of: def)) // Array<Any>
for element in def {
print(element, "is a", type(of: element)) // a is a NSTaggedPointerString
// b is a NSTaggedPointerString
}
}
print(["a", "b"] as [NSString]) // [a, b]

In the first example, you are optional unwrapping/inferring the array to an array of strings. So it's logged as ["a", "b"] which is the standard representation of string value in double-quotes.
In the second one, the type is not inferred so by default it's logged as an array of Any type values where you don't see any double-quotes.

Related

Using reduce() with an inferred return type

Given:
let a = [1, 2, 3]
Use reduce() to map to an array of strings:
let b: [String] = a.reduce([], { $0 + [String($1)] })
This returns ["1", "2", "3"] as you'd expect.
let c: [String] = a.reduce([], { $0 + [$1] })
This generates a compiler error (can't convert from Int to String), also as you'd expect.
let d = a.reduce([String](), { $0 + [$1] })
This returns [1, 2, 3] of type Array<Any>. Why doesn't the compiler consider the Result type to be Array<String>, and also generate an error in this case?
You want an error to occur (and I think a warning is in order), but the compiler assumes you want code to compile, instead. So it's going to fall back to Any for heterogenous collections, unless you type it otherwise. E.g.
protocol Protocol { }
extension Bool: Protocol { }
extension Int: Protocol { }
[[true]].reduce([0], +) as [any Protocol]
The correct return type can be inferred. Just not if you create a heterogeneous result.
a.map(String.init)
a.reduce(into: [String]()) { $0.append(.init($1)) }
a.reduce(into: []) { $0.append(String($1)) }
[Any] is a type of array that can contain 1, 2 and 3. When you try to append those values to [String] it just turns it into [Any].
It doesn't work on the other case because you're asking for the final array to be [String] by using it on the declaration.
let a = // array of [Int] ✅
let b: [String] = // array of [String] ✅
let c: [String] = // array of [Any] ❌
let d = // array of [Any] ✅

Why do `map(foo)` and `map{ foo($0) }` return different results?

For the following code snippet, why do res1 and res2 have different values?
func test() {
let a: String? = nil
let b: String? = nil
let foo: (String?) -> Int = { $0 == nil ? 0 : 1}
let res1 = [a, b].compactMap { $0 }.map(foo)
let res2 = [a, b].compactMap { $0 }.map { foo($0) }
print("res1: ", res1)
print("res2: ", res2)
}
The output is
res1: [0, 0]
res2: []
This appears to be a result in how the compiler selects the types of [a, b].compactMap based on the downstream operation. You can see this by inspecting the types of the arguments as they pass through the functions:
func printType<T>(_ i: String, _ v: T) -> T {
print(i, "->", T.self)
return v
}
func test() {
let a: String? = nil
let b: String? = nil
let foo: (String?) -> Int = { $0 == nil ? 0 : 1 }
let res1 = printType("cm1", printType("a1", [a, b]).compactMap { $0 }).map(foo)
let res2 = printType("cm2", printType("a2", [a, b]).compactMap { $0 }).map { foo($0) }
print("res1: ", res1)
print("res2: ", res2)
}
test()
// a1 -> Array<Optional<Optional<String>>>
// cm1 -> Array<Optional<String>>
// a2 -> Array<Optional<String>>
// cm2 -> Array<String>
// res1: [0, 0]
// res2: []
It appears that:
In the case of res1:
Because foo takes a String, map(foo) is typed such that a String? is passed through — and for map to be receiving a String?, compactMap must be returning a [String?]
In order for compactMap to be returning a [String?], its input must be a [String??]
Although [a, b] defaults to being a [String?], the compiler can also implicitly upcast it to a [String??], so that's what it does
Hence, only one layer of optionality is removed from [String??], and each of the String? values is passed to the map and into foo
In the case of res2, the compiler isn't restricted as heavily by map { foo($0) }, since $0 can be implicitly upcast inside of the closure before being passed to foo, and this is what the compiler prefers:
$0 is a String which is upcast to String? before being passed to foo
For map to receive String values, compactMap must return [String]
Since compactMap is returning a [String], its input must be [String?], which [a, b] is, no upcasting needed
Hence, now compactMap is actually filtering the nils out of [String?] into [String], and since no values are left, foo is never even called (you can see this with more print statements inside of foo
This sort of thing is very situational in the compiler, and you happened to have found a specific case where this happens. For instance, the compiler parses the results of single-statement closures differently from multi-statement closures: if you turn compactMap { $0 } into compactMap { _ = 1 + 1; return $0 }, the closure will parsed differently, and type checking will occur in a different order, resulting in [] in both cases:
let res1 = printType("cm1", printType("a1", [a, b]).compactMap { _ = 1 + 1; return $0 }).map(foo)
let res2 = printType("cm2", printType("a2", [a, b]).compactMap { _ = 1 + 1; return $0 }).map { foo($0) }
// a1 -> Array<Optional<String>>
// cm1 -> Array<Optional<String>>
// a2 -> Array<Optional<String>>
// cm2 -> Array<Optional<String>>
// res1: [0, 0]
// res2: [0, 0]
In this case, the compiler actually ended up preferring the surprising case in both instances, since return $0 allows the compiler to upcast from String? ➡ String?? (and then filter back down from [String??] ➡ [String?])!
Either way, this seems worthy of a report on https://bugs.swift.org, since the behavior is incredibly surprising. I will be happy to add test cases and comments to the report if you go ahead and file.

Unwrapping with flatMap

I want to get the weekday int of a date that I know exists like this:
let dayOfWeek = Calendar.current.dateComponents([.weekday], from: row.date).weekday
However, this returns an optional, and I'm trying to figure out how to avoid force unwrapping it.
My though was to do:
let dayOfWeek = (row.date).compactMap( { Calendar.current.dateComponents([.weekday], from: $0).weekday!
})
However this gives me the error "value of type 'Date' has no member 'compactMap'"
Can someone tell me what I'm doing wrong, or how I should go about fixing this?
No optionals are needed. It's simply:
let dayOfWeek = Calendar.current.component(.weekday, from: row.date)
Note, flatMap is not relevant here because row.date is not an optional. But even if it were, it's worth noting that the Optional method flatMap has not had its name changed. It's still flatMap. Only the Sequence method of this name has been changed to compactMap.
See Swift Evolution 0187: Introduce Sequence.compactMap(_:)
So, this is still flatMap:
let foo: Int? = 42
let bar: String? = foo.flatMap { i -> String in
return "The value is \(i)"
}
// Optional("The value is 42")
Note, the returned value is an optional String?. In your example, it looks like you're trying to use flatMap to unwrap your optional, but that's not what flatMap is for. It's for calling the closure if it can unwrap foo, but for returning nil if it can't unwrap it. So it just returns another optional (a String? in my above example).
The flatMap that has been renamed to compactMap in Swift 4.1 is the Sequence rendition:
let baz = ["1", "2", "x", "3"]
let qux: [Int] = baz.compactMap { string -> Int? in
return Int(string) // return integer value if it could convert it, return `nil` if not
}
// [1, 2, 3]
To make it even more confusing, there is still flatMap used with sequences:
let letters = ["a", "b", "c"]
let quux = letters.map { Array(repeating: $0, count: 3) }
// [["a", "a", "a"], ["b", "b", "b"], ["c", "c", "c"]]
let quuz = letters.flatMap { Array(repeating: $0, count: 3) }
// ["a", "a", "a", "b", "b", "b", "c", "c", "c"]
There is no need to avoid forced unwrapping in this case. While weekday is optional, it will never be nil when you specifically request the .weekday component.
let dayOfWeek = Calendar.current.dateComponents([.weekday], from: row.date).weekday!

Can swift optionals be mapped to new values?

In java 8, you can do the following: -
Optional<String> foo = Optional.empty(); // or Optional.of("hello");
foo.map(String::toUpperCase).orElse("Empty String")
edit - another example
class Bar {
public Bar(final String baz) { ... }
}
foo.map(f -> new Bar(f)) // has mapped into an optional Bar
That toUpperCase was a little too simplified; I'm thinking more like being able to specify a lambda or function that can provide a mutated value based on an optional present.
I see that you can do the following in swift: -
guard let foo = optional else { return "Empty String" }
which is massively useful, and also
optional ?? "Empty String"
but is there any way to map a value based on the value being optionally present? i.e. so if a value is present, you can return a mutated value?
This functionality is also available for java/c# collections
Say for a given array of optional strings, you can make use of optional chaining within your map operation
let foo: [String?] = ["foo", "bar", nil, "baz"]
let bar = foo.map { $0?.uppercaseString ?? "Empty string" }
print(bar) // ["FOO", "BAR", "Empty string", "BAZ"]
A possibly more useful alternative is to apply a flatMap operation to simply remove nil-valued optional entries from the string array
let foo: [String?] = ["foo", "bar", nil, "baz"]
let bar = foo.flatMap { $0?.uppercaseString }
print(bar) // ["FOO", "BAR", "BAZ"]
W.r.t. your comment, you could e.g. call a failable initializer for, say, some structure Foo, within the flatMap operation, yielding an array of Foo instances given that the initializer succeeds.
struct Foo {
let foo: String
init?(bar: String?) {
guard let foo = bar else {
return nil
}
self.foo = foo
}
}
let foo: [String?] = ["foo", "bar", nil, "baz"]
let bar = foo.flatMap { Foo(bar: $0) }
print(bar) // [Foo(foo: "foo"), Foo(foo: "bar"), Foo(foo: "baz")]
In your example, that's just:
optional?.uppercaseString ?? "EMPTY STRING"
or if you'd prefer to modify after defaulting:
(optional ?? "empty string").uppercaseString
Did you have something more complicated in mind?
(You may be unfamiliar with the ?. operator. It's called optional chaining in Swift, and is similar to map.)

Convert dictionary values to a different type in Swift

I have have a dictionary. I would like to go through it and convert the values to a different type. .map{ } would be perfect except this is a dictionary and not an array. So, I found a mapPairs function on stack overflow that should work for dictionaries. Unfortunately I get a conversion error.
extension Dictionary {
// Since Dictionary conforms to CollectionType, and its Element typealias is a (key, value) tuple, that means you ought to be able to do something like this:
//
// result = dict.map { (key, value) in (key, value.uppercaseString) }
//
// However, that won't actually assign to a Dictionary-typed variable. THE MAP METHOD IS DEFINED TO ALWAYS RETURN AN ARRAY (THE [T]), even for other types like dictionaries. If you write a constructor that'll turn an array of two-tuples into a Dictionary and all will be right with the world:
// Now you can do this:
// result = Dictionary(dict.map { (key, value) in (key, value.uppercaseString) })
//
init(_ pairs: [Element]) {
self.init()
for (k, v) in pairs {
self[k] = v
}
}
// You may even want to write a Dictionary-specific version of map just to avoid explicitly calling the constructor. Here I've also included an implementation of filter:
// let testarr = ["foo" : 1, "bar" : 2]
// let result = testarr.mapPairs { (key, value) in (key, value * 2) }
// result["bar"]
func mapPairs<OutKey: Hashable, OutValue>(#noescape transform: Element throws -> (OutKey, OutValue)) rethrows -> [OutKey: OutValue] {
return Dictionary<OutKey, OutValue>(try map(transform))
}
}
var dict1 = ["a" : 1, "b": 2, "c": 3]
let convertedDict: [String: String] = dict1.mapPairs { // ERROR: cannot convert value of type '_ -> (String, Int)' to expected argument type '(String, Int) -> (String, String)'
element -> (String, Int) in
element[0] = String(element.1)
return element
}
In Swift 5 and later:
let originalDict: [TypeA: TypeB] = /* */
let convertedDict: [TypeA: TypeC] = originalDict.mapValues { /* conversion here */ }
Example:
let integerDict: [String: Int] = ["a": 1, "b": 2]
let doubleDict: [String: Double] = integerDict.mapValues(Double.init)
print(doubleDict) // ["a": 1.0, "b": 2.0]
If you want to change a dict of [String: Int] to [String: String], you can pretty much do the same as my previous answer:
let dict1 = ["a" : 1, "b": 2, "c": 3]
var dict2 = [String: String]()
dict1.forEach { dict2[$0.0] = String($0.1) }
print("\(dict2.dynamicType): \(dict2)")
Output:
Dictionary<String, String>: ["b": "2", "a": "1", "c": "3"]
I don't know if this might help, but since Swift 4.2 there is a new operator called mapValues(_:) (https://developer.apple.com/documentation/swift/dictionary/2995348-mapvalues), which would transform the result you are looking for to:
let convertedDict = dict1.mapValues { String($0) }
As the example given by the method block, you should use mapPairs like this:
let convertedDict: [String: String] = dict1.mapPairs { (key, value) in
(key, String(value))
}
Note, since Swift supports implicit inference, you don't need explicitly return.