Confusion when reading large open source library codebases

Hey, I'm learning Swift at the moment and I'm reading a ton of code to try and improve. I made this post because there are two patterns I see a lot in large swift projects which I am having understanding:

  • Declaration of classes as if they were protocols. Meaning the class only has the function signatures but not the implementations in it.
  • New types get declared and immediately get an extension, instead of just defining what the extension is adding in the initial type declaration in the first place, which is what would seem more reasonable to me at least.

The UTTraitCollection definition has an example of both cases:

import Foundation
import UIKit
import _SwiftUIKitOverlayShims

//
//  UITraitCollection.h
//  UIKit
//
//  Copyright (c) 2013-2018 Apple Inc. All rights reserved.
//


/** A trait collection encapsulates the system traits of an interface's environment. */
@available(iOS 8.0, *)
open class UITraitCollection : NSObject, NSCopying, NSSecureCoding {

    
    public init()

    public init?(coder: NSCoder)

    
    open func containsTraits(in trait: UITraitCollection?) -> Bool

    
    /** Returns a trait collection by merging the traits in `traitCollections`. The last trait along any given
     axis (e.g. interface usage) will supersede any others. */
    public /*not inherited*/ init(traitsFrom traitCollections: [UITraitCollection])

    
    public /*not inherited*/ init(userInterfaceIdiom idiom: UIUserInterfaceIdiom)

    open var userInterfaceIdiom: UIUserInterfaceIdiom { get } // unspecified: UIUserInterfaceIdiomUnspecified

    
    @available(iOS 12.0, *)
    public /*not inherited*/ init(userInterfaceStyle: UIUserInterfaceStyle)

    @available(iOS 12.0, *)
    open var userInterfaceStyle: UIUserInterfaceStyle { get } // unspecified: UIUserInterfaceStyleUnspecified

    
    @available(iOS 10.0, *)
    public /*not inherited*/ init(layoutDirection: UITraitEnvironmentLayoutDirection)

    @available(iOS 10.0, *)
    open var layoutDirection: UITraitEnvironmentLayoutDirection { get } // unspecified: UITraitEnvironmentLayoutDirectionUnspecified

    
    public /*not inherited*/ init(displayScale scale: CGFloat)

    open var displayScale: CGFloat { get } // unspecified: 0.0

    
    public /*not inherited*/ init(horizontalSizeClass: UIUserInterfaceSizeClass)

    open var horizontalSizeClass: UIUserInterfaceSizeClass { get } // unspecified: UIUserInterfaceSizeClassUnspecified

    
    public /*not inherited*/ init(verticalSizeClass: UIUserInterfaceSizeClass)

    open var verticalSizeClass: UIUserInterfaceSizeClass { get } // unspecified: UIUserInterfaceSizeClassUnspecified

    
    @available(iOS 9.0, *)
    public /*not inherited*/ init(forceTouchCapability capability: UIForceTouchCapability)

    @available(iOS 9.0, *)
    open var forceTouchCapability: UIForceTouchCapability { get } // unspecified: UIForceTouchCapabilityUnknown

    
    @available(iOS 10.0, *)
    public /*not inherited*/ init(preferredContentSizeCategory: UIContentSizeCategory)

    @available(iOS 10.0, *)
    open var preferredContentSizeCategory: UIContentSizeCategory { get } // unspecified: UIContentSizeCategoryUnspecified

    
    @available(iOS 10.0, *)
    public /*not inherited*/ init(displayGamut: UIDisplayGamut)

    @available(iOS 10.0, *)
    open var displayGamut: UIDisplayGamut { get } // unspecified: UIDisplayGamutUnspecified

    
    @available(iOS 13.0, *)
    public /*not inherited*/ init(accessibilityContrast: UIAccessibilityContrast)

    @available(iOS 13.0, *)
    open var accessibilityContrast: UIAccessibilityContrast { get } // unspecified: UIAccessibilityContrastUnspecified

    
    @available(iOS 13.0, *)
    public /*not inherited*/ init(userInterfaceLevel: UIUserInterfaceLevel)

    @available(iOS 13.0, *)
    open var userInterfaceLevel: UIUserInterfaceLevel { get } // unspecified: UIUserInterfaceLevelUnspecified

    
    @available(iOS 13.0, *)
    public /*not inherited*/ init(legibilityWeight: UILegibilityWeight)

    @available(iOS 13.0, *)
    open var legibilityWeight: UILegibilityWeight { get } // unspecified: UILegibilityWeightUnspecified
}

