"nondeterministic order" annotations to make it explicit in the code when the order of something is not deterministic (e.g., when converting a set to a list)

So if you have a set s say, you would need to use something like Array(s.nondeterministicOrder) to convert it to an array.

It's very easy to forget when an order is nondeterministic and run into strange bugs because your code depends on deterministic order.

If you need a set with elements in a well-defined order, consider using OrderedSet from the Swift Collections library.

It's not the conversion from a set to an array that causes you a problem if you "forget" that Set isn't an ordered collection type—indeed, that conversion actually fixes the element order—but rather the problem is that your value was an instance of Set to begin with.

2 Likes

But the conversion doesn't fix the element order. You could get different orders from the same set.

An array holds its elements in a well-defined order; if you convert a set to an array, then you no longer have an issue with the order of elements being arbitrary.

Also, note that if you do not mutate a value of type Set, you can iterate over it any number of times and you will always get the elements in the same order.

Iteration over a set is not deterministic. You can get different results even when the executions up to that point are identical.

Allow me to be very precise in my meaning:

Within the same execution of a program, given a unique, immutable binding s to a value of type Set—that is, let s = Set(...)—it is guaranteed that for _ in 0..<x { print(s.first) } prints the same element x times.

But if you have something like:

let s = Set([1,2,3,4,5,6,7,8,9,10])
let s2 = Set([1,2,3,4,5,6,7,8,9,10])

Then the order is usually different for s and s2. This can result in hard to debug issues because it may not be something you expect/remember.

An explicit annotation would remind you if this and also allow you to search for nondetermistic orderings in your code.

See my first reply: if you're expecting to use a ordered collection, then consider OrderedSet; the error is in creating a value of type Set, not in converting the value to an array.

4 Likes

I don't want to use external libraries.

Moreover, the problem is more about writing code that results in hard to debug issues.

If you are constantly being reminded of this behavior via explicit annotations in conversions/iteration, then you would write code that works correctly given this behavior.

Any usage of a Set is a perfect indicator of an arbitrary ordering of values. You can just search for usages of Set rather than needing some additional annotation. If you’re worried about the ordering of values in a Set to begin with that is likely a sign you should use an OrderedSet instead as mentioned.

3 Likes

The use of Set is not necessarily an error. But conversions/iterations on sets can introduce hard to debug problems. That's why the explicit annotation would go where you do the conversion/iteration.

I would suggest that if the ordering of values is important to maintain then using a plain old Set would indeed be an programmer error since that data type does not provide such a guarantee.

8 Likes

It may not be obvious that the ordering of values is important though. Having explicit annotations would make you think of this issue more.

If the boundaries of where sorting is important are not clear in your codebase then someone may knowingly choose to use Array(s.nondeterministicOrder) thinking it won’t cause any issues, so it doesn’t really fix the problem.

I don't trust external libraries as much as I trust what Apple ships as part of Xcode.

Also, nondeterministic Set behavior is something that Apple introduced in a later version of Swift, so not everyone knows about it and even those who do know about it could temporarily forget it while coding.

Constant reminders via explicit annotations will help you keep this issue in mind and avoid hard to debug problems.

Also, you can use a search tool to find nondeterministic orderings in your code via the explicit annotations when you suspect a bug might be caused by one or more of them.

you have far too much trust in what Apple ships with XCode :slight_smile:

but i agree with you that introducing a swift-collections dependency is probably overkill. as many others in this thread have mentioned, your data should probably be stored in an Array. you can wrap this array in a user-defined type that also stores a private Set that it keeps in sync with mutations on the array.

1 Like

I am getting old and have seen this sort of thread a lot. Someone uses the pretense of being irritated about the usability of some tool, due to poor quirk discoverability, but is actually looking to use the mnemonic capabilities of argument with other people to cement whatever needs to be remembered in order for the tool to virtually have improved usability. Does this trope have a name?

1 Like

it don’t know if it has a name, but i would say if it helps someone learn swift better, then the forums are functioning as intended…

3 Likes

I can see your point. You are using an array initializer:

Array(...) == Array(...)

and depending upon the initializer either getting a consistent "true":

Array(arrayLiteral: 1, 2, 3) == Array(arrayLiteral: 1, 2, 3)

or inconsistent random result:

Array(Set([1, 2, 3])) == Array(Set([1, 2, 3]))

It's easy to foresee bugs when someone refactors the code:

let s = Set([1, 2, 3]) 
Array(s) == Array(s)

or goes from the latter to the former form, changing the behaviour along the way potentially without realising it. Or just without realising the order is non deterministic.

What you can easily do is define this extension:

extension Array where Element: Hashable {
    init(nondeterministicOrder elements: Set<Element>) {
        self.init(elements)
    }
}

and use it consistently throughout your code base, along with agreeing with your team members (if any), to use it instead of the original. It won't be possible (without modifying swift interface files, or, perhaps, some custom swift lint rules?) marking the original initializer deprecated, so some discipline would be required.

Another option would be to have a two-in-one initializer of the form:

init(_ elements: Set<Element>, sort: (Element, Element) -> Bool)

with a non optional "sort" parameter and some explicit opt-in option for the current non-deterministic behaviour:

Array(Set([1, 2, 3]), sort: {$0 < $1}) // with sorting
Array(Set([1, 2, 3]), sort: .nonDeterministic) // without sorting
Array(Set([1, 2, 3])) // Ideally warning or Error here: obsolete, set order explicitly
2 Likes

shifting implicit features to be explicit by default.