Poll: what do you strongly dislike about Swift?

I have a couple complaints, all of which are non-issues in TypeScript.

I worked on a dependency injection library. The crux of the library is a single method, resolve:

public protocol Resolver {
    func resolve<T>(_ type: T.Type, name: String?) -> T
}

In principle, the library could store a dictionary and return dict[(type, name)]!. It doesn't do that for several reasons, but that's the core of the idea behind it.

First problem: no default values in protocols. The vast majority of the time, the name is going to be nil, so it doesn't need to be provided. In TypeScript, I can do this easily:

// TS doesn't have an equivalent to Swift's `T.Type`, so you do this instead.
interface Constructor<T extends object | null> {
	constructor(...args: any[]): T;
}

interface Resolver {
    // By typing `name?: string` rather than `name: string | undefined`, it defaults to `undefined`.
    resolve<T extends object | null>(type: Constructor<T>, name?: string): T;
}

Now, onto the issue of how to store it. It can't be stored in a dictionary or dictionary-like container, since there's no way to represent the behavior at the type level -- at least, not more safely than [(Any.Type, String?): Any]. Instead, it stores an array of registration objects, which I want to model as:

public protocol Registration<T> {
    associatedtype T
    var name: String? { get }
    func resolve(_ resolver: Resolver) -> T
}

extension Registration {
    public var type: T.Type { T.self }
}

However, this runs into a problem when implementing the resolve method. It seems deceptively straightforward:

private var registrations: [any Registration]
public func resolve<T>(_ type: T.Type, name: String?) -> T {
    for registration in self.registrations {
        // You can't do `as? Registration<T>` because the associatedtype doesn't work like that...
        if regstration.type == type && registration.name == name {
            // ...but because I haven't cast it to a concrete type, this next line is illegal:
            return registration.resolve(self)
        }
    }

    preconditionFailure("No matching registration found")
}

This isn't a problem in TypeScript because you can write your own typecheck function:

interface Registration<out T extends object | null> {
	readonly type: Constructor<T>;
	readonly name: string | undefined;
	resolve(resolver: Resolver): T;
}

class Factory implements Resolver {
	private readonly registrations: Registration<object | null>[] = [];

	private checkRegistration<T extends object | null>(
		registration: Registration<object | null>,
		type: Constructor<T>
	): registration is Registration<T> {
		return registration.type === type;
	}

	resolve<T extends object | null>(type: Constructor<T>, name?: string): T {
		for (const registration of this.registrations) {
			if (this.checkRegistration(registration, type) && registration.name === name) {
				return registration.resolve(this);
			}
		}

		throw new Error('No matching registration found');
	}
}

This highlights the second problem I have: Swift errs so hard on the side of type-safety, its associated types don't even work right. I originally wrote this in Swift 5.5 or 5.6, so I was hoping 5.7 would fix it, but unfortunately it didn't. The result is code littered with Any and as!, that's only safe if you follow the documentation and don't implement your registration like this:

struct Bad: Registration {
    var type: Any.Type { String.self }
    var name: String? { nil }
    func resolve(_ resolver: Resolver) -> Any {
        return 5
    }
}

I've made as much of the user-facing API generic as possible, even providing a generic wrapper around the Any-based registration, but I still can't prevent this from compiling.

My final gripe is that you can't say this:

func f<T1: AnyObject, T2: T1>(_ arg: T2) -> T1 {
    return arg
}

This is another source of Any.Type in that project, and as much as I can do checking at runtime, I was very disappointed that the compiler offered no help here. Again, TypeScript has no problem with this,

function f<T1, T2 extends T1>(arg: T2): T1 {
	return arg;
}

and even lets you forgo the requirement that T1 be a class -- you could call f<boolean, boolean>(true).

2 Likes

You should be able to cast a any Registration to any Registration<T>, and then call resolve() on it and get back a T.

2 Likes

You can make use of ObjectIdentifier(T.self) as a dictionary key and pair that up with the String? value however you like, such as in an internal key type like struct Key: Hashable { let typeID: ObjectIdentifier; let label: String? } and a var dict: [Key: Any]. You still need Any but the as! cast becomes safe when you control with a type parameter what's stored in dict.

(This is similar to what SwiftUI does in EnvironmentValues, for example.)

1 Like

Can you? This project had to support iOS 14. I'd be unsurprised if this works in iOS 16+, but I don't expect that check to backport well. I already had to deal with the surprise that Collection<Int>.self == Collection<String>.self.

It's true that dynamic casts involving constrained existentials don't backward deploy, which has been discussed numerous times on here, but that's not a limitation of the static type system.

1 Like

In that case, I have to agree with what was said upthread about language versions being tied to OS versions. Every new version of Swift feels like "here's a cool new feature you can't use."

6 Likes

Maybe I'm misunderstanding the issue, but can't you just write:

public extension Resolver {
    func resolve<T>(_ type: T.Type) -> T {
        resolve(type, name: nil)
    }
}
1 Like

I can, and I did. I just don't like that I have to.

The code below can be complied:

@resultBuilder
struct Build {
    static func buildBlock<T>(_ components: T) -> [T] { return [] }
}

@propertyWrapper
struct Wrapper<T> { var wrappedValue: T }

@globalActor
final actor Atomic {
    static var shared = Atomic()
}

struct ST {
    @usableFromInline
    @warn_unused_result
    @MainActor
    var call: (@Atomic @Sendable @convention(c) () -> Void)? = nil

    var val: Int {
        mutating get { 0 }
        nonmutating set {
        }
        mutating _modify {
            var val = 0
            yield &val
        }
    }

