cwal

s2: enums
Login

s2: enums

(⬑Table of Contents)

The Enum Type

Jump to...

Enums

Enums were added to the trunk on 20160107.

In s2, "enums" are a special-case type of object intended to denote an immutable set of globally unique keys, optionally with values associated to them. They are notably different from C/C++ enums, being more like (but not identical to) Java enums. Unlike conventional enums, s2 enums do not have an inherent ordinal/integer value, but each may optionally have a single value (of any type) associated with it. An enum can be thought of as a special-case object with an immutable set of keys which triggers an error if any unknown key is referenced.

Each enum entry is of a cwal type called (for lack of a better name) "unique." A "unique" value is intended primarily to act as a unique key which will never compare as equivalent to any other value. Each entry may optionally wrap a single value of any type. Referencing an enum entry via the property access operators, e.g. myEnum.X, resolves to the entry itself (its unique identity), and accessing the value pseudo-property of that entry resolves to its wrapped value (if any). (Examples are below.)

Enums are different from other types of containers in several a number of notable ways:

A brief example:

const e = enum OptionalTypeName {
    a, // its own name (a string) as its value
    b: 2, // any value type is fine
    f: proc(){return this}
};
assert 3 === e.#;
assert 'a' === e.a.value;
assert 'a' === e::a; // equivalent
assert 2 === e.b.value;
assert 2 === e::b; // equivalent unless the entry.value is a Function call:
assert e.f === e.f.value(); // e.f is bound as 'this'
assert e::f === e::f(); // no 'this', so e.f.value is its own 'this'
assert 'b' === e[e.b]; // get the string-form name of an entry
assert e.a === e['a']; // note that e['a'] is functionally identical to e.a.
assert 'OptionalTypeName' === typeinfo(name e);
assert undefined === e->'c'; //-> op does not throw for unknown properties
assert catch {e#'c'}.codeString()==='CWAL_RC_TYPE'; // hash op not allowed
assert catch {e::c}.codeString()==='CWAL_RC_NOT_FOUND'; // unknown property
assert catch {e.c}.codeString()==='CWAL_RC_NOT_FOUND'; // unknown property
assert catch {e.a = 1}.codeString()==='CWAL_RC_DISALLOW_PROP_SET';

Enums treat standalone keys (without values) differently than objects do: an entry entry with no value gets (as of 20171204) the string form of its identifier as its value. e.g. enum{x} has a single entry, x, with a string value of x. (This change was made after practice showed that them defaulting to undefined, as they did before, was pretty useless.)

A feature-complete example can be found in the s2 unit tests.

Note that enum entries never compare equivalent to any other value, and yet still evaluate to true in a boolean context:

assert true === !!e.a; // will pass, but…
assert true == e.a; // will fail!
assert false == e.a; // will also fail!

Open points:

Enum Members

Enums all share a common prototype (which itself has no prototype) with the methods listed below. The prototype may be modified further by clients.

Enum eachEnumEntry( Function(entryName, entryIdentity) )

For each distinct entry in the enum, this routine calls the given callback, passing it the enum entry name and its unique identity (in that order). The 2nd argument is an enum entry, so to get its wrapped value (if any), fetch its value property. Propagates any exceptions. If the callback returns a literal false (not another falsy value) then iteration stops without an error. Note that while each enum holds a reverse mapping of its entries to its names, the callback is not called twice by this method - it is only passed they name/identity mappings, not the identity/name mappings. In the context of the callback, the enum itself is bound to this and eachEnumEntry() returns its this. Remember that the order of enum entries is indeterminate, meaning the callback will not be passed the entries in a well-defined order.

Note that foreach(anEnum=>name,identity) is much more efficient than using this function (but this method predates that feature).

Potential TODO(???): in the context of the callback, make this the enum entry (the Unique-type value).

Array getEnumKeys()

Returns an array containing the names of all entries in the enum (in an unspecified order, since they come from storage with unspecified ordering).

bool hasEnumEntry(key)

Returns true if the enum part of this value (not including prototypes or derived parts) contains the given key, else false. Note that this will resolve both the "forward" and "reverse" enum entries, so the key may be either an entry's name or its identity.

mixed operator->(key)

Overloaded to perform a non-throwing enum entry lookup, only checking for enum properties (only those listed in the body of the enum). Note that this will resolve both the "forward" and "reverse" enum entries, so the key may be either an entry's name or its identity. It will return the entry it finds, or the undefined value for unknown entries. Notes:

mixed operator::key

Overloaded to resolve to the value wrapped by the given key in the enum, rather than resolving to the entry itself. The RHS of the this operator may be an identifier literal or an expression which evaluates to a string value. e.g. given enum e{a:1}, then: e.a.value === e::a and e::a === e::'a' and e::a === e::(e[e.a]). Like the dot operator, it throws if the given property is not in this enum.

Enum Tips and Tricks

Enum Entries without Enums

Currently, the only mechanism s2 provides for creating values of the type used for enum entries is constructing an enum. Enum entries need not be left tied to their initial enum, though. They can be copied around just like any other properties, and can be used as both keys and values:

var e = enum { a, b, c };
var x = {};
var a = [];
foreach(e=>k,v) { x[k] = v; x[v] = k; a.push(k, v) }
// or: e.eachEnumEntry(proc(k,v){x[k]=v; x[v]=k; a[]=k;a[]=v});
assert x.a === e.a; // === enum entry
assert x[e.a] === e[e.a]; // === reverse mapping, i.e. "a"
assert a.indexOf(e.a) >= 0;
unset e; // x/a both still hold refs to the enum entries
assert a.indexOf('b') >= 0;

If script code needs some guaranteed-unique key, enum entries can be useful even if one doesn't really need an enum (simply discard the enum after extracting the keys).

"Sealed" Objects

An enum is immutable, making it potentially suitable for use as a "sealed" configuration object, namespace, or similar. The notable wart there is the requirement to use enumEntry.value to access the underlying values:

const e = enum {
 f: function(){...}
};
e.f.value(); // kinda ugly

Hmm. Necessity being the mother of invention, that led to the the :: operator being introduced. See the next section…

The :: Operator and Binding of this in enum.entry.value() Calls

To simplify access to enum.entry.value, enums overload the :: operator so that we can do...

const e = enum {
 a: 1,
 f: function(){...}
};
assert e.a.value === e::a; // equivalent operations
e::f(); // *almost* equivalent to e.f.value()

The minor difference between e.f.value and e::f comes when they resolve to a Function which is called in the next operation. The e.f.value() form will bind the enum entry f as the this of the function call, where :: does not (which means that f.value becomes its own this in the context of a call). A demonstration of that difference:

[stephan@host:~/fossil/cwal/s2]$ ./s2sh ...
s2sh> var e = enum {f:proc(){print(this)}}
s2sh> e.f.value()
unique@2211050
s2sh> print(e.f)
unique@2211050
s2sh> e::f()
function@22332B0
s2sh> print(e::f)
function@22332B0

In practice that makes effectively no difference, as such functions are unlikely to be able to do anything interesting with this in either case. If the embedded functions need access to the outer enum, it can be provided after initialization of the enum, like so:

const e = enum {
 f1: proc(){assert 'enum' === typeinfo(name E)},
 f2: proc(){assert 'unique' === typeinfo(name E.f1)}
};
const imports = {E:e};
foreach(e=>k,v) v.value.importSymbols(imports)
/**
  note: we don't inline the imports Object in importSymbols argument
  list b/c that would create a new wrapper Object and "E" key for each
  iteration!

  Note that importSymbols() will overwrite any symbols which were
  installed in such functions via the "using" modifier.
*/;