Hacker News new | past | comments | ask | show | jobs | submit login
Type Erasure in Swift (mikeash.com)
76 points by ingve on Dec 15, 2017 | hide | past | favorite | 31 comments

> For example, if you want to write some code that accepts any Sequence of Int values, you can't write this:

> func f(seq: Sequence<Int>) { ...

> […] Swift provides the AnySequence type to solve this problem. AnySequence wraps an arbitrary Sequence and erases its type, providing access to it through the AnySequence type instead. Using this, we can rewrite f and g:

> func f(seq: AnySequence<Int>) { ...

> […] The Swift standard library has a bunch of these Any types, such as AnyCollection, AnyHashable, and AnyIndex.

Please excuse my ignorance, but what’s the point of this?

Have all these <SomeType> and their Any<SomeType> counterpart seems like unnecessary code duplication to me.

The biggest reason is that you cannot write `var someVariable: SomeGenericType<Type>` nor use it as a parameter or a return in a protocol, for example.

For another type to reference a generic type, it would either have to be generic itself, use a type erased `AnySomeGenericType<Type>` reference, or specify the exact version that's being used, e.g. ConcreteGenericType<Type>, which makes your code wayyyyy less abstract and entangled with another type.

This is very much something the language should handle, it just hasn't yet, so you have to write these large complicated boxing and unboxings.

"The biggest reason is that you cannot write `var someVariable: SomeGenericType<Type>` nor use it as a parameter or a return in a protocol, for example."

What's the reason for that? In Java, for example, interfaces can be generic in just the same way concrete types can, which also means you can declare variables and parameters of some specific instantiation of the interface. It looks like Swift has two different generics systems, one for concrete types and one for protocols, leading to this extra complexity. Presumably there's some reason for doing things this way.

As with many deficiencies in Swift, the reason is just that it takes time and effort to make it happen and it hasn’t been done yet.

Code duplication yes, but it's not unnecessary. You need them so you can write code that works on Sequences and other such things without knowing the actual implementing type.

Ideally this would not be necessary, and in some future version of Swift it probably won't be, but for now it is.

In the function input scenario, it's more like an alternative to a generic type Elements: Sequence with the Element constrained to Int. Either would work.

There are cases where this is not possible, like when you are returning a sequence from a function whose API you don't want to bind to a specific type. Then the only thing you can do is to pass an AnySequence.

Perhaps you'll be interested in someone's reverse engineering of how AnySequence was implemented?


Not sure why it was reverse engineered when the source code is publicly available: https://github.com/apple/swift/blob/4ddac3fbbd5b9ba8d980fa88...

Swift was open-sourced in December of 2015, which was a few months after that gist was written.

Will swift not get the `impl trait` that Rust is slowly churning out?

I believe it's coming eventually, but for the moment we need workarounds like this.

It looks like trait objects could also work for the essay, e.g. the original examples could be achieved as:

    fn f(seq: &mut Iterator<Item=&u8>)
although due to object-safety constraints they may not be suitable for every case.

To further my understanding this would be akin to trait objects in Rust right? Instead of having generic type constraints in the function signature you can `Box` your traits or use pointers to a trait and use dynamic dispatch. I'm sure there are also some differences as well and would like to hear them.

If you're interested, there's a very nice talk on how Swift's generics compile. It's different to Rust in some key ways.


Thanks for posting this, I've been meaning to watch it but keep forgetting about it.

The direct analog of Rust's trait objects is Swift's protocol objects; call them existential types. Both Rust and Swift have limitations on which traits (protocols) can be used as an existential.

Swift forbids existentials when the protocol has an associated type. Rust has a notion of "object safety" which is both more flexible and more complicated, but applies the same restriction in most cases.

I think the closest analog of Swift's `AnySequence<Int>` would be a `Box<Iterator<Item=Int>>`.

> func g<S: Sequence>() -> S where S.Element == Int {

I think Rust can do this case. I think this is how a simple version of `collect` would work (or very nearly).

Saw Erasure and was expecting this instead


Is this different from structural typing?

Why would you even think it has any relation?

Condescending much?

Because it sure sounds similar. Look at Python's PEP 544, for example.

The only commonality I can see is that both use the word "protocol"…

AnySequence is a behavior specification, right? That seems pretty structural to me.

AnySequence is a concrete type conforming to the Sequence protocol. It implements Sequence by forwarding calls to some other implementation of Sequence.

Strictly speaking, AnySequence is a generic type, whereas AnySequence<Int>, AnySequence<String>, and AnySequence<Any> would be three different concrete types.

AnySequence is a named type conforming to a named protocol, it's all nominative. I don't know that Swift even has a structural subset.

I recently spent a couple of weeks in Swift land to see what it's all about; but from my experience it's still a mine field of exceptions and hoop jumping. I feel that the problem with both Rust and Swift is that they're aiming so hard for perfection that they're missing the point of the exercise; which is to make it easier to write good code, period. Wasting half your energy-budget on spoon feeding the compiler is just as bad as spending it on managing memory manually in C.

> Wasting half your energy-budget on spoon feeding the compiler is just as bad as spending it on managing memory manually in C.

This isn't quite a fair comparison. Manual memory management increases the risk of producing incorrect or buggy programs. Satisfying a strict compiler increases the chance that your program is written correctly.

And for what it's worth, this boilerplate dance for "type erasure" will be solved with what the Swift team calls generalized existentials, where you'll be able to use nominal types like Any<Sequence where .Iterator.Element == String>. "Type erasure" isn't the result of "aiming so hard for perfection" as you say, but because Swift is still a young language and there is only so much the creators can work on at any one time. Most Swift devs (think iOS apps) aren't running up against this problem much, and I haven't seen a single situation in an app where implementing a "type erased" generic type was necessary. At this point in the Swift timeline, just making Swift code compile faster would make devs much happier.

> it's still a mine field of exceptions

Unlike languages that actually use exceptions, which really are mine fields of exceptions in a different sense :P

I think the reason swift and rust are mine fields of hoop jumping are opposite :

rust took a few very opinionated design decisions (eg : strong safety in concurrency), and derived everything else from those principles, trying to reach something "user friendly" in the end, and is not there yet.

Swift always claimed to be a very "pragmatic" language, not sacrificing user convenience to purity. The problem is that they grabbed good ideas a bit everywhere, but still struggle to reach enough power in the language to become truly user friendly when the user start using all those bits to solve their particular problem (eg : generics and protocols, or concurrency, or typed exception, or result type, etc.).

Hmm, Have you looked at Crystal? It comes so very close to the sweet spot for me that I don't really know the difference.

Guidelines | FAQ | Lists | API | Security | Legal | Apply to YC | Contact