[Feedback] 2019: Swift User Feedback

Hi all. Following a previous post from 2016 and with Swift 5 around the corner, I would like to continue this tradition of sharing our "field testing" developing apps with Swift.

It's worth noting that 2018 has been surprisingly good on that front thanks to:

  • Xcode 10 + the Swift toolchain that became very good in terms of language parsing speed, code indexing & coloring, incremental compilation times, overall robustness of the build system, and the debugging tools. It's the first year in a long time we've felt to be that productive with the tools. Thumbs up.
  • Static libraries (Swift) that have arrived & are supported (2017 - Xcode 9b4). This was my #1 request in 2015-2016, and although it looked like it was a bit improvised the way it happened, it's there. Static libs are so useful to organize complex development project without being force into many inefficient micro-dylibs (code size b/c no stripping, load times). We can finally remove our hacks from the toolchain.

Thank you for that, it is appreciated daily!

Previous Swift advantages remain:

  • it enables to detect many errors or code design problems very early (compile time)
  • there are fewer unexpected crashes at runtime (when reading our crash reports)
  • it is very well suited to high performance (static structs & collections), mathematical code (operators, protocols), where we would have used C++ previously (STL, templates, operators).

There remain things that could be improved though. This list isn't meant to be exhaustive, and part of it may even have been solved already with Swift 5 (please do tell!). It is what comes first to my mind when I ask myself the question "What could be improved in Swift?". Here's the list.

  1. Fixed-size arrays. It is still ineffective that we can't use fixed-size arrays in code as we do in C. Low-level programming needs this badly, be it network frames, fixed size math types, etc. We need the compiler to check on indexes, to allow stack allocation (unlike Array), and enumeration (set/get). After a few years of Swift evolution, I can't think of any excuse for not tackling this very basic use case.

C

struct Mat4 {
    double vals[16];
}

for(int i=0; i<16; i++) { mat.vals[i] = 0.0 }

Swift

struct Mat4 {
    var vals: (Double, Double, Double, Double, Double, Double, Double, Double, Double, Double, Double, Double, Double, Double, Double, Double)
}

