3

I got a surprise today while looking at another SO question:

let s = "1,a"
let arr = s.split(separator: ",")
let result = arr.compactMap{Int($0)} // ok
let result2 = arr.compactMap(Int.init) // error

Why is line 3 legal but line 4 is not? I would have thought these two ways of saying "coerce the incoming parameter to Int if possible" would be completely equivalent.

I understand that line 4 is choking on the Subsequence, and I see how to get out of the difficulty:

let result2 = arr.map(String.init).compactMap(Int.init) // ok

What I don't understand is why they both don't choke in the same way.

matt
  • 515,959
  • 87
  • 875
  • 1,141
  • Does `.init` actually init? – aheze Mar 22 '21 at 22:48
  • 1
    @aheze `Int(...)` _is_ `Int.init`. One is a call, the other is the name. With parentheses we are providing the name of a function and that's what I'm doing. The question is, why isn't this exactly the same function I was using in the curly brace. As I say, I can twiddle line 4 to make it work, I just don't understand why I have to. – matt Mar 22 '21 at 22:56
  • It has something to do with the fact that `split` returns a `[Substring]`, but `Int.init` wants a `String`. This, for example, works: `arr.map(String.init).compactMap(Int.init)`. I'm curious why `Int($0)` works to begin with – New Dev Mar 22 '21 at 23:00
  • @NewDev Yeah, I know, and that’s the question. – matt Mar 22 '21 at 23:04
  • 1
    @matt, didn't notice your edit before commenting :) – New Dev Mar 22 '21 at 23:05
  • 1
    The two answers demonstrate why. There are two places I find that I frequently can’t use `.init` when I want to because overloads are missing. Default arguments are one, which you’re running up against here. The other is autoclosures: https://stackoverflow.com/questions/34699576/use-logical-operator-as-combine-closure-in-reduce –  Mar 23 '21 at 02:04
  • 1
    @Jessy I still find it weird linguistically that `Int()` is not itself identically syntactic sugar for `Int.init`. I guess it’s because the logic and syntax of function references is different; it has to be unambiguous. – matt Mar 23 '21 at 02:42

3 Answers3

4

Looks like the Int.init overload that accepts a Substring has the following signature:

public init?<S>(_ text: S, radix: Int = 10) where S : StringProtocol

So, Int($0) works because it uses the default radix, but there isn't an Int.init(_:) that accepts a Substring - there's only Int.init(_:radix:) that does - and so it fails.

But if there was one:

extension Int {
    public init?<S>(_ text: S) where S : StringProtocol {
        self.init(text, radix: 10)
    }
}

then this would work:

let result1 = arr.compactMap(Int.init)
New Dev
  • 48,427
  • 12
  • 87
  • 129
4

In fact the first version (Int($0)) calls this initializer, which has two parameters (one of them has a default value):

@inlinable public init?<S>(_ text: S, radix: Int = 10) where S : StringProtocol

If I define a custom initializer like so, then the second example works too.

extension Int {
    init?<S>(_ string: S) where S: StringProtocol {
        // convert somehow, e.g: self.init(string, radix: 10)
        return nil
    }
}

let result2 = arr.compactMap(Int.init)

It seems to me that if I write Int.init in the compactMap, it can call only the exact initializer (or function), and the second parameter of the first called initializer cannot be inferred.

Another example:

func test1<S>(param1: S) -> String where S: StringProtocol {
    return ""
}

func test2<S>(param1: S, defaultParam: String = "") -> String where S: StringProtocol {
    return ""
}

extension Sequence {
    func customCompactMap<ElementOfResult>(_ transform: (Element) -> ElementOfResult?) -> [ElementOfResult] {
        compactMap(transform)
    }
}

arr.customCompactMap(test1)
arr.customCompactMap(test2) // error

I think the function references cannot hold any default values. Unfortunately I didn't find any official reference to this, but seems interesting.

Proof, last example:

func test3(param1: String, defaultParam: String = "") { }
let functionReference = test3
functionReference("", "")
functionReference("") // error

Here the functionReference's type is (String, String) -> (), even though the test3 function has a default value for the second parameter. As you can see functionReference cannot be called with only one value.

jason d
  • 416
  • 1
  • 5
  • 10
1

I tried looking for the Swift forum post where someone on the core team explained this, but sorry, I couldn't find it. You can go asking there and get clarification on this point:

Default arguments don't actually produce overloads.

Instead, using default arguments at call site is syntactic sugar for using all arguments. The compiler inserts the defaults for the ones you don't use.

A few results of that…


You cannot use functions with default arguments as closures with simplified signatures. You have to wrap them in new closures, as you demonstrated in your question.

func ƒ(_: Int = 0) { }

let intToVoid: (Int) -> Void = ƒ // compiles

// Cannot convert value of type '(Int) -> ()' to specified type '() -> Void'
let voidToVoid: () -> Void = ƒ

Methods with different default argument patterns, that look the same at call site, are not considered overrides.

class Base {
  func ƒ(_: Any? = nil) -> String { "Base" }
}

final class Derived: Base {
  // No `override` required.
  func ƒ() -> String { "Derived" }
}

Base().ƒ() // "Base"
Derived().ƒ() // "Derived"
(Derived().ƒ as (Any?) -> String)("argument") // "Base"

Default arguments do not allow for satisfaction of protocol requirements.

protocol Protocol {
  func ƒ() -> String
}

// Type 'Base' does not conform to protocol 'Protocol'
extension Base: Protocol { }