Skip to content

Similar features in other languages (module scope) #16

@johnchristopherjones

Description

@johnchristopherjones

There are similar concepts in other languages, often called a "Symbol" or "Keyword".

Related Implementations

JavaScript

JavaScript has Symbol. It's distinct from the reference implementation in the following ways:

  • Each call to Symbol('foo') creates a new symbol (similar to Python's object()).
  • Symbol has two static methods:
    • Symbol.for(key) always returns the same Symbol from a global registry (global registry is not public)
    • Symbol.keyFor(symbol) always returns the registry key string ('foo') for a given symbol.
  • There is no module-scoped registry. Module scoping is implicit as variable declaration.
    • A call to Symbol.for('foo') in two different modules produces identical values.

JS' Symbols achieve module scope by simply declaring them once. They can be exported and imported elsewhere to effectively achieve module scopes without explicit namespaces.

// constants.js
export const MISSING = Symbol('missing')

// another module
import { MISSING } from 'constants';

Two or more modules can obtain identical symbols without depending on each other by using Symbol.for(key) with the same key, which provides access to the global registry.

// red.js
export const MISSING  = Symbol.for('missing');

// blue.js
import { MISSING } from 'red';

MISSING === Symbol.for('missing')

Surprisingly, Symbol('foo') !== Symbol('foo'), since each call to Symbol creates a new instance. This guarantees the global uniqueness of symbols and guards against accidental name collisions, but it is surprising. Thankfully, Symbol.for('foo') === Symbol.for('foo').

Clojure

Clojure has two concepts related to the Sentinel proposal: keyword and symbol. Conventionally in Clojure, keywords are used for sentinel values, though symbols can be used almost identically. Keywords have a literal syntax that makes them especially convenient. For a keyword "foo", the literal is :foo. Keywords can also be namespaced explicitly,:hello.world/foo, or implicitly in the current namespace, ::foo. There is also a functional notation: (keyword name) or (keyword ns name).

They differ from the reference implementation in that:

  • Each call to keyword with the same namespace and name returns the same (interned) object.
  • There is no module-scoped registry. Module scoping is implicit as variable declaration.
    • A call to (keyword "foo") in two different modules produces identical values.

Conclusion

Comparatively, the JS and Clojure implementations differ from the proposal in that neither has a module-scoped registry. Instead, they rely on (a) a global registry and (b) the scope of variables for module-scoped behavior.

Namespaces are a great idea and we don't do well enough by them in Python. (You can't switch namespaces in the Python REPL, for example.) But, I think we already have a module-scoped registry in Python that everyone's familiar with. It's called a module.

So, how about the following?

  • Sentinel(name) always produces an identical value, analogous to the typical behavior of str(name).
  • UniqueSentinel(name) always produces a new unique value, similar to object() but with a more readable repr. (Alternative option: Sentinel.unique(name).)
  • For module-scoped (namespaced) values, import a UniqueSentinel from a module: from my_module import MISSING
  • Optionally, provide get access to a global registry of UniqueSentinel, such as UniqueSentinel.get(module, name)
    • e.g., UniqueSentinel.get("mypackage.mymodule", "MISSING")

I think the trivial nature of this version Sentinel means that it's perhaps a very good candidate for the typing module. Without UniqueSentinel.get, I think UniqueSentinel could also belong in typing. This seems to cover every use case I'm aware of.

I don't know that the global registry implied by UniqueSentinel.get is really necessary. If it were, that might be motivation to put both in a distinct module. Having a distinct Sentinel and UniqueSentinel should obviate the need for a registry to re-use sentinels without importing.

As an aside, I personally prefer the name Symbol, but I recognize it's an overloaded term and might imply a lot more features than are being proposed.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions