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
2 Likes
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.

2 Likes

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.

I was just talking about having the compiler check it when the values are literals, e.g.:

let mySet: Set = ["literalString", "literalString2", "literalString3"] // compiles, all literals are different
let mySet: Set = ["foo", "foo"] // doesn't compile

Wasn't expecting the compiler to be able to check functions since obviously they could give a random result.

However if the compiler cannot make sure a set doesn't contain two literals of the same thing, then it does not seem likely we could have set-iadic generics, since they depend on a set with one (and only one) of each type of variable in it:

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.

Since types like Int are basically just literals, seems to me that the same logic for validating that a set of types has no duplicates could also be used to validate that a set of literals has no duplicates. Am I wrong there?

Maybe it would be better to say, "a set-literal" is a set of literals that is declared all in one place, because we don't want to get into the weeds of adding together random sets of literals as obviously the compiler is not going to make guarantees past the initial declaration.

The main point here is to provide a solution when we want the order of the generic parameters to be irrelevant. In certain situations (like dependency injection) it doesn't matter what order the types of things are in the DI scope, all that matters is whether or not it contains something of a given type.