/** Trait environments expose a trait collection that describes their environment. */
public protocol UITraitEnvironment : NSObjectProtocol {

    @available(iOS 8.0, *)
    var traitCollection: UITraitCollection { get }

    
    /** To be overridden as needed to provide custom behavior when the environment's traits change. */
    @available(iOS 8.0, *)
    func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?)
}

extension UITraitCollection {

    
    /* The current trait collection, used when resolving the appearance of dynamic UIColors and similar objects.
     * This is a thread-local property, so it may be changed on non-main threads without affecting the main thread.
     */
    @available(iOS 13.0, *)
    open class var current: UITraitCollection

    
    /* Sets `UITraitCollection.currentTraitCollection` to this trait collection, performs the given actions,
     * then restores `UITraitCollection.currentTraitCollection` to its original value.
     * Just like `currentTraitCollection`, this only affects the current thread, and may be used on non-main threads
     * without affecting the main thread.
     */
    @available(iOS 13.0, *)
    open func performAsCurrent(_ actions: () -> Void)
}

extension UITraitCollection {

    
    /* Return whether this trait collection, compared to a different trait collection, could show a different appearance
     * for dynamic colors that are provided by UIKit or are in an asset catalog.
     * If you need to be aware of when dynamic colors might change, override `traitCollectionDidChange` in your view or view controller,
     * and use this method to compare `self.traitCollection` with `previousTraitCollection`.
     *
     * Currently, a change in any of these traits could affect dynamic colors:
     *    userInterfaceIdiom, userInterfaceStyle, displayGamut, accessibilityContrast, userInterfaceLevel
     * and more could be added in the future.
     */
    @available(iOS 13.0, *)
    open func hasDifferentColorAppearance(comparedTo traitCollection: UITraitCollection?) -> Bool
}

extension UITraitCollection {

    
    /*
     * Returns an image configuration compatible with this trait collection.
     */
    @available(iOS 13.0, *)
    open var imageConfiguration: UIImage.Configuration { get }
}

UITraitCollection gets defined and immediately extended, and it is also a class that has no implementations for its methods.

If anybody can tell me what is it that I am missing I'd be really thankful!

1 Like

What you are seeing is the 'generated interface'. For any framework that is closed source, Swift can synthesize this view so you can see all the public symbols ... but naturally you can't see the private implementations*. So for every method and property you see there, imagine that there is a redacted implementation present.

*(You can also generate the interface for an open source framework for convenience; it's easier to peruse than the actual source which has all the implementation noise.)

6 Likes

Regarding the extensions, that's just how the original source code is organized. UIKit is an Objective-C framework, and Xcode can show you the original .h header if you want to compare side-by-side. You'll notice that the Objective-C interface categories are translated into Swift extensions.

2 Likes

Awesome, thanks for the reply.

So about these generated interfaces, can I just get a generated interface from a swift library binary using some swift compiler command? Are these interfaces necessary in any way or just a nice to have self documentation feature?

Regarding the extensions, is this a stylistic thing then? I've seen it in a lot of different projects and what confused me was that to me it seems that it would be better to have all the code concerning a class inside the class body instead of distributed among the class and several extensions. There could be something I am not seeing about this though.

So about these generated interfaces, can I just get a generated interface from a swift library binary using some swift compiler command? Are these interfaces necessary in any way or just a nice to have self documentation feature?

I only look at them through Xcode, but I'm sure there's a Swift/SourceKit command to make them.

Regarding the extensions, is this a stylistic thing then?

Yeah, it's an organisation technique. Putting everything in the main type definition can get unwieldy; extensions can let you group related functionality together. One particularly common case is for protocol conformances. Rather than doing class Example : Protocol1, Protocol2, Protocol3, you can declare your main class Example and then have each protocol conformance as separate extensions.

1 Like

Aside from grouping protocol conformances as @bzamayo says, it can also be used to distinguish members that have access to internals and need to preserve invariants from convenience members that don't have access to internals.

Personally, I like being able to look at a class or struct and quickly see a) what stored properties it has, and b) if there are any private properties, which methods and properties have access to them. This is usually a much smaller set than the full set of methods and properties.

Yeah, that's a good point too. It is often helpful to separate public from private members, placing all public properties and methods in one place at the top of the file*, and the extensions let you do that.

Also, I would advise against looking at UIKit interfaces to see organisation patterns, as they are all transliterated from Objective-C origins. Stuff actually written in Swift is your best avenue to learn the idioms. Here's a real world example from my open source Swift framework, for instance.

