“Lightweight” Swift module?

I just tracked down a very puzzling bug we had that ended up being related to having two similarly named, but distinct methods: pin(productId:) and pin(productId:completion:). The latter has an optional argument with nil default value.

The former is intended to be used only by that subsystem in our app. There's a companion class that calls it, but higher-level code should not. Unfortunately, higher-level code was trying to call the second form, but not passing a completion closure, and the compiler resolved that into a call to the former.

The obvious solution here is for one of them to be public and the other…not public. But afaik, Swift offers no way to do this without either combining all the related classes into one file and using fileprivate, or creating a separate module and using internal.

Thing is, creating a module is a bit heavyweight. In Xcode, I'd have to make a Package or Framework project, and set up some dependencies, and I'm constantly making changes to it, it adds a lot of friction.

I kinda wish Swift had adopted Java's approach of packaging with hierarchical naming. But in that vein, is there any way to designate some files as belonging together without going all the way to a package?

Would it make sense to be able to write module Foo in my Swift source files, and import Foo where I want to use them, without them having to be in a separate physical package?

Also: Shouldn't the compiler have warned me that a call was amiguous?

2 Likes

Yes: You can create either a separate module, or a separate library within a package. Please clarify whether or not you are using a Swift Package.

I'm writing an iOS app in Xcode. I do have SPM dependencies, but this is what I'm trying to avoid as being too heavyweight.

Oh, I found this. It might be lightweight enough. But lemme know what you were leading to.

And why can't you just create a separate target in your app that contains code for your "subsystem"? You don't need a separate package/framework for this.

Does that create a separate module as far as the compiler is concerned?

The overloads are valid, and the more precise match is preferred by the compiler just like between foo(_ any: Any) and foo(_ boolean: Bool).

When the behaviour is fundamentally different, and not just and optimization, I usually name the more “hidden” method _pin(...), unsafePin(...), internalPin(...), sneakyPin(...), pinWithoutCompletion(...), pinForSuchAndSuchACallSite(...), or something similar, depending on how widely it is intended to be used. The point is to make it clear, “Don’t call this unless you are the privileged reason for which this method exists.”

3 Likes

Boy, it really seems that it should warn me, or even be an error.

Yeah, I'm trying to come up with distinguishing names for these, but I find naming them based on their intended access unsatisfying. I'm going to try making them into a module, but I think that's going to force me to put a lot of other code into the module (or yet another module) as well.

I'm not finding any clear answers in the docs I can find online: What defines a Swift module?

  1. Is it a target in an Xcode project?
  2. Is it a directory under Sources in a Swift package (and its corresponding test files)?

(If #2 is true, it's shame Sources and Test are two high-level directories, instead of each module having Sources and Tests within it.)

To answer one of my own questions above, from Access Control:

Each build target (such as an app bundle or framework) in Xcode is treated as a separate module in Swift.

It would be interesting to know if there are any examples of intentional use on GitHub or elsewhere in the wild, or if most people see it as a trap waiting to spring. I have no recollection of ever wanting to use such an overload pattern, or of knowingly seeing one used. Maybe it would be worth a warning or error? (Disclaimer though: I’m not bothered enough about it to invest my own time in the research or pitch.)

1 Like

A module is a set of one or more source files that are compiled together into a single unit. Most importantly, a module defines the access control boundaries for your code. For example, internal symbols are only accessible within the module that they are defined in. Generally, all of the source files for a module are contained within a common parent directory (although these files may be spread among multiple sub-directories).

For Swift packages, by default each module (also called a target) contains all of the files located in the Sources/<module name> directory; however, you can also manually specify which directories/files you want to be part of each target in the Package.swift manifest file.

Xcode projects are similar: The files for each target are usually located in a common parent directory, but you can customize this using the "Build Phases" and "Build Settings" tabs in the project editor. To create a new target in your project, select File > New > Target... from the menu bar.

Note that your project already has at least one target in it. All source files must be contained in a target, otherwise, they wouldn't be built.

Package manager docs:

1 Like

It shouldn't be an error because some people may want to use this overload. How do you expect the compiler to know which overload you intended to use and warn you when you use a different one?

I've always hated this design decision. What if I want to organize some files into a common directory without putting them into a separate target?

Yes, you create a new target in your project, as described above. Again, your project already has at least one target.

1 Like

The idea would be to warn if you use any. Rather, if I have

func foo(p1:)
func foo(p1:p2=nil)

and I call

foo(p1:)

It's not really clear which I intended, is it? I am not as perfect as the compiler, and may not even know of the existence of the other. Not only should it warn at the call site, it should probably warn at the declarations, too.

I believe C++ does this.

Are you really suggesting that the compiler should emit a warning every time you reference a method that's overloaded? That would lead to countless warnings in almost every single project. How would you suppress it?

1 Like

No. Only when it's ambiguous. If a call to an overloaded method isn't ambiguous, it needs no warning. Similarly, if two method signatures differ, they're not ambiguous. It's when they have the potential to be ambiguous that you'd get a warning.

Which one of the two foos did I intend to call?

func foo(a: Int) { print("foo(a:)") }
func foo(a: Int, b: Int = 0) { print("foo(a:b:)") }

foo(a: 1)

The compiler determines I intended foo(a:), but nothing about this is clear. In fact, the default value for b can never be used. This deserves at least a warning on the call foo(a: 1), and really at each foo definition, because the intent of having both of them is unclear.

Note that these two overloaded methods are perfectly unambiguous:

func foo(a: Int) { print("foo(a:)") }
func foo(a: Int, b: Int) { print("foo(a:b:)") }
1 Like

And how would you suppress this warning?

Generally, you should avoid these kinds of overloads in the first place.

If it's truly ambiguous, there should be an error, as some cases do:

func foo(a: Int, b: Int = 0) {}
func foo(a: Int, c: Int = 0) {}

foo(a: 0) // Error: ambiguous use of `foo`

Now, it's near impossible for a type checker to reflect intuition, so improvements definitely exist. This one, in particular, would be source-breaking, though.

1 Like

You can use a struct/class to emulate a namespace in Swift.

func pin(productId: String, completion: (() -> Void)? = nil) {
    print("top-level: \(productId)")
    completion?()
}

struct Subsystem {
    private init() {}
    static func pin(productId: String) {
        print("subsystem: \(productId)")
    }
}

pin(productId: "some-id") // prints "top-level: some-id"
Subsystem.pin(productId: "some-id") // prints "subsystem: some-id"
1 Like

Or use enum instead of struct. In that case you can even leave the init() out.

1 Like

It's not so much the namespace I want; it's the protection levels. I need internal to allow unit tests and the related classes to work, but internal is open to the entire module (app, in this case), so I can't hide anything. I have to put the specific classes into their own module. I had hoped there was a way to do this declaratively, rather than restructuring my project.

1 Like
Terms of Service

Privacy Policy

Cookie Policy