pointfreeco / swift-nonempty

🎁 A compile-time guarantee that a collection contains a value.

Home Page:https://www.pointfree.co/episodes/ep20-nonempty

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Compiler can't infer that flat-mapped NonEmpty<[String]> results in [String]

wearhere opened this issue · comments

struct Emoji {
    let aliases: NonEmpty<[String]>
    let tags: [String]
}

let emoji = /* an Emoji */
let searchTerms: [String] = [emoji.aliases, emoji.tags].flatMap { $0 } // error

The error is Cannot convert value of type '[Any]' to specified type '[String]'.

I'm using Xcode 11.4.1 and Swift 5.

your [emoji.aliases, emoji.tags] is of type [Any] and you've specified the searchTerms to be [String]

@ibrahimkteish I don't understand your implication, can you clarify? Regardless of the type of [emoji.aliases, emoji.tags], it remains that the code above works if aliases is typed as [String] and not when it is typed as NonEmpty<[String]>. As far as I understand the library, NonEmpty is supposed to present as an instance of whatever collection it wraps in every way except for enforcing non-emptiness, so this seems like a bug.

commented

The problem here is that Swift does not yet have disjoint types (like Either).

emoji.aliases is a NonEmpty, and emoji.tags is an Array, so [emoji.aliases, emoji.tags] is a[Any] (AKA Array<Any>) (ideally, it might be a [(NonEmpty<[String]>|[String])], but again, Swift can't do that yet. It'd also be nice if it were a [Collection], but knowing that the sub-collections all have String Elements is also not possible yet in Swift).

Because that array literal is a [Any], it doesn't have a flatMap method; it doesn't see that its contents are all collections which can be flattened.

All you have to do is prove to the compiler that these are compatible collections - currently the only way to do that is by making them the same type (turn the NonEmpty<[String]> into a [String] or vice versa).


Here's a version of your code that works:

import NonEmpty

struct Emoji {
    let aliases: NonEmpty<[String]>
    let tags: [String]
}

let emoji = Emoji(aliases: NonEmpty("smile", "grin"), tags: ["smiley", "face"])
let searchTerms: [String] = [Array(emoji.aliases), emoji.tags].flatMap { $0 }

image

Thanks for the explanation @BenLeggiero ! Turning the NonEmpty<[String]> into a [String] like that doesn’t look too bad. I don't know if you'd prefer to keep this issue open to track adopting the (eventual) Swift features you mention, but if you'd like to close that's fine by me.

commented

Glad I could help, @wearhere! I think it's probably best to close this since we don't know for sure whether Swift will ever support disjoint type combinations

I'm tagging @stephencelis and @mbrandonw since they control this repo

Glad y'all figured this out, thanks for the help @BenLeggiero and @ibrahimkteish!

Another way of doing this would be to wrap the collections in AnyCollection in order to get Swift to think it's a homogenous collection:

[AnyCollection(emoji.aliases), AnyCollection(emoji.tags)]
  .flatMap { $0 }

It's a bit longer, but has the potential benefit that it won't allocate an array for emoji.aliases, it will just wrap the collection.