Private extension to built in types - thoughts

Hi,

Just wondering if it is a good approach to add private extensions to types such as URL or create function within my own types.

I have a struct called School which fetches data

My thoughts:

  • I had created it as a static function inside School but was advised to create it as a private extension inside URL
  • My approach was it was going to be used inside School and what is external URL might vary based on a different class / struct.

Question

  • Is it better to create it inside School or extend URL?
  • When to choose which approach?

School.swift

struct School {
    func fetchData(from url: URL) {
        //use option1 or option2
    }
    
    // Option 1
    private static func isExternalURL(_ url: URL) -> Bool {
        // some logic
        // return true or false
    }
}


// Option 2
private extension URL {
    var isExternal: Bool {
        // some logic to determine
        // return true / false
    }
}

I firmly believe Option 2 is the right approach. From the perspective of modeling your type it is not an obvious concern of a School type to determine if an URL is external or not.

With Option 1 your URL handling code would also starts to read differently depending on if you did build some logic around URLs or if the Swift Foundation team did.

Whenever you call a function on a type it will always raise the question of a future reader what the connection to that type is and if the answer after reading the source code is "none", than this feels like wasted time, because with Option 2 this question is not even arising.

But I would highly advise you to keep these small extensions private, especially when their implementation is trivial. If you get in the habit to collect them globally you easily end up polluting the API surface of commonly used types and often these collection of extensions are not well maintained in bigger projects. So private extension get a big thumbs up, but extending everything globally can become a problem if these extension are not maintained carefully.

4 Likes

If the privateness of the extension starts to bite, there is always option 3.

// Option 3
class URLWrapper  {
    let url: URL

    init (url: URL) {
       self.url = url
    }
    ...
    var isExternal: Bool {
        // some logic to determine
        // return true / false
    }
}
2 Likes

Option 2 is the most semantically correct (as it's a property of a URL, but only makes sense in the context of a School, hence the private extension), and most appropriate for the language (as it leverages a unique and characterizing feature of Swift).

We do it all the time, in a slightly different way:

// Option 2
extension URL {
    fileprivate var isExternal: Bool {
        // some logic to determine
        // return true / false
    }
}
2 Likes

It is interesting to see the responses.

Please bear with me as I try to understand better.

May be I am a bit worried about using it on standard types and having custom implementations be executed, I guess that is what extensions are for but just want to be careful going in this route

I was under the impression not to extend standard types unless they needed to be. (again what is needed is debatable .. lol)

Just wondering when isExternal is different in different files that would would be ok?

Is there some guidance or is this completely subjective?

There is no such guidance: you can confidently extend any type.

What you should not do is retroactively conform a type that is not your own to a protocol that is also not your own.

5 Likes

Thanks a lot @xwu, @let_zeppelin, @ibex10, @ExFalsoQuodlibet

I think I have a better understanding now, extending built in types is fine as long as I keep the following in mind:

  • @xwu - you should not do is retroactively conform a type that is not your own to a protocol that is also not your own.
  • @let_zeppelin, @ExFalsoQuodlibet - Private extensions and not expose them outside of the file

not expose them outside of the file

That is more like a guiding principle than a rule. If you have a very good reason in your app/domain then go for it, but I would advice to avoid a bunch of files of one-off extensions that were never meant to be more than a helper for one particular use case. I also just brought it up, because in my experience this is a common pitfall, because at first it seems like a good idea, but when the project grows this ca become hard to maintain.

1 Like

Thanks @let_zeppelin so are you saying a lot of these one of private extensions creates a mess?

The reason for asking is the code base I am working on seems to have a lot of these one off private extensions (most of them are just used once in a file and are private).

It can create issues with other people working on the project not realizing that it's from an extension and being confused when they can't find it in the official documentation. But imo this is easily solved by educating people about command-clicking things in Xcode: if "jump to definition" jumps somewhere else in your own project, it's pretty clear what's going on.

The issue xwu mentions is the only hazard that's not a matter of opinion/style.

4 Likes

I agree command clicking definitely would get it cleared about the definition.

Personally in my workflow, I don't command click a lot of the standard types to check if it was a custom implementation or not. I don't know many of the APIs of the standard type I just rely on Xcode to suggest them, and when it does suggest, I just wrongly assume it is from the original standard type and is not a custom implementation.

May be it is just ignorance on my part, but I just worry command clicking almost all of the standard type APIs would slow me down mostly because I don't know the APIs well enough for me distinguish if it is custom or original.

Thanks @let_zeppelin so are you saying a lot of these one of private extensions creates a mess?

No … keeping them private is avoiding the mess. Having them internal in different files, -> can <- lead to a mess.

1 Like

I tend to avoid extending standard library types unless there's a really, really good reason to do so, and the extension's use and benefit is widespread enough over an entire codebase.

Users come with preexisting mental models of what certain types do, how they behave, and the methods and properties they have. Most of the time those models are consistent because each type deals with a specific concern, and the standard library has standards of consistency that make those models feel predictable and easy to understand.

Extensions can violate those models, especially the more specific and scoped the concerns they're addressing. Not only that, but (as implied by the name) extensions also require users to extend their models. Those two things (violation and extension) aren't evil in and of themselves and people can tolerate a little learning, but without constraints they can make a codebase feel inconsistent, alien, and disorganized, and ideally the exchange should be worth it.

Sometimes asking more questions can help hone in on alternatives. With external URLs, some questions that come to mind are: "external relative to what?" or inversely, "what does local mean?" or "who cares?" (by who, I mean what code and semantics).

If URL externality is a concept specific to the context of Schools, that's a big hint that the code that determines URL externality should be associated with School, as you've done in your original code, or, if you really wanted, in a School.Utilities enum.

Typing is also an option (which @ibex10 sort of was pointing towards): you could introduce a type that clarifies a URL's externality, though in my opinion this really only makes sense if there's an app/library-level concept of externality:

struct ExternalURL {

    let url: URL

    init?(url: URL) {
        let isExternal  = ...

        if isExternal {
            self.url = url
        } else {
            return nil
        }
    }

}

(This also starts getting into tagged types territory, which has been brought up before many times.)

At the end of the day this is all somewhat subjective, but for me a big factor for this kind of decision is how it slots into the overall ontology of a codebase.

3 Likes

The SE-0364 proposal also has more details behind the motivation for avoiding retroactive conformances.

4 Likes

haha .. ah got it, thanks

Thanks @mattcurtis, I agree it is all subjective.

It is nice to hear the different approaches, I think your suggestion kind of fits into my mental model, thanks

1 Like