On alternatives to `using` namespaces

working with namespaced declarations can be really painful.

import Dreamhousing

extension Dreamhood
{
    public
    init(dreamhouses:[Dreamhouse.Identifier],
        garages:[Dreamhouse.Garage.Identifier],
        pools:[Dreamhouse.Pool.Identifier]
    {
        var dreamhouseTable:Dreamhouse.AddressTable<Dreamhouse> = .init()
        var garageTable:Dreamhouse.AddressTable<Dreamhouse.Garage> = .init()
        var poolTable:Dreamhouse.AddressTable<Dreamhouse.Pool> = .init()

        ...
    }
}

we can sort of get around this is by mimicking C++ using by adding typealias declarations to the client type.

extension Dreamhood
{
    public
    typealias AddressTable = Dreamhouse.AddressTable

    public
    typealias Identifier = Dreamhouse.Identifier

    public
    typealias Garage = Dreamhouse.Garage

    public
    typealias Pool = Dreamhouse.Pool
}
extension Dreamhood
{
    public
    init(dreamhouses:[Identifier],
        garages:[Garage.Identifier],
        pools:[Pool.Identifier]
    {
        var dreamhouseTable:AddressTable<Dreamhouse> = .init()
        var garageTable:AddressTable<Garage> = .init()
        var poolTable:AddressTable<Pool> = .init()

        ...
    }
}

but i really hate this workaround, and don’t end up using it because:

  1. there is no way to include an entire type’s namespace, you have to insert each typealias individually.

  2. the typealias is an extra thing to rename when renaming types

  3. the typealiases have to be public, and have a documentation footprint.

