[Proposal] Set literals

It's way better than a bunch more colons, too.

let c = [
    ["s1": [["a", "b"], ["c"]] as Set],
    ["s2": [["c"], ["d", "e"]]],
    ["s3": [[]]]
] as Set
let c: Set<[_: Set]> = [
    ["s1": [["a", "b"], ["c"]]],
    ["s2": [["c"], ["d", "e"]]],
    ["s3": [[]]]
]

I think this thread exists because of relative lack of prominent learning material featuring not-Array ExpressibleByArrayLiteral types. "First-class" is in the familiarity of the beholder.

4 Likes

can be taken a step further, too:

let c: Set<_> = [ ... ]

is sufficient.

Edit: This is not correct; I misread the type annotation above — Set<_> here would be equivalent to Set<[_: Array]>, not Set<[_: Set]>.

1 Like

Indeed, but if Swift supported compile-time evaluation (a la constexpr) then it could actually be addressed in a general fashion.

It is inherently different, though - static vs dynamic behaviour. It's definitely suspicious to define a set of values all in one time & place and to have duplicates in there. To the reader it creates distraction and confusion as it's not clear if there is a typo or a logic error or some other flaw.

Technically it's also wasteful, although I think in most cases that's insignificant.

Of course, sometimes by the nature of complex code you end up with duplicates, but you don't necessarily want the compiler to complain about it. Given abstraction and indirection through constants and macros and other such things.

Thus, I think this is less something that should be tied intrinsically to Set and more something you elect to use on a case-by-case basis. I think that's what the proposed :[] syntax was getting at.

However, there may be a better way - if Swift gains compile-time evaluation, one could simply write e.g. a constexpr func unique<Element: Equatable>(_ elements: Element…) -> Element… which fatalErrors if duplicate elements are encountered.

There might also be synergy with compile-time layout of Sets (IIRC currently only the list literal is baked into the const data segment, and at runtime the relevant collection's init is called to actually construct the final in-memory form). That would also address binary size wastage due to duplicate literals.

1 Like

No, that's a different type. These are all the same.

let c: Set<_> = [
let c: Set = [
let c: Set<[_: Array]> = [
let c: Set<[_: [_]]> = [

You're absolutely right! I misread the code. Will fix.

Introduced in 5.6, _ inside a generic type means ā€œinfer this part of the typeā€. Mainly used when only some parts can be inferred, e.g.

let x: [_: [String]] = [1: []]
6 Likes

First-class is when you don't need to typecast every time :slight_smile: For the same reason I prefer 1.0 to Double(1), now that's first-class, Double(1) as the only way wouldn't be.

1 Like

There is no casting here. let x = Double(1) is nowadays equivalent to let x = 1 as Double, which as I discussed above is equivalent to let x: Double = 1. Note that Double.init(1), however, does involve casting (from Int to Double).

6 Likes

Whether there's a physical typecast or not, what I meant was let's say syntactic typecast. 1.0 is not the same as 1 as Double, it's just... shorter. The less you type and read the better.

1 Like

No, I am not speaking about what the machine actually executes. What I'm saying is this: neither Double(1) nor 1 as Double is the syntax for a casting operation—that is a common misunderstanding.

It was a deliberate choice on the part of Swift to reject C precedent where you can write 1.0f to indicate the type. Instead, in Swift the spelling is 1.0 as Float.

2 Likes

I'm sorry we seem to be talking about different things.

One of the reasons I love Swift is how easily it adopts all kinds of syntactic shortcuts, from trailing closures to optional return etc, there are very many of them. Shorter code and fewer keywords and special characters make the language nicer and more readable. The semicolon was dropped for the sake of it, to remove another unnecessary mental tax of reading!

So, just like 1.0 always, always wins against 1 as Double for me,

let s = :["a", "b", "c"]

(or something similar) wins against

let s = ["a", "b", "c"] as Set
// or
let s = Set(["a", "b", "c"])

The whole point of this post is to show that sets (1) deserve to have a syntactic shortcut and (2) should be and will be used more often if they have one.

As codebase grows, it is actually better to have type being explicitly declared, as it helps with compilation time and simplifies reading the code (since you as well have to infer types as you read the code).

Shortcuts in languages, despite their usability in some cases, has a long history of ambiguity, hidden bugs, and other complications, for instance, I cannot remember any new language (say Swift age) that allows omitting figure braces as C does:

if (someFlag) callFunc();

Just as one of the examples. Or Swift's removal of increments and decrements. Or part of languages removal of ternary operator. And list goes on.

3 Likes

It's because those languages dropped the semicolon and now need to resolve ambiguity, not because it's "bad" to have a single-statement block without curly braces.

That's a very broad assertion that is actually wrong. As a codebase grows, nothing really changes if you keep up the good quality of your code. You won't notice a difference in compilation time if you use 1.0 instead of Double(1) and actually it's not even obvious which way it will go (in terms of build times).

Another very broad and unprovable assertion. Shortcuts added tastefully like they are done in Swift, only improve readability.

It is exactly because it is bad – the number of issues created because of this is huge. Languages that still has semicolons (Rust) has still omitting disallowed exactly due to issues it brings, not because they can't resolve ambiguity.

You can search only by this forum for "slow compilation" how many times this issue were faced. Each type inference adds up, and while for something like integer or double this is actually negligible, for collection types it can have huge impact. But even math operations for long has been resulting in compiler just giving up on resolving type on complex expressions. And still can if you eventually put Int inside expression on Doubles, making compiler confused.

You can find tons of example how omitted curly braces has created major issues in codebases, even when maintained by extremely skilled developers. Or using ternary operator to call void functions. And so on.

On the other hand, improved readability here is doubtful, as you have to bear additional cognitive load even to read something as simple as

let x = 1.0

While this statement seems to be an easy one, for a reader to evaluate, it requires to understand that 1.0 literal is for double, not float type, apart from other things. This example too simple to say it is actually hard to understand it, but as you go through a lot of code, keeping in mind various details, inferring type for each statement you read is load you can escape as well.

Ternary operator, probably, better example of how readability is declines with shortcuts:

let x = a ? y : b ? z : t

vs

let x: Int
if a {
    x = y
} else if b {
    x = z
} else {
    x = t
}

Despite the need to write a little bit more code, readability has improved a lot, with a bonus of keeping away from making precedence mistake somewhere in the middle.


On a side note, I don't see this as a productive discussion to just throw accusation of "everything said here wrong" just because you don't think so. It would be much better to have examples to support such claims.

1 Like

Not to me, no. Sorry for this analogy, but your second form reminds me of kids that count the steps as they go up or down the stairs.

@vns has provided a good example to illustrate the point.

Although I like the first form and always use it, because I like math and abstract things, I find the second form easier to read and understand, not to mention the fact that it is much easier to debug. :slight_smile:

Yes, programming is about this type of compromises. Regular expressions are an extreme example of unreadable mess, just try to find a canonical RE for validating email addresses :slight_smile:

However, all disputes aside, I wouldn't hire someone who has difficulty reading and understanding

let x = a ? y : b ? z : t

let alone making a mistake in it that would require step-by-step debugging. The more verbose form of it takes longer to read and understand because - well, it is longer. If you are not comfortable with the ternary operator or you don't have the intuition to know when to resort to if's, it means sorry, I'll pass and won't hire you.

Just like I wouldn't hire someone who thinks that

if cond {
    return true
}
else {
    return false
}

is "better" because it is more "readable". I actually had arguments around this (online) and not once. Even those who actually know how booleans work were telling me the above is "better"! That's how irrational it can get.

In any case, anything that can shorten the code without making it too cryptic <- and this is where it gets tricky of course, but anything that can make your code shorter is good unless proven otherwise, not the other way around.

A slight modification to make it more entertaining:

let a: Int? = 1
let b: Int? = nil
let x = a > 0 ? y : b < 0 ? z : t

That won't compile as comparative operators not allowed for optionals. How we can modify that one?

let x = (a ?? 0) > 0 ? y : (b ?? 0) < 0 ? z : t 

Probably, correct? Maybe yes, maybe not. Maybe, better to have something like:

let x = a != nil ? a! > 0 ? y : -1 : b != nil ? b! < 0 ? z : t : -1

And this can be tweaked a bit depending on what you want to have if, e.g. a == nil: -1 or else clause? But to just parse what will happen here in the latter case requires non-trivial amount of work.

1 Like

perhaps it would help to link the proposal that first introduced this (strange but probably unavoidable) behavior:

4 Likes

BTW, as pointed in parallel thread, that is not what Swift aims for at all:

  • Clarity is more important than brevity. Although Swift code can be compact, it is a non-goal to enable the smallest possible code with the fewest characters. Brevity in Swift code, where it occurs, is a side-effect of the strong type system and features that naturally reduce boilerplate.
3 Likes