Changes to SetAlgebra or OptionSet in Swift 5?

I just upgraded to the new Xcode and my code isn't compiling because it says a type which is an OptionSet no longer conforms to SetAlgebra.

I figured I would ask if there were any changes to those protocols which would cause this (and how to fix it)

(I built the code immediately before the update and it ran fine)

It claims to be missing the following methods:

mutating func insert(_ newMember: __owned StarRating) -> (inserted: Bool, memberAfterInsert: StarRating) 
mutating func remove(_ member: StarRating) -> StarRating?
mutating func update(with newMember: __owned StarRating) -> StarRating? 

I'm wondering if it has to do with the __owned modifier, since I haven't seen that before...

You probably need to provide more details here. As a quick test, in Xcode Version 10.2 (10E125), I copied the sample ShippingOptions code from the OptionSet into a new playground and it worked fine.

struct ShippingOptions: OptionSet {
  let rawValue: Int
  
  static let nextDay    = ShippingOptions(rawValue: 1 << 0)
  static let secondDay  = ShippingOptions(rawValue: 1 << 1)
  static let priority   = ShippingOptions(rawValue: 1 << 2)
  static let standard   = ShippingOptions(rawValue: 1 << 3)
  
  static let express: ShippingOptions = [.nextDay, .secondDay]
  static let all: ShippingOptions = [.express, .priority, .standard]
}

I think I found the problem after digging around the Standard Library code.

For some reason it seems that whatever intuits the type of OptionSet's Element changed between versions. The extension that defines the "missing" properties requires that the OptionSet's Element == Self. I explicitly defined it as such and it started working again...

Paging @Ben_Cohen to see whether this was an intentional change.

This was not an intentional change to my knowledge.

@Jon_Hull can you post a reproduction? Since OptionSet conforms to SetAlgebra this can't be a blanket problem or no option sets would be compiling so presumably there's some other factor here that's breaking. Looking at the code, it doesn't look like anything changed (ownership annotations shouldn't matter) so I'm guessing it's something about associated type inference interacting with something else about the code (like a conformance to Sequence or ExpressibleByArrayLiteral or something like that).

I do have two types which are very similarly named.

First is a "StarRating" which defines a rating of 1-5:

enum StarRating:Int,Comparable {
    case unknown = 0
    case star1
    case star2
    case star3
    case star4
    case star5
    
    static func < (lhs: StarRating, rhs: StarRating)->Bool {
        return lhs.rawValue < rhs.rawValue
    }
}

There are also extensions which conform this to CustomStringConvertable and Codable.

Then we have a StarRatingSet, which is the OptionSet:

struct StarRatingSet:OptionSet {
    typealias RawValue = Int
    typealias Element = StarRatingSet //<-- This is the line I had to add
    var rawValue: Int
    
    static let unknown = StarRatingSet(rawValue: 1 << 0)
    static let star1 = StarRatingSet(rawValue: 1 << 1)
    static let star2 = StarRatingSet(rawValue: 1 << 2)
    static let star3 = StarRatingSet(rawValue: 1 << 3)
    static let star4 = StarRatingSet(rawValue: 1 << 4)
    static let star5 = StarRatingSet(rawValue: 1 << 5)
    
    static let star4Plus:StarRatingSet = [.star4,.star5]
    static let star3Plus:StarRatingSet = [.star3,.star4,.star5]
    static let star2Plus:StarRatingSet = [.star2,.star3,.star4,.star5]
    static let star1Plus:StarRatingSet = [.star1,.star2,.star3,.star4,.star5]
    static let any:StarRatingSet = [.unknown,.star1,.star2,.star3,.star4,.star5]
    
    func contains(_ rating:StarRating) -> Bool {
        return self.contains(StarRatingSet(rating))
    }
    
    var highest:StarRating {
        if self.contains(.star5) {return .star5}
        else if self.contains(.star4) {return .star4}
        else if self.contains(.star3) {return .star3}
        else if self.contains(.star2) {return .star2}
        else if self.contains(.star1) {return .star1}
        return .unknown
    }
    
    var lowest:StarRating {
        if self.contains(.star1) {return .star1}
        else if self.contains(.star2) {return .star2}
        else if self.contains(.star3) {return .star3}
        else if self.contains(.star4) {return .star4}
        else if self.contains(.star5) {return .star5}
        return .unknown
    }
}

There are also some extensions which let you create a set from a rating, and to conform this type to Codable.

As you can see, the types are very similar, and they have similarly named members. They are both necessary for our API though. For example, if you are searching for a hotel, you may want to search over a desired set of ratings (StarRatingSet), but any particular hotel returned should only have (or allow) a single rating (StarRating).

Maybe it was inferring StarRating as the Element for StarRatingSet because .starX has the same name?

At a quick glance I think this might be caused by your custom implementation of the contains method.

SetAlgebra has a customization point:

func contains(_ member: Self.Element) -> Bool

In your case you provide manually an overload that confuses the compiler as Self.Element is redirected to StarRating which would otherwise infer as StarRatingSet.

1 Like

Good spot @DevAndArtist that does indeed look like the cause of the inference failure. Why that is causing the inference failure in 5.0 but not 4.2 though I have no idea – but we do know that associated type inference is a little brittle and some issues were fixed.

@Jon_Hull do you think you could put this into a jira at bugs.swift.org?

To be honest I would consider the behavior in Swift 4.2 as a bug, if this previously compiled and worked. I would expect the compiler to behave exactly as it did now, because the manually implemented customization point should redirect the inference.

Also the enum cases match the static members of the StarRatingSet which causes more ambiguity in some cases.

struct StarRatingSet: OptionSet {
  typealias RawValue = Int
  var rawValue: Int
  static let star1 = StarRatingSet(rawValue: 0)
  static let star2 = StarRatingSet(rawValue: 1)
  static let combined: StarRatingSet = [.star1, .star2]
}

// StarRatingSet.Element inferred as StarRatingSet (correct)

Providing two overloads does not cause the inference to prefer the original assumption that Self.Element should be Self.

// error: Type 'StarRatingSet' does not conform to protocol 'ExpressibleByArrayLiteral'
// error: Type 'StarRatingSet' does not conform to protocol 'OptionSet'
// error: ype 'StarRatingSet' does not conform to protocol 'SetAlgebra'
struct StarRatingSet: OptionSet {
  typealias RawValue = Int
  var rawValue: Int
  static let star1 = StarRatingSet(rawValue: 0)
  static let star2 = StarRatingSet(rawValue: 1)
  static let combined: StarRatingSet = [.star1, .star2] // error: Type '<<error type>>' has no member 'star1'

  func contains(_ rating: StarRating) -> Bool {
    return false
  }

  func contains(_ member: StarRatingSet) -> Bool {
    return false
  }
}

But that's expected, I would argue that because .star1 can be either StarRaring or StarRatingSet this is clearly ambiguous without more context.

Adding typealias Element = StarRatingSet resolves all the issues of course.

SR-10311

I am at work so the report is kind of rushed. Let me know if you need more info...

Terms of Service

Privacy Policy

Cookie Policy