  4. the typealiases have to have a documentation/IDE footprint (cannot use @_documentation to hide them) because swift typealiases are not “transparent”, they are real symbols that can be clicked on and have definitions, when what you probably want is to be able to click on the name and go straight to the underlying type.

do we have a solution? and if not, do changes have to be made to the language to make this easier?

EDIT: numbered bullet points

7 Likes

subscript(dynamicMember: KeyPath) is a good precedent for this kind of forwarding.
I don't think a static subscript could work.

that would solve #1 and #2, but not #3 or #4. (as would macros)

you also can't use keypaths to refer to types themselves, you can only reference metatypes

1 Like

My recommandation would be to avoid nesting types and try to only use modules for namespace most of the time. It's fine for Identifier to reside in the type it identifies if the containing type name basically has to be part of the identifier's type name because the identifier is specific to that type (not sure if that's the case here).

But of course if you're importing someone else's code there isn't much choice.


Maybe if we supported nested types in import statements it'd help:

import struct Dreamhousing.Dreamhouse.Garage

// can use Garage without the namespacing here
func install(garage: Garage) {} 

But this import syntax already has another meaning: searching for a Name struct inside of a Dreamhousing.Dreamhouse submodule. It's only good for importing top-level types right now and we'd need another syntax for nested ones.


It could also be nice to have "renamed" imports:

import struct Dreamhousing.Dreamhouse as DH

// Here DH refers to Dreamhousing.Dreamhouse
func install(garage: DH.Garage) {} 

Basically, creating a transparent typealias within the current file.

6 Likes

i don’t think this is advisable for libraries, because adding a new top level symbol is a major version break, because it may collide with another symbol.

agreed!

4 Likes

Something like world (akin to a namespace in C++) would be really nice to have: :joy:

world <World> {
   Definitions
}

Definitions : Definitions Definition

Definition : world ...
Definition : protocol ...
Definition : class ...
Definition : enum ...
Definition : struct ...
Definition : var ...
Definition : let...
Definition : func ...

All public symbols of a world can be imported with the using statement:

using <World>

Individual symbols can be accessed from outside a world by qualifying their names:

<World>::<Symbol>
<World>::<Symbol>.<Symbol>
<World>::<World>::<Symbol>
<World>::<World>::<Symbol>.<Symbol>

A world can be given another name:

using <Alias> = <World>

If we wanted to go really wild, we could have instances of a world, but that would raise a lot of eyebrows. :slight_smile:

I never thought about it that way but it does make some sense since people generally import the whole library, not individual types. However, my first thought here is that other modules can also add symbols in your public types through extensions which can also create name collisions, collisions that are even harder to disambiguate than top level symbols.

So how absolute can you be in setting the threshold for major version breaks? By the looks of it, any public symbol added anywhere is potentially a source-compatibility breakage.

2 Likes

this is correct, and this is why i often say that virtually no one (not even SwiftNIO) really adheres to semver.

as a practical matter, i've just resigned to ignoring the problem and assuming that extension conflicts with nested types simply won't occur. but name conflicts with toplevel symbols can and do happen quite often, and this is something i've been burned by many times using other peoples' libraries.

so, as a general best practice, nest everything.

1 Like

Maybe two-letters prefixes should make a return. They would create a namespace for reducing source-level breakage without the issues you're dealing with when nesting.

1 Like

Swift could still add support for real namespaces...
I remember this has been discarded in the past because the benefit would be too small — but now we are about to get a new access level to allow more organization of code, and imo namespaces could give us something similar:
When everything declared in a namespace is hidden by default, this would already be a key feature of package-visibility, just with another mechanism to make the contents usable.
On top of that, a public namespace would create additional possibilities.

7 Likes

this isn’t really that different from renaming the Dreamhouse type to DH.

import Dreamhousing

extension Dreamhood
{
    public
    init(dreamhouses:[DH.Identifier],
        garages:[DH.Garage.Identifier],
        pools:[DH.Pool.Identifier]
    {
        var dreamhouseTable:DH.AddressTable<DH> = .init()
        var garageTable:DH.AddressTable<DH.Garage> = .init()
        var poolTable:DH.AddressTable<DH.Pool> = .init()

        ...
    }
}

but DH isn’t a particularly legible name.

I agree DH would be a bad name. But I meant DH as a prefix for the module or package, not a type. So it'd be DHDreamhouse, not DH. Like this:

import Dreamhousing

extension Dreamhood
{
    public
    init(dreamhouses:[DHDreamhouse.Identifier],
        garages:[DHGarage.Identifier],
        pools:[DHPool.Identifier]
    {
        var dreamhouseTable:DHAddressTable<DHDreamhouse> = .init()
        var garageTable:DHAddressTable<DHGarage> = .init()
        var poolTable:DHAddressTable<DHPool> = .init()

        ...
    }
}

I suppose a variant on this would be to put everything in an empty enum as a namespace:

enum DH {
    // no cases in this enum, this is purely a namespace

    class Dreamhouse { ... }
    class Garage { ... }
}

But personally I find all this nesting makes the API more confusing than a simple name prefix. It's also impossible to disambiguate using the module name in the eventuality another module adds a type in this same enum using an extension since that type it is no longer top-level.

Wouldn't that conflict with the fact that module itself stands as a "namespace" for its interfaces?

Personally, I try to think differently about imports from "in house packages" and "public" packages. For the first group it's more relevant to have stuff in global namespace (to some degree) as it's closely related to the project domain and commonly used. For example, domain package like "Core".

For the second group I think you shouldn't allow 3rd party naming to leak into your codebase, so creating a small wrapping layer in which you need to force yourself to make that long type chain isn't that bad.

Although it would be awesome to have some concept of using keyword it could also cause a lot of API leaks all around and hard to trace problems.

There is this comment on name spacing Namespacing - #6 by clayellis mentioning apple definition of Combine Publishers, if you're creating API for very generic usage it's probably a good idea to approach it like that to prevent conflicts with codebases importing your lib. But in the discussion example if the Dreamhousing would be team owned package. Then types such as Garage or Pool could live outside the Dreamhouse unless they are of course tightly coupled with the Dreamhouse itself, but then why would we need to work with this api a lot outside the Dreamhouse?

My point is nesting types to me is a sign that those types are closely related to each other (otherwise you wouldn't define one inside the other?) and most probably the owning type will be handling most of the stuff related to the child type. Which would focus my attention towards extending that type closer to its original definition, like in the library itself.

Linking another similar discussion for context: Enable module names to be type aliased - #16 by jayton

Pulling a few notes from the link:

  1. Nobody wants to go back to the era of objective-c prefixes on everything. We're trying to kill NSThis and AFThat!!!
  2. There are edge cases where the current system is unworkable without renaming your type when a library adds a new type that conflicts with yours (due to Swift limitations not because the problem is fundamentally unsolvable). So even "do things at the module level and don't nest types" is not a workable/universal solution.

On nested types: I often reach for nested types when I want to communicate hierarchy and not end up with a bunch of top-level types. Let's say I have some Frob that has a Cache data structure that does need to be publicly visible for some reason. I don't want consumers using Frob and FrobCache types. I want consumers using Frob and Frob.Cache. Maybe it's personal preference but, not least because namespacing limitations have driven the community to de-facto adopt empty enums as a janky namespacing technique, I use the pattern pretty commonly.

9 Likes

+1!

5 Likes

How do you namespace protocols? This is the part that annoys me :)

What's the problem with name-spacing protocols? Many languages support namespaced interfaces/traits/protocols/whatever you call it.

He was asking how you do it in Swift. The problem is that, currently, you can't. He wasn't saying there's a problem with the concept, he was expressing his frustration that it's not something Swift currently supports.

Personally, I think we need a :: operator, so that we can disambiguate types that are named the same as a module, and C++ style namespaces. Which are among the only parts of the C++ core language worth stealing.

2 Likes

any progress on this front? right now i am working with a number of types in a namespace called DocumentationDatabase.Record and it’s frankly getting ridiculous how long their function signatures are getting…

1 Like