mat.vals.0 = 0.0
mat.vals.1 = 0.0
mat.vals.2 = 0.0
…
mat.vals.13 = 0.0
mat.vals.14 = 0.0
mat.vals.15 = 0.0
  1. Improved access levels. There is a common scenario that is impractical in the current situation: splitting a type implementation into multiple files. You typically do that to add specific functionality to a type that is unrelated to the rest (modularity), or to split implementation complexity into multiple pieces (complexity). We need some form of "type private" or scoped access level. Today we are forced to use public (or internal) level, that is, giving a public interface to enable private implementation. This messes up the whole concept of public interface / private implementation. The language shouldn't make us decide between a single large file, or exposing type internals like it does today.
  2. Namespaces: we often want to scope some code to some contexts as a way to organize it. Currently, modules do that but it is mostly implicit (you can't manually define them). When we want to do it explicitly, we have to use empty/fake structs, and structs in structs, etc. It works, but is a bit weird. The ability to create namespaces or modules (maybe not the same module as the one defined for libs) would be a great addition to better organize code. Could be linked to the previous point (access level), as we could for instance define a module for the implementation of a given type, where all implementation vars would be available (think module private).
  3. Module import/re-export rules. We were sometimes confronted with the import of a module that would re-export all of its imported modules which we consider as implementation details. For instance, A import B, B imports C, but A gets all the types of C in the code completion for instance. The reason was not clear, as we were careful not to expose C type in B's public API. It would make sense to have finer control about what gets re-exported (or not). Constraining dependency propagation can be important to manage the complexity of large projects.
  4. Better interface readability. Swift code can be a long stream with public and private funcs / vars intermixed. It can be hard to read, especially if we compare to Obj-C header files. In Obj-C we could craft those public API to make it easily parseable by humans: we dug into the complexity of Class.m and Class+Private.h when implementing the type, and then just with the easy & clear Class.h when using the type. The split required by the tools had a good effect on readability (OK, tbh, maintaining those was also a pain). In Swift, all is mixed like a soup, and it all relies on conventions. The tool (Xcode) can show the "interface", but it is just a filter, it is no more "crafted". Moreover, it is not customizable (I personally don't enjoy the extra spacing between funcs, I'd like a more compact representation). Not sure what is the best route, but better readability would be great.
  5. Bridging headers. We have started making/using hybrid static libraries made of Swift and Obj-C. Typically those were Obj-C libs that we augmented with new capabilities in Swift. There are edge cases we currently can't handle well, it's mostly linked to access levels (again), because there are some part of the library we want to keep private, some other that we want to expose, and same thing for Swift which can also expose API to Obj-C internally or externally with the caller of the lib. Currently, the only thing that works robustly is that the bridging header that we use inside the library has to be made public as well. That constraints the lib Swift code to only use the public Obj-C API of the lib. This is better than nothing, but still an important limitation for library development.
  6. The if case let (or is it if let case, already forgot…) syntax is very hard to remember, and requires an if block when there are associated values. Also, testing enum cases should be possible at all times. Reminds me of Obj-C block type syntax, which was a mess. That is solved now (good!), but it looks like when you push that thing down, another one pops up somewhere. Anyway, I'm in favor of a language I can use without having to read the manual for basic stuff.
  7. Edge-case access level behaviors. I noticed that adding public func in an extension of a private protocol would keep these method public in the type adopting the protocol. This is useful. Is it a feature? It looks like an error that could be removed from a future compiler version. Also, it does not work with an initializer. This should definitely be part of a more global access level discussion.
  8. There are some strange syntax elements, like the comma in if let x = xxx, x.a == true. Here the comma becomes a logical and. In simple expression, there's no problem, but when this becomes more complex, you forget whether you have to use a comma or a && because their meaning is the same.
  9. About nested funcs, the dependency on self (strong reference) is hidden, which is sometimes misleading. Closures on the other hand make it explicit. Not sure we need 2 different approaches here.
  10. Asynchronous programming (async/await, or even better). We need more powerful asynchronous programming tools. Crossed fingers for this so that we can remove lots of spaghetti code / code pyramids.

Thank you for reading.

18 Likes

Your observations are all astute and your writing style is refreshingly concise. It is great to have someone like you bringing valuable feedback.

Many of these are being discussed in other threads. Posting more specifically about each topic in its own thread would make it more likely that those designing, implementing or just petitioning for the those things will hear what you have to say. If they are already focused on something, they are less likely to take the time to read new threads with unspecific titles. There is certainly value to having a general overview somewhere, so thank you for this. But you are more likely to push things forward by posting in the existing threads. Everyone who has already posted in an existing thread or spent a lot of time reading it will receive a notification of your response, even if nothing new had been posted to that thread in a long time. But brand‐new threads like this one will only be seen by those who aimlessly peruse the “latest” section on the home page or who have actively registered to track the general subject—and even then you are relying on the title to catch their interest, because that is all they will see until after they have decided whether or not to click.

These threads may interest you:

I don’t see anything related yet. You could start a specific thread.

That is probably worth a bug report: bugs.swift.org

I don’t see anything related to either of these yet. You could start specific threads.

There are a lot of threads about different aspects: Search results for 'async' - Swift Forums

Chris Lattner’s concurrency manifesto and his draft proposal for async and await might also interest you if you plan on joining the discussion:

8 Likes

My wishlist:

Agree.

In addition to what you said, I would like Xcode's Generated Interface to:

  1. include MARK comments and
  2. have a default (standard) keyboard shortcut assigned (solved).
1 Like

Thank you Jeremy for your detailed answer and for enumerating all the threads for each point. They look very interesting. I will try to get up to speed and hopefully bring some feedback there.

It totally makes sense to provide specific feedback in appropriate threads, even though this post was also meant as a snapshot of my overall current experience with the language and communicate some sense of priorities where improvement is needed.

Isn't cmd-ctrl-up OK?

2 Likes

I still see frequent problems with the debugger. Several problems I see every day:

  1. stopped at a breakpoint. po someObject. Get an error saying the object is unknown when I can it's clearly in scope. (debug code not optimized code)

  2. stopped at a breakpoint. view an object in the Xcode debugger view. All the ivars are nil or "". If I po someObject it prints out with its correct values.

  3. The debugger always takes a while to do anything the first time I use it in a given session. After that it seems to be normal speed.

Overall deciphering crash logs is a catastrophe. The compiler does a bunch of weird things to optimize the code resulting in crashlogs that are very hard to read and call stacks that seem impossible given the original swift code. What I'm asking for is a document that describes how these things work so that I can understand the crashlogs.

5 Likes

Overall deciphering crash logs is a catastrophe. The compiler does a bunch of weird things to optimize the code resulting in crashlogs that are very hard to read and call stacks that seem impossible given the original swift code. What I'm asking for is a document that describes how these things work so that I can understand the crashlogs.

This is a weird one I've never been able to figure out. We definitely get crashes that have actually impossible call stacks through most of the crash log, so it ends up being trial and error based on other indicators. Definitely been a blocker to identifying issues quicker.

The explanation I've received is that the compiler/linker merges some methods together. So your swift code has a call chain of A -> B -> C but the optimized code has a call chain of A -> B' -> C. You need to work from the tip of the call chain backwards to understand it.

The compiler also inserts helper methods for @objc-labeled code, which is confusing but you can usually ignore them. Also, some code is indicated to be on line 0 of the file. This is also usually methods or closures that have been generated by the compiler.

Today I learned! Thanks for letting me know.

1 Like
  1. Namespaces: we often want to scope some code to some contexts as a way to organize it.

Totally agree. I tried to use the nested types but currently they are very limited to play this role.

  1. Asynchronous programming (async/await, or even better). We need more powerful asynchronous programming tools.

Totally agree. For really powerful async and await instructions they would have to rely on something very low level like LLVM's coroutines. I hope it will not be just an overlay over swift-nio.

1 Like

There is one minor thing that really itches me: same file names within different folders. Good lord it's annoying that all filenames should be different, and I can't have file structure like this

Entity
    User.swift
    Comment.swift
Logic
    User.swift    // wouldn't work, should've call it LogicUser.swift or smth
    Comment.swift
4 Likes

agreed. Better than before, but can still be improved. I sometimes notice an interaction with Obj-C dependencies, and loading those manually in the debugger (sometimes) resolves such issues.

For crash logs, not too sure. We're on HockeyApp, and I'm told the stack trace is less clear than it is on Fabric for instance.

It's been a while, but I seem to remember that groups != folders. You may have a group structure there, with all files living in the same folder. I think files of the same name can exist in separate folders.

Either I'm doing something wrong, or you are mistaken :slight_smile:
06

swift build
Compile Swift Module 'test' (3 sources)
<unknown>:0: error: filename "Baz.swift" used twice: '/path/path/path/test/Sources/test/Bar/Baz.swift' and '/path/path/path/test/Sources/test/Foo/Baz.swift'
<unknown>:0: note: filenames are used to distinguish private declarations with the same name

Never mind, then. Seconded, strongly.

I've just had a shower thought. A few more items to wishlist (all Codable-related):

  1. Decodable evolutions (you name it). Originally I though that if I just add a new property to my class/struct and give it default value, Decoder would simply pick it up if it's not present in JSON, but in reality I have to either pre-migrate raw data with new default property, or to make this new property an Optional. Either way it's not very cool and user-friendly.

  2. Make an additional Decoder mode (strategy?) in which it would aggregate all decoding errors into one dictionary (something like [String: [DecodingError]]), instead of throwing one on first error

  3. Provide an abstract layer of Encoder/Decoder. Currently it's private and underscored. However, these classes would perfectly help to build new Encoders/Decoders (like MsgPack which I personally prefer over JSON).

I have wished for that too. You would definitely have my vote if you made a pitch out of it.

1 Like