Set sugar + Set-iadic Generics + Dynamic Scope Capturing

Set sugar:

let set = ~[1,1,2] // Compiler error, static set may not contain two identical literals (1 and 1).

let set = ~[1,2,3] // happy

Set-iadic Generics:

struct Container<~[T_n...]> {
    some vars t_n: ~[T_n...] // opaque set destructuring
}

assert(Container<~[Int, Bool, Float, String]> == Container<~[String, Float, Bool, Int]>) // passes
assert(Container<~[Int, Int, Float, String]> == Container<~[Int, Int, Float, Strong]>) // Compiler error: a set may not contain multiple identical type literals.

let c = Container(42, 42.0, "42", true)
let fortyTwo: String = c.t_n // opaque set extraction
assert(fortyTwo == "42") // passes

func myFunc<T: StringProtocol>(_ t: T) { print(t) }
myFunc(c.t_n) // prints "42"
myFunc("\(c.t_n)") // Compiler error: multiple matches within opaque var set conform to CustomStringConvertible. Extract a member of the opaque var set to a local variable of explicit type first, to resolve this error. 

Dynamic Scope Capturing

Given:

let x = 42

let scope = ^scope

assert(scope[\.x] == 42) // passes

Feedback welcome... just some ideas I had recently, not very well-formed admittedly.

1 Like

Re the Set sugar, worth noting that Swift already supports Set initialization via array literals:

[2, 3, 5] as Set  // works

let nums: Set = [1, 2, 3]  // also works
1 Like
let set = ~[1,1,2] // Compiler error, static set may not contain two identical literals (1 and 1).

What is the benefit to making this a compiler error? It's not an error at runtime. In fact, adding an element already in the set at runtime returns a useful value.

1 Like

Curious what you mean? I was not aware that the addition operator is valid for sets in Swift:

let usefulValue = (["nope"] as Set) + (["nope"] as Set) 
// Compiler error: Binary operator '+' cannot be applied to two 'Set<String>' operands.

let hmm = (["nope"] as Set) + "nope"
 // Compiler error: Binary operator '+' cannot be applied to operands of type 'Set<String>' and 'String'.

Why to have Set Sugar via Compiler-enforced Immutable Set Literals

At any rate, the idea is that an immutable set of literals should be invalid if it's not a valid set, from a set theoretical standpoint. On a fundamental level, a set is defined as "a set of different things." That is the basis set theory.

And yet, presently in Swift, you cannot actually directly create a statically-defined set, which would be a necessary precondition to support unordered set-iadic generics.

Consider:

let set = [1,1,2] as Set

What's actually happening here is that the array [1,1,2] is created ephemerally, then it is cast to a Set, i.e. the runtime will implicitly call Set([1,1,2]).

One might argue that this makes perfect sense, but consider the following, which behaves exactly the same way, albeit in a much more deceptive manner:

let set: Set<Int> = [1,1,2] 

I would argue that when you initialize an immutable stored property from a group of literals, the stored group should always have the same number of literal members as what was assigned to it, and if that's impossible then it should be a compiler error, to the extent all the members are literals that the compiler already knows the value of at compile-time.

Clearly the compiler is capable of checking whether it knows something or not, as evidenced by how opaque types work.

I think for any immutable set literal it should never have fewer members or more members at runtime after assignment than it had at compile-time, because that would indicate mutability or magic behaviors.

We should not let people store invalid literals that could very well be simple typos, in cases where the compiler can very easily validate this input in a meaningful way.

Suppose I had written:

let nuclearLaunchAbortValues: Set<Int> = [42, -42, 812, -812, 12, 12]

It would be very unfortunate if a nuclear war ensued because the Swift compiler could not at least place a warning on this obviously invalid set, an immutable value declared with 6 members but which will only have 5 members at runtime.

For set-iadic generics, one of the nice benefits would be to have a set of values mapping to the set of generic types, such that we are guaranteed one and only one value per type—but if two ints are supplied, the compiler needs to be smart enough to realize that's invalid.

This is why I'm suggesting set literal sugar:

let setWithThreeStrings = ~["firstLiteral", "secondLiteral", "thirdLiteral"]
let setWithThreeTypes = ~[Int.self, String.self, SwiftUISheetWithDetents.self]

This tells the compiler this is supposed to be an actual set. (It would not be valid for variables, for obvious reasons. However it could be used for set-iadics, and necessary also because I don't know of another way to represent a set within a parameter list that normally behaves like an array.

You can't use the addition operator specifically but that isn't what Avi meant. Instead you can do something like this:

var set1 = ["yes"] as Set
set1.insert("yes")
print(set1)
// prints '["yes"]'

let set2 = (["yes"] as Set).intersection(["yes"])
print(set2)
// prints '["yes"]'
1 Like

It makes sense, as it's most likely a programmer error.

Interestingly, dictionary literals with duplicate keys compile just fine, but crash at runtime:

let d = [
	"a": 1,
	"a": 2,
]

Swift/Dictionary.swift:826: Fatal error: Dictionary literal contains duplicate keys
Terminated due to signal: TRACE/BPT TRAP (5)

Though diagnosing this at compile time (in the general case) would be hard, because you'd need to be able to do compile-time hashing and equality comparisons.

1 Like

Perhaps if the compiler finds a trap when constant folding, e.g. when inlining the dictionary initializer, it could warn the user that the given code always crashes. (Of course that would only work in some cases.)

1 Like

Well, doesn't it already treat the global scope as a set that cannot have two identical members?

let x = 42
let x = 93 // fails to compile, x is already defined

Why can't the same logic apply within a set?

The analysis required to know if two values can coexist in a collection is completely different than looking up a symbol in a symbol table to see if it already exists. For one, the latter is a simple string comparison.

That's because local variables have nifty little handles that fully characterize their identity: their names.

Values, on the other hand, have no such thing. To know if two values are the same, you need to know that they're the same type (check), hash in the same way (optional, but improves performance), and call == on the two values to check them for equality.

Consider this case:

struct S: Hashable {
    func hash(into hasher: inout Hasher) {
        hasher.combine(0)
    }
    
    static func == (lhs: Self, rhs: Self) -> Bool {
        true
    }
}

let mySet: Set = [S(), S(), S()]

How would the compiler know that this is invalid? Doing so would require it to be able to do compile time execution of the == operator.