7

It just occurred to me that when working with subsequences in Swift,

func suffix(from: Int) seems to be identical to just dropFirst(_:) (Obviously, you just change the input value from say "3" to "7" in the case of an array of length "10".)

Just to repeat that. So: of course, for an array of say length ten. What I mean is func suffix(from: Int) with "2" would be the same as dropFirst(_:) with "8", for example.

Similarly upTo / through seem to be identical to dropLast(_:)

Other than convenience is there any difference at all?

(Perhaps in error conditions, performance, or?)

I was wondering whether, in fact, inside Swift one or the other is just implemented by calling the other?

Hamish
  • 78,605
  • 19
  • 187
  • 280
Fattie
  • 27,874
  • 70
  • 431
  • 719
  • 2
    It would appear that they both end up calling subscript method: https://github.com/apple/swift/blob/3f534c254df2b90d176b7daea3cd7a6a9cfbfa5e/stdlib/public/core/Collection.swift – Rob Oct 30 '16 at 21:00
  • @JoeBlow It does – [the subscript method of `Collection` that takes a `Range` input](https://developer.apple.com/reference/swift/collection/1641249-subscript) returns a `SubSequence`. – Hamish Oct 31 '16 at 22:52
  • @JoeBlow Nope, [`suffix(from:)`](https://developer.apple.com/reference/swift/collection/1641744-suffix) also returns a `SubSequence` – Hamish Nov 01 '16 at 09:42
  • 1
    `ArraySlice` is `Array`'s `SubSequence` @JoeBlow :) – Hamish Nov 01 '16 at 09:44

3 Answers3

8

They are completely different.

  • suffix(from:)

  • dropFirst(_:)

    • Defined by the Sequence protocol.
    • Returns a SubSequence with a given maximum number of elements removed from the head of the sequence.
    • Has a documented time complexity of O(n)*. Although its default implementation actually has a time complexity of O(1), this just postpones the O(n) walk through the dropped elements until iteration.
    • Returns an empty subsequence if the number you input is greater than the sequence's length.

*As with all protocol requirement documented time complexities, it's possible for the conforming type to have an implementation with a lower time complexity. For example, a RandomAccessCollection's dropFirst(_:) method will run in O(1) time.


However, when it comes to Array, these methods just happen to behave identically (except for the handling of out of range inputs).

This is because Array has an Index of type Int that starts from 0 and sequentially counts up to array.count - 1, therefore meaning that a subsequence with the first n elements dropped is the same subsequence that starts from the index n.

Also because Array is a RandomAccessCollection, both methods will run in O(1) time.

Hamish
  • 78,605
  • 19
  • 187
  • 280
  • Hamish, I think you truly answered the question right here: **"However, when it comes to Array, these methods *just happen* to behave identically (except for the handling of out of range inputs)."** Totally amesome - thanks. Hamish is even my nephew's name :) – Fattie Nov 14 '16 at 14:10
  • Happy to help @JoeBlow :) – Hamish Nov 14 '16 at 15:36
2

You're right that they're connected, but yes there is a difference. From the docs:

let numbers = [1, 2, 3, 4, 5]
print(numbers.suffix(2))
// Prints "[4, 5]"
print(numbers.suffix(10))
// Prints "[1, 2, 3, 4, 5]"

versus

let numbers = [1, 2, 3, 4, 5]
print(numbers.dropFirst(2))
// Prints "[3, 4, 5]"
print(numbers.dropFirst(10))
// Prints "[]"

In the first example, suffix(2) returns only the last two elements, whereas dropFirst(2) returns all but the first two elements. Similarly, they behave differently wrt. arguments longer than the sequence is long. (Additionally, suffix will only work on finite sequences.)

Likewise with prefix and dropLast. Another way of thinking about it, for a sequence of length n, prefix(k) == dropLast(n-k), where k <= n.

Fattie
  • 27,874
  • 70
  • 431
  • 719
Colin Barrett
  • 4,451
  • 1
  • 26
  • 33
  • Hang on Colin! (Note my new example in bold in the question.) In your example, to go from suffix to dropFirst, you'd use "2" then change to "3". "10" you would change to "0". (Well, "-5", but we'd understand to make it "0" in such conversions.) They would **seem to be identical.** – Fattie Oct 31 '16 at 21:33
  • To be clear! You see your final sentence: "Another........." that is in fact precisely what I'm asking: are they ACTUALLY the same thing, prefix(k) === dropLast(n-k) ?? – Fattie Oct 31 '16 at 21:34
  • I'm not sure how functions that produce different output with the same arguments are identical? – Colin Barrett Nov 02 '16 at 21:00
  • Hey @ColinBarrett ! - right, of course I meant trivially mapping the relevant argument. In any event, Rob above seems to have answered the question - yes, they're identical, they call the same substrate. – Fattie Nov 02 '16 at 21:03
  • @ColinBarrett Note that your answer is talking about `suffix(_:)`, not `suffix(from:)` – which is what OP is asking about. – Hamish Nov 02 '16 at 21:45
