8
$\begingroup$

Objective-C didn't have namespaces, so everyone chose a two- or three-letter prefix to stick on everything. This has effects to this day as typenames like NSObject and UIView are still prevalent in Swift code.

C++ and C# allow you to nest namespaces as deeply as you want, and define them wherever.

Swift's only formal namespaceoid is the module, and to get namespaces at a finer level, people use empty enums (example from the standard library).

What are some pros and cons to these approaches, and what other approaches exist?

$\endgroup$

3 Answers 3

7
$\begingroup$

A common approach to namespacing is to use objects, particularly where single-object literals are part of the language. This is a common tradition in JavaScript, for example, and has been baked into other languages as the core of the module system; it's in very broad strokes how Python modules work. A namespace is then a variable containing an object full of methods, fields, types, classes, or whatever kinds of named value suit the language, and all those entities are accessed through the variable name.

Objects offer some advantages in interaction with the rest of the language:

  • All operations performed on them are the same as used elsewhere in the language, without an additional modality to learn: UI.View works the same way and means the same thing whether UI is a namespace object or one the user made.
  • They (often) offer controlled export innately: a private method is usable but not exposed, and these features are well-understood by the programmers.
  • They can be nested: an object can expose a field with another object, representing a layered namespace, or a field can be created on demand by the import system if appropriate.
  • They are shareable: a namespace object can be passed around to other code to use, for dependency injection or to scope its own operations.
  • They are aliasable: closely related to the previous point, code can always give a new name to a namespace locally if it's convenient or necessary just by putting the object into a local variable.
  • They are opaque: objects already represent an encapsulation boundary within the language model, so a namespace can be replaced, supplemented, or shimmed without other code needing to be aware.
  • They can be typed: in a typed language, the object will have a type and may be able to be ascribed further types, allowing separate declaration and validation of what the namespace provides, or additional information hiding.

There are some concomitant disadvantages, however:

  • An object is a dynamic entity with real run-time existence, meaning:
    • static analysis is harder
    • outside run-time values can incidentally affect the behaviour of the module
    • real memory is allocated, initialised, and disposed of
    • accesses may pay dynamic dispatch costs
  • In a typed language, namespaced types are a common want, and so types need to be permitted inside objects, but types are often static entities and can be an uncomfortable fit within objects.
  • Similarly, nested classes may be a poor fit in some languages, though languages where this model is prevalent often have a more uniform treatment of class construction (or none)
  • Having namespace accesses be undistinguished from ordinary operations may be undesirable and lead to mistakes in code
  • Finally, many languages do not have or want the ability to create one-off objects, and probably would not and should not include them just for the sake of easy namespacing. On the other hand, Python modules are one-off objects without object literals in the language and it doesn't seem to be a problem.

I worked a lot on a language that used objects as the fundamental basis of its module system, and I'd say that most of the disadvantages had limited impact, but types in objects were still a bit uncomfortable and needed care to make them statically safe. This was also a language with object literals as a core feature, so it already fit in well in that sense: each file of code was treated as implicitly wrapped inside an object literal, and other code could import that object. In a language with objects, this would be my first step for a namespace system at least.

$\endgroup$
2
  • $\begingroup$ Fully agree. Newspeak and (core) Scala do this really well. Scala has packages, but that's only for compatibility with the underlying host platform. (In Scala, the host platform is not an implementation detail, tight integration and seamless interoperability with the host platform is a specific design goal.) In the core of Scala, only objects and traits are needed and they can play triple role as modules and namespaces as well. $\endgroup$ Commented Jun 29, 2023 at 9:33
  • $\begingroup$ One difference between classes and namespaces (in e.g. C++ and C#, but perhaps not in Smalltalk?) is that classes are closed (defined once and for all) but namespaces are open (extensible, can be added to). Whether that's a pro or a con is sometimes debated. :) $\endgroup$
    – Pablo H
    Commented Jul 5, 2023 at 19:07
2
$\begingroup$

Everything is a namespace

The approach is to make any named construct a "namespacing resource". This normalizes the namespace resolution on statically typed languages.

A namespace is only a reserved name, carved in a global namespace. It's open, as new named constructs can be added at any time to a namespace. A namespace is not instantiable.

Classes, types and functions can be declared anywhere inside another named construct, and then they reserve their names in that name construct. In other words, classes can be declared inside classes, types inside types and methods inside methods.

Name resolution works somewhat like dynamic scoping. Referenced names are searched first in the actual namespace scope, then in fully namespaced entities.

The cons of this schema is that it is not that easy to grasp.

The pros is that a lot of local functionality is really declared as such, as internal types or internal functions cannot possibly escape (by name) from its declaration scope. Internal types are declared in the types where they are defined, and internal functions are only visible locally, with no risk of a collision with another code.

This, in turn, makes the open nature of namespaces a lot less scary.


This answer was inspired by Pablo H's comment, about a concept that I use in my experimental languages.

$\endgroup$
2
$\begingroup$

Another strategy is to completely dissociate names from other concerns. Let's say you have identifiers in your language, let's call them symbols because this answer is directly inspired by the Common Lisp way.

Anything that is named, a class, a function, a type or a variable is identified by a symbol, you manage symbols independently from the kind of objects it names.

Symbols are grouped together in packages.

Packages carry the notion of visibility, so you can mark types or functions as public or private by exporting or not a symbol. For protected visibility you need to assemble packages in layers that may imports and reexports subsets of symbols, which is best done with extensions like conduit-packages. This can be useful to emulate nested scopes, which is nice to have sometimes.

Most of the implementations of CL also allow :local-nickname to import a package as another name.

An example of symbol management that I find useful is the ability to have multiple packages that export different, possibly overlapping, sets of symbols for the same library.

For example you define package that only export user-visible functions:

(defpackage :server
  (:use) ;; use nothing
  (:export #:start
           #:stop
           #:make-server))

(one drawback is that you cannot say that you only want the functions to be accessible, anything named start, stop or make-server is accessible, even if its a type or class).

Another package can define symbols used when you are administrating the server:

(defpackage :server.admin
  (:use) ;; use nothing
  (:export #:setup
           #:pause
           #:resume))

Finally, you have an implementation package that uses both of them and also brings a lot of other symbols that are not accessible through the above interfaces:

(defpackage :server.implementation
  (:use :server :server.admin :common-lisp :tcp-server))

Elsewhere in your code, you can use :server, :server.admin or boths to access different facets of your server library.

Apart from that, there is no special case for classes or functions, the symbols that are accessible in the current package depends only on the package definition.

This can be a disadvantage because sometimes the scoping rules of other languages are nice: for enumerated types or class slot symbols you sometimes would like to define one package per type, something that is implicit elsewhere. This is something that is not done often in CL, and as a consequence you often have big packages where symbols are in fact made of a prefix: server-port, server-address, address-family, address-name, etc.

Something you could try to implement is for packages to have an optional parent package, which does not exist in Common Lisp.

$\endgroup$

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .