Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[SR-13107] compactMap behavior changes depending on receiving type? #55553

Closed
mattneub opened this issue Jun 28, 2020 · 5 comments
Closed

[SR-13107] compactMap behavior changes depending on receiving type? #55553

mattneub opened this issue Jun 28, 2020 · 5 comments
Labels
bug A deviation from expected or documented behavior. Also: expected but undesirable behavior. compiler The Swift compiler in itself type checker Area → compiler: Semantic analysis

Comments

@mattneub
Copy link

Previous ID SR-13107
Radar None
Original Reporter @mattneub
Type Bug
Status Resolved
Resolution Invalid
Environment

Xcode 12, but the behavior is the same in, say, Xcode 9.2 (where of course `compactMap` is called `flatMap`)

Additional Detail from JIRA
Votes 0
Component/s Compiler
Labels Bug, TypeChecker
Assignee None
Priority Medium

md5: 5405a15a45ffe8222241a56c6a24ba15

Issue Description:

The following has just arisen on SO:

func f() {
    let marks = ["86", "45", "thiry six", "76"]
    let result = marks.compactMap { Int($0) }
    let result2 : [Int?] = result
    let result3 : [Int?] = marks.compactMap { Int($0) }
    print(result)
    print(result2)
    print(result3)
}

Write a project or playground that calls `f()`. I would expect result2 and result3 to be the same; the first uses `result` as an intermediary, the second simply assigns directly what would have been assigned to the intermediary. However, they are different. `result3` has a `nil` element that I can't explain, since `compactMap` has the job of eliminating nil.

The `result3` line is in fact behaving as if we had called `map` and not `compactMap`, but I cannot explain why merely casting would cause that to happen.

It looks like this behavior is very old in Swift, so perhaps it's by design? But if so, it's a very odd design; this is a circumstance where

b = a; c = b

gives a different result from

c = a

which is not what one usually expects of a computer language.

@typesanitizer
Copy link

The problem is that the type parameters are not getting instantiated the way you are expecting them to be. The signature for compactMap is as follows:

func compactMap<ElementOfResult>(_ transform: (Element) throws -> ElementOfResult?) rethrows -> [ElementOfResult] 

The second call is

let result3 : [Int?] = marks.compactMap { Int($0) }

Based on the explicit type annotation, the compiler picks ElementOfResult = Int? so that the return type matches up. Another possibility is that it could've picked ElementOfResult = Int and then added an implicit conversion from [Int] to [Int?] on the return value of compactMap, but it doesn't do that; that is what is happening in the case of result and result2 because of the way the type annotations are written.

Since ElementOfResult = Int?, the closure now has type (Int) throws -> Int??. This means that parsing failures get translated to .some(nil) which then becomes nil thanks to compactMap.

I understand this is probably not a very satisfactory answer. As you pointed out, this breaks what is seemingly a basic reasoning principle: let-substitution should not change the semantics of the program. Unfortunately, the presence of convenience features like implicit conversions, the lack of a formalized type system (on which one can prove theorems, such as obeying let-substitution) and the complexity of different interacting language features make it quite difficult to guarantee properties like this.

@mattneub
Copy link
Author

Thanks theindigamer (JIRA User), the Int?? explanation was in fact my other secret hypothesis about this. This is hard for the average user to reason about, but as you say, it’s the price we pay for the convenience of magic Optional wrapping on assignment.

@mattneub
Copy link
Author

theindigamer (JIRA User) I've attempted to re-explain this for the layman at https://stackoverflow.com/a/62642823/341994. I hope I have not misrepresented your explanation. Thanks again!

@typesanitizer
Copy link

Thanks. I would not call myself a Swift expert but otherwise your explanation is pretty spot-on. 🙂 If I were to make an adjustment, I would make the explanation in terms of compactMap itself instead of thinking about the implementation. After all, when you are reasoning about how things work with framework code, all you'll have is the type signature; you shouldn't have to think about the implementation to understand how the types work out. Couple of other things you might want to do:

  1. Link to this bug report/share the bug number so that anyone else reading your answer can check the bug report.

  2. Link the developer to the new WWDC 2020 talk 'Embrace Swift Type Inference' which might help them understand and guide type inference in Swift better. https://developer.apple.com/videos/play/wwdc2020/10165

@typesanitizer
Copy link

Another "interesting" example, where a simple refactoring breaks code. You'd think that I should be able to refactor f(expr) to let a = expr; f(a) but that's not the case.

// Before
func f(_: Int8) {}
f(0) // Ok

// After
func f(_: Int8) {}
let x = 0
f(x) // error: cannot convert value of type 'Int' to expected argument type 'Int8'

This is because inference works one statement at a time, and defaults are immediately "collapsed". Hence x gets the type Int without looking at its uses. This has the benefit of higher predictability (avoiding spooky-action-at-a-distance where types much later in a function influence types in earlier parts) at the cost of permitting fewer seemingly innocuous refactorings.

@swift-ci swift-ci transferred this issue from apple/swift-issues Apr 25, 2022
This issue was closed.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bug A deviation from expected or documented behavior. Also: expected but undesirable behavior. compiler The Swift compiler in itself type checker Area → compiler: Semantic analysis
Projects
None yet
Development

No branches or pull requests

2 participants