(*Currently, you can't declare stored properties in extensions — so the private property members unfortunately have to be in the main body. There's a possibility that restriction is relaxed in the future though, at least for extensions in the same file.)

1 Like

Thanks Benjamin and Jonathan, it makes much more sense now! Specially looking at that real world example, class sizes can get quite unwieldy.

That opinion is as good as any other — there's just a huge cargo cult around putting code into extensions (I know this probably receives strong backlash; but that's how a cargo cult works ;-).
There is nothing wrong with saving some redundant characters and using comments to structure your types.

2 Likes

Perhaps we have different definitions, but as I understand it “cargo cult” means copying what you see other people doing without understanding why.

And I would draw a significant distinction between “I saw other code structured with extensions, so I’ll do the same even though I don’t understand it,” and “Wow, that code looks great when it’s organized into extensions, I’m going to adopt the same style myself because I like it a lot!”

The first one is cargo-culting, the second one is…discovering something that you personally like.

4 Likes

I would broaden the definition, but I guess it's more or less the same:
It boils down to keep doing things without ever questioning them.

I'd say unless you can add another "because", it is still cargo cult — but that is not bad per se.
There's nothing wrong with code that looks great; there are just other aspects which I consider more important. One example for this is when people start treating their source like ASCII-art and use whitespace extensively to line things up, making it hard to correct names or add new lines.

Like in a true religion, a cargo cult starts to become nasty when members try to force their believes onto others; in a mild form, this already happens with protocol conformances, which are so common that you sometimes have to justify when you refuse to put them into extensions - whereas I never have seen a proper justification to follow that practice.

“I like it better that way” is a perfectly valid justification for a style choice.

3 Likes

"I like it better that way" is a perfectly valid justification for me — it is a terrible justification for someone who has good reasons to prefer another style. ;-)

The justification is that it makes it easy to see at a glance which methods are related to which protocol. For a type that conforms to many protocols, this can make a big difference in readability.

It also gives you that extension’s scope as a tool to use when organizing code. Got a helper method/property that’s only used in that conformance? Put it in the extension and make it private. Now it’s immediately clear to the reader that it’s only being used in that conformance.

3 Likes

Extensions can be used to structure code — but they are not particular well suited to do so.
You have to do everything by yourself, without support from neither standard tooling nor the language itself, and there is no way to guarantee that

  • the "protocol extension" only contains code which is relevant for the protocol
  • all code relevant for that protocol is contained in the extension

It is very easy to mix things up, and wrong structure does more harm then no structure at all.
Maybe in the future things will change, but until then, comments have some big advantages over extensions: They can't give you that false sense of security — and they have tooling support, as Xcode will show structuring comments in the method menu.

That was the driving motivation for adding fileprivate and adding stronger restrictions to private (SE-0025) — but I have sad news for you: SE-0169, which shredded that guarantee, turning it into another source for false sense of security.
The irony in this story is that SE-0169 was motivated by the "cult" of using conformances to structure code… therefor, I think it's valid to consider that ritual as harmful, as it is responsible for a big complication in the system of access rights.

Still, I don't say you should stop using "same-file-extensions" — I'm fine with that.
However, I wish all people would think more about pros and cons themselves instead of repeating claims from blogposts they read.
Above all, everyone should be free to decide that it's more reasonable to put their methods in the main declaration, without having to justify this preference (to be clear: I'm not blaming anyone here for suppression — but the cult is much bigger ;-).

1 Like

Neither of these is even desirable to guarantee.

There will often be members semantically related to protocol requirements, sometimes refactored out of them, that are not themselves requirements. It makes sense to place them in proximity to the requirements to which they relate.

There can be members that are requirements of more than one protocol which happen to compose well together. There can be members that happen to be requirements of some protocol, but are semantically so related to other members of the type that have nothing to do with the protocol, and which would exist on the type whether the protocol conformance were implemented or not. It can make sense to organize these members elsewhere, apart from other protocol requirements.

No, the reason one should use extensions to structure protocol conformances has to do with neither of these points.

Rather, this strategy allows you to build up a series of conformances to a protocol hierarchy in layers, and to have the completeness of your conformance at each step checked by the compiler. This is not just a nice-to-have, but rather it is essential to avoid creating infinite recursion inadvertently.

Consider the following toy protocol hierarchy:

