Skip to content

Swift subset

Ken Harris edited this page Oct 11, 2020 · 3 revisions

I mentioned recently that I don't feel that I "know Swift". It's just way too big and complex. I write in a not-well-defined subset of Swift. Beyond the compiler bugs mentioned in Swift, here's some of the parts that I simply avoid, because I know I'm not smart enough to be able to use them effectively:

  • PATs. I've read the docs and watched this video several times, and I can kind of understand the intent of the feature, and given some code I can probably tell you why it won't compile, but I'm far from being able to use PATs myself.

    • I've tried to use PATs in a few different cases. Every time, I end up wasting a couple days chasing down the craziest type-checker error messages I've seen since g++ 2.7.2. Or it'll work fine for a while, and then I'll discover 3 layers later that my original design won't possibly work in another case. So I always give up and just throw an Any in my protocol, document what it's supposed to be, and cast the sucker when I need to take it out. This takes about 2 minutes to do, and I've never once run into a type error from it.
  • The C API. It's the most confusing C API I've ever seen. Worse, it changed completely every year for a while, though it seems to have stabilized a bit since Swift 5. You might not be able to avoid it (either for third-party C libraries you need, or even some of Apple's own libraries which only have a C interface), but you should absolutely minimize the surface area. Encapsulate the C API parts in a tiny Swift wrapper, so when you need to update it, it's one isolated piece. Or you can replace it with a native Swift library, if they ever write one.

    • In hindsight, I shouldn't have touched the Swift C API at all. What I should have done is write that part in Objective-C. Objective-C is a strict superset of C, and Swift is built to be great at wrapping Objective-C.
  • Callable types. I guess this is mostly for interop with dynamic languages that support this feature. You don't need it.

  • Any numeric types apart from Int and Double. For some reason, Swift made it painful to use other types, especially unsigned integers. Swift follows this guideline itself: even in cases where a negative number makes no sense (like counts or lengths), the stdlib uses signed ints.

    • The only case I've ever used other numeric types is when a C interface requires it, and even that's rare.
    • Are you worried that you can't use the type system to enforce bounds? You can't do that for 99% of bounds checks, anyway. Look at your code. It's full of cases where you want to say "This is an integer from 1 to 100", and the type system doesn't support that. Use Int/Double everywhere, and toss in the occasional precondition for bounds checks.
  • All the crazy new string interpolation, including multiline string literals. I get why it exists, but just, ick. Too much.

    • Plus, Xcode 12 still can't seem to offer any autocomplete inside string literals -- which is the one place I need it!
  • @escaping/@noescape. I do some pretty crazy async stuff, and I've only needed these twice -- when conforming to AppKit/WebKit protocols that require it. I've read descriptions of them a dozen times, and I still have no intuition for why they exist. Is it because of Swift's use of refcounting? I've used a dozen other languages with closures and none of them needed this.

  • @autoclosure. Normally I'm all for making code as short as possible (I won't type return unless it's necessary) but this one just looks like a recipe for confusion. Spend an extra 2 characters and write it normally.

  • propertyWrappers. These seem like a decent idea, and I've used similar functionality in other languages. It's just that the documentation looks awfully brief for how much functionality and how many rules there must be. All of Apple's official docs are mostly "here's a tiny demo, isn't it neat?".

    • Plus, the compiler's error messages are bad enough already. I don't trust them to say anything intelligent about how I'm inevitably going to misuse a wrapped property.
  • operators, precedencgroups, etc. Maybe this is useful if you're writing numeric code (though I'm not sure why you'd pick Swift for that!). Swift already has the most complex syntax of any language I've ever learned. I can't remember the precedence rules for the built-in operators. Please don't go adding new ones.

    • When I get confused on precedence, I add parens everywhere to make it explicit. At that point, you may as well have just written a func, because at least then you'd have arg labels.
  • in-out parameters. These I actually understand just fine. I've just almost never had a use for them. I think I've used this maybe 3 times in my life, and even that was probably too much. Unless you're implementing a protocol which requires it, there's almost certainly a better way.

  • Key path literals. I love the idea. I hate the syntax. Also, the KeyPath interface seems somehow both overly complex and yet not powerful enough for common uses. Also, they're currently 10 times slower than closures.

    • Normally, I'd be hoisting the flag for the Principle of Least Power. Key paths are kind of like Lisp symbols! But right now, Swift closures are already pretty concise, and key path literals are a bit shorter but have ugly syntax, and gain almost no actual features over closures, and are also much slower. I'll give them another go when they're fully baked.
  • Opaque return types. I'm really having trouble wrapping my head around these. It seems like it's a band-aid for all the unintuitive ways that PATs don't behave like normal protocols, and/or, it's because the compiler folks can't stand the idea of ever using dynamic dispatch (even though their entire operating system has been using it for the past 30+ years).

    • Again, this falls into the category of "No other language I've ever used had this feature, and none of their documentation did a decent job of explaining why this language needed to add Yet More Syntax for this".
  • Codable, except in trivial cases. I guess you need a good basic serialization system, but I'm no fan of Codable. It makes one trivial case easy (which I've used literally once ever), and doesn't seem to help at all for the 99% of other cases I care about. It seems to assume that either (a) I don't care what the wire format looks like, or (b) I'm able to redo all my data structures to match the (one) wire format.

    • They claim it was designed to support multiple formats, but you have to write monolithic encode/decode methods which switch on the Encoder/Decoder types (ick), and there's no standard way to ask what type to use (ick). So you just write a couple big switches that check for decoder is JSONDecoder ... wait, no, it uses a private type, and JSONDecoder isn't actually a Decoder, so even that won't work. This is all a disaster.
    • So I always end up writing my own protocols for encoding/decoding my own types, and then writing my own types to conform to them for each format I need to support. It works great. Don't confuse future maintainers by trying to stretch Codable to within an inch of its life.
    • The one case where I might use Codable is if I have a simple type that I only need to pass from one part of my program to another part of that same program over a serial link (like drag-n-drop within my own application). Slap Codable on the thing, and then throw it in literally any Encoder/Decoder.
  • Mirror. I think this is potentially one of the more useful parts of the language, but instead of breaking down functionality that already exists, it builds up more stuff. I'll probably learn it someday, but man, they didn't make this easy.

  • FunctionResult builders. At some point, you're just throwing all the features of Lisp into a blender. I'd rather just write in Lisp.

    • I know they're necessary for SwiftUI, but fortunately, SwiftUI (at least for macOS) is still such a buggy heap that I can safely ignore it for a while.
    • I actually tried using these, because they seem like a good fit for what I'm doing today. The error messages from the compiler are even worse than usual. It doesn't yet support half the features in SE-0289. It's a mess.
    • So apparently this isn't actually part of Swift. Despite being used for SwiftUI for over a year now, they were first proposed 28 Aug 2020. Also, what the Swift compiler implements (as of 5.3) is just a small fraction of what was proposed. For-loops (buildArray) seem to be completely absent so far. If-statements in SwiftUI use some completely undocumented method (buildIf) rather than the proposed interfaces. Yikes. I was frustrated by this feature when I thought it was well documented, implemented, and supported!
  • Multiple trailing closures. OMG make it stop.

  • for-where. It's true I often need to loop on a subset of an iterator, but the for line is usually already pretty long, and we've already got perfectly good ways to do this that don't need special new syntax.

    • The samples of this that I see are always either for obj in objects where obj.isActive or for i in range where i%2==1, and none of my loop filters ever look anything like those.
Clone this wiki locally