    @_spi(abv)
    @discardableResult
    @inline(__always)
    @_effects(readonly)
    @_eagerMove
    @_disfavoredOverload
    @_alwaysEmitIntoClient
    @_silgen_name("????")
    @_specialize(where T == Int)
    public nonisolated __consuming func foo<T>(@_implicitSelfCapture 
                                               call: 
                                                   @escaping
                                                   @Atomic 
                                                   @autoclosure 
                                                   @Sendable 
                                                   () -> Void, 
                                               @Build 
                                               build: () throws -> [T], 
                                               @Wrapper 
                                               wrappedValue: isolated inout T
    ) async rethrows {}
}

I can add more esoteric keywords.
I really want to throw up.

2 Likes

I believe it is going to get worse.

1 Like

Answering the question "What most needs to be fixed?", I've noticed that there doesn't seem to be any true Swift experts being paid to address complex topics in a structured manner.

Apple documentation seems conservatively simple and pared-down, likely to avoid noise and confusion in such a large audience. Outside Apple, there are a few good people doing the tutorial/site subscription/book thing who explain the basics to the large numbers of beginning/intermediate people just looking to get things done. Some others explain the API's they are promoting, but not core Swift issues. Some good discussion happens in forums, but it requires a lot of context and filtering. So there's a gap on the high end.

Swift operates as much from practice as principle, creating a broad surface that has to be learned by feel instead of instruction. Figuring out the UIKit/SwiftUI boundary, or what result builders really accept, or what open-source Foundation actually does, or how to think about existentials, or best practices on Linux or Windows, or a host of async patterns, or just how to get a reasonable stack trace -- all are unduly complicated, in part because the landscape is shifting.

I get that the people who know this have better things to do than write, and the audience is likely smaller. But I can't help believing there's something amiss when a strong market need is going unfulfilled: what's the blocker?

Perhaps it's a solved issue for teams, where relevant expertise is shared quickly, and individual developers are mostly on the junior side.

Or am I wrong, and I just haven't found the good sources?

5 Likes

I think platform-specific things like UIKit/SwiftUI do not belong to the documentation on swift.org. And I would say the Swift book is really good, and there are also some other useful links and pages in the documentation section of swift.org, also see the “open source efforts” section e.g. about Swift on the server. But there are indeed some things missing:

Yes, there is too little guidance on with swift.org with those kind of things. I personally miss a lot explanations of intentions, why some things in Swift are the way they are. And there are a few nice-to-know things besides the l language itself e.g. about how to easily visualize the dependencies of a package that you do not easily find as a beginner. (I put together some of those things for my co-workers — which may be new to Swift — in my own “Swift Guide”.)

That said, the Swift website is also a community effort and we can help making the documentation better.

Writing good documentation is hard and time-consuming. And there also used to be a lot of other books about Swift, but Swift is evolving rapidly, so I think publishing books about Swift is no fun. I think the Swift website is the way to go.

3 Likes

What is the Swift book? Where can I get/read it?

The Swift Programming Language

As someone who very much considers himself a newbie on Swift, the biggest obstacle to adding more Swift to my existing, mixed Obj-C/Swift codebase is the absolutely, unbelievably poor performance when compiling. And it doesn't even feel like the increase is linear... meaning that with each Swift file added to the workspace, the compiler seems to gift me an exponential slowdown. It can take seconds to report/hide "live" errors.
I could obviously write about what I like... or try and provide decent factual evidence to back the general feeling that the entire thing seems to be held by tape, but if I could just have one wish, it would be that I can at least continue adding Swift sources at the current rate. Reasonable compilation times are key to making that possible.

  • use type annotations for any binding that can take one. i do this for readability, not compile times, but it is also very helpful for improving compile times.

  • upgrade to a newer toolchain (e.g. 5.8). typechecking performance has increased dramatically since 5.5. imo this has been one of the few and most impactful changes in swift in two years, and it has largely flown under the radar because it is not a shiny new feature that can be written about in blog posts or news articles.

  • break up large source files into many smaller source files, which aside from being better for organization in general, also apparently impacts compile times (independently of whole-module optimization), to an extent i did not appreciate until very recently.

there is not one single magic bullet for improving compile times. i once (2016-2020?) had a very dour assessment of the swift compiler because of very poor compiler performance. but today i think it can be mitigated effectively, to the point where i no longer consider compilation speed to be a drawback of the swift language.

hooray?

5 Likes

I'm (sadly) already on the 5.8 toolchain, and my sources are very small already (that‘s one of the pluses of Swift, the resulting code is brilliantly short). I haven't taken to annotating expressions with type information, but honestly this feels like a big step backward. Automatic type inference (I hope I'm using the correct term) is one of the selling points of the language, would be a shame to limit yourself that way.

I’ve seen mentioned elsewhere that there are inefficiencies that are specific to framework targets... but that’s not something one can just eliminate from a codebase.

type inference

Without type inference, generic is unusable. There is no way you can hand annotate correctly all the generic type info. So slow type inference is unacceptable and adding explicit type info is not a solution.

1 Like

i have been doing it for years, and have found tremendous value in thinking about types.

there are some things the language forces me to think about that i do not find productive. for example, i waste a lot of time thinking about lexical hierarchy in swift, time that i do not waste in C++, because C++ has a stronger namespacing system than swift’s. it honestly drives me insane when i have namespaced concrete types and smurf-named protocols in the same module, and i wish swift would just allow us to namespace protocols like any other modern language.

so i understand why some people do not like thinking about types and prefer to use auto everywhere.

6 Likes

:+1: :smile:

3 Likes