2

Biggest difference IMO is that dropFirst() doesn't expose your code to out-of-range index errors. So you can safely use either form of dropFirst on an empty array, while the prefix / suffix methods can crash on an empty array or with out-of-range parameters.

So dropFirst() FTW if you'd prefer an empty array result when you specify more elements than are available rather than a crash, or if you don't want / need to check to make sure your the index you'll be using is less than the array.count, etc.

Conceptually, I think this makes sense for the operation as named, considering that first is an optional-typed property that returns the first element if it exists. Saying dropFirst(3) means "remove the maybe-present first element if it exists, and do so three times"

Daniel Hall
  • 13,457
  • 4
  • 41
  • 37
  • Hold it - that would be awesome, but unfortunately `dropFirst()` immediately (and entirely stupidly) crashes on negative numbers. – Fattie Nov 02 '16 at 21:17
  • But yeah, you're 100% right that Apple graciously made it "pointlessly crash less". – Fattie Nov 02 '16 at 21:18
  • @JoeBlow It _is_ surprising that it doesn't require a UInt. I supposed I should clarify that it won't crash with any "logically feasible" / non-negative parameters. – Daniel Hall Nov 02 '16 at 21:19
  • You know, it would take some thought to figure out if - in fact - they both crash "in the same way". You know what I mean ?! heh. Rob has critically pointed out that they (apparently) use the same underlying code, so :/ I can't read computer code or documentation, so I can't work it out. (It's **astoundingly absurd** that for such a simple operation, you have to rewrite your own calls :/ ) – Fattie Nov 02 '16 at 21:20
  • 1
    @JoeBlow I don't think it's pointlessly crashing less. When doing functional-style chaining on an array, e.g. `array.filter{ $0 > 10 }.dropFirst(3).reduce{ + }` you don't have to insert additional lines of imperative range-checking in the middle. I think for many chained operation cases, `dropFirst()` is a distinctively better option. – Daniel Hall Nov 02 '16 at 21:22
  • 1
    @DanielHall It's not actually not really surprising that it doesn't take a `UInt` – the Swift team recommend using `Int`s even when the values are known to be non-negative, as it aids interoperability. – Hamish Nov 02 '16 at 21:22
  • 1
    @Hamish I can see that, but I think that interoperability will eventually be a relic, and even now interoperability trumping type checking and safety in this case is...questionable – Daniel Hall Nov 02 '16 at 21:24
  • I think the whole thing is unbelievably stupid. There is never, ever, ever, ever, ever, ever, ever, ever, ever any situation where you would want anything other than the obvious result. (ie, return a nil set and/or range, at worst). if I ask you as a human "what's the billionth item in this set of ten" the answer is **the empty set**. Anyway just a rant. – Fattie Nov 02 '16 at 21:26
  • @JoeBlow Personally I find `dropFirst()` to behave exactly as expected. If I have an array of 2 elements and drop the first three elements encountered, I get an empty array. If I started with an empty array and drop the first 10 elements in it, I get an empty array. Or if I have a 10 element array and drop the first 6, I get a 4 element array. Makes sense to me and is useful when I don't want to specify the "obviously, only drop elements until you run out of elements" part imperatively. Whereas prefix/suffix/range-based functions imply strict valid start and end requirements. – Daniel Hall Nov 02 '16 at 21:30
  • @DanielHall Yeah, I do agree that the design choice is questionable – trading convenience for safety does seem to somewhat contradict the ideals of Swift. – Hamish Nov 02 '16 at 21:34
  • 1
    Hey DH and H, I clicked a bounty to say thanks, stupidly on SO you can't click out two bounties. I think the insights you two guys worked out here are quite incredible. Amazing stuff! Cheers – Fattie Nov 16 '16 at 14:44
  • Thanks @JoeBlow! Appreciate the bounty and the discussion you kicked off! – Daniel Hall Nov 16 '16 at 23:39