// written freehand; pardon any typos
protocol A {
  var isFrobnicated: Bool { get }
}
protocol B : A { }
protocol C : B {
  func frobnicated() -> Int
}
extension C {
  func frobnicated() -> Int { isFrobnicated ? 42 : 0 }
}

Now consider this conformance:

struct S : C {
  var isFrobnicated: Bool { frobnicated() != 0 }
}

Yikes! Now you've got an infinite recursion. What went wrong?

Default implementations of protocol requirements are built from requirements that aren't defaulted. (It couldn't be otherwise, if you think about it.) You always run the risk of infinite recursion if you implement a non-defaulted protocol requirement by calling a default implementation provided in the same or a more refined protocol in the hierarchy. Even if the default implementation today doesn't call the requirement you're implementing recursively, a future version could.

So how do we decrease the risk of making this mistake unintentionally?

In my experience, it's usually pretty obvious when a requirement of A calls a default implementation of another requirement of A. Possibly in part because, when you're working on it, these requirements are all at the forefront of your mind.

It's much more difficult to reason about requirements that have default implementations elsewhere in the protocol hierarchy; it can easily slip your mind which members you've implemented yourself and which ones you didn't, particularly when requirements differ only by argument or return type. Sometimes, with this kind of overloading, you may think you're calling a member required by a less refined protocol which you've already implemented, but mistakenly call a member required by a more refined protocol which you haven't implemented, but which has a default implementation!

This is where it helps greatly to build up the conformance layer by layer. If first we ensure that S has a working conformance to A without trying to conform to B or C, and then we ensure a working conformance to B, and then to C, we can avoid this pitfall. This is because an implementation when conforming to A can't accidentally call a defaulted requirement of C while the type doesn't yet conform to C. The natural way to do this is to write extension S : A { ... } first, ensure that everything compiles and works as expected, then proceed to extension S : B { ... }, and so on.

Do examples of such unintentional infinite recursion actually happen? Yes! Does this strategy of building up conformances in layers actually help? Yes--only by adhering to this stringently was I able to avoid accidentally causing infinite recursion in implementing certain DoubleWidth functions (now sadly relegated to a prototype instead of part of the standard library).

Now, could you take this same strategy but in the end lump everything together again without using a series of separate extensions? Sure, but that's depriving your reader of the ability to follow along in the building-up of the type in the same way that helped you write the code.

8 Likes

:+1: … and as extensions can't add stored properties, it's also often impossible have a clean separation without extra boilerplate.

That's a pattern I like as well — but afaics it's more common at library/framework level than in application code.

However, extensions are also used in other situations:

… and I agree with @joaqo that this not a good practice.
A real-world example for the kind of misuse I'm talking about looks like this:

class MyTableViewDataSource {
  var entries: [Entry] = []
  //… lots of code
}

extension MyTableViewDataSource: UITableViewDataSource {
  func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
     return entries.count
  }
  func tableView(_ tableView: UITableView, estimatedHeightForRowAt indexPath: IndexPath) -> CGFloat {
    return 50
  }
 //…. lots of code
}

extension MyTableViewDataSource: UITableViewDelegate {
  func tableView(_ tableView: UITableView, canEditRowAt indexPath: IndexPath) -> Bool {
    return true
  }
//…. more code
}

It's fine to not like a particular style choice—that's what style is all about—but to label it as "misuse" or dismissing it as "cult"-ish or a "cargo cult" is pretty hyperbolic and a bit insulting to folks who do use that style.

The pattern that you're labeling as misuse here can be found throughout the source of Swift's own standard library. Are you suggesting that the Swift team is misusing or cargo culting their own language?

5 Likes

You can’t even do this if you want a set-like collection; both SetAlgebra and Collection define isEmpty.

1 Like

Please excuse me if I chose inappropriate words — but please also don't assume bad intentions:
Neither did I say that every use of extensions is "cultish" (and/or misuse), nor did I use the word in a discriminative way.
The dictionary offers harmless interpretations as well.
cult (noun) someone or something that has become very popular with a particular group of people
misuse (noun) an occasion when something is used in an unsuitable way or in a way that was not intended

I have little doubt that using conformance extensions is quite popular, and while I guess extensions originally had a different intention than today, I think nearly everything can be used in a wrong way.

Usually, I try hard to answer every direct question — but this kind of rhetoric figure appears too aggressive to me; let's not try to construct insults.
However, I'm convinced that the Swift team would agree that the code in my example needs fixing because it makes bad use of extensions.