Once classes

by Alexander Kogtenkov (modified: 2020 Dec 15)

Background

Once classes represent a mechanism to specify unique values in a program. The values behave like any other objects, but preserve their identity at creation time. In other words, only a single distinguishable instance of a class is created regardless of the number of times the creation is used. This enables discrimination over the values of multi-branch constructs similarly to integers.

Declaration

A once class is declared with a keyword once in front of the class declaration. All creation procedures listed in the declaration should be once procedures. As usual, they can be used to initialize some attributes:

once class DIRECTION create down, left, right, up feature {NONE} -- Creation down once y_scroll := 3 end left once x_scroll := -1 end right once x_scroll := 1 end up once y_scroll := -3 end feature -- Access x_scroll: INTEGER assign set_x_scroll -- The number of columns to scroll when current direction is used. -- Positive value is used for scrolling right, negative for scrolling left. y_scroll: INTEGER -- The number of lines to scroll when current direction is used. -- Positive value is used for scrolling down, negative for scrolling up. scroll (p: POINT): POINT -- Compute the position of `p` after scrolling. require non_negative_x: p.x >= 0 non_negative_y: p.y >= 0 do create Result.make ((p.x + x_scroll).max (0), (p.y + y_scroll).max (0)) ensure non_negative_x: Result.x >= 0 non_negative_y: Result.y >= 0 end set_x_scroll (new_x_scroll: like x_scroll) -- Set `x_scroll` to the given value. require same_direction: x_scroll.sign = new_x_scroll.sign do x_scroll := new_x_scroll ensure x_scroll_set: x_scroll = new_x_scroll end end

Once classes are automatically frozen, i.e. cannot be used as parents of other classes.

Access

Objects of a once class can be created using the regular object creation syntax, for example, create direction.up -- Creation instruction foo (create {DIRECTION}.up) -- Creation expression where the variable direction is of type DIRECTION.

There is also a simplified version for creation expressions: the keyword create can be omitted:

foo ({DIRECTION}.up) -- Creation expression

Semantics

Uniqueness

For any creation instruction only a single object is created. Successive attempts to create an object with the same creation procedure yields the object obtained the first time.

create direction.up print (direction = {DIRECTION}.up) -- Prints `True`: the same creation procedure is used. create direction.down print (direction = {DIRECTION}.up) -- Prints `False`: different creation procedures are used.

In particular, this allows for using equality tests in conditionals to check the value of the object

if direction = {DIRECTION}.up then -- Do something. else -- Do something else. end

Also, given that only one instance created with a given creation procedure exists, changing fields of the object updates the fields for any other access:

print ({DIRECTION}.left.x_scroll) -- Prints `-1`. {DIRECTION}.left.x_scroll := -4 print ({DIRECTION}.left.x_scroll) -- Prints `-4`.

Multi-branch constructs

Single values

Expressions based on a once class can be used as inspect expressions in multi-branch constructs. Case values are discriminated by the once class creation procedures:

print (inspect direction when {DIRECTION}.down then "Down" when {DIRECTION}.left then "Left" when {DIRECTION}.right then "Right" when {DIRECTION}.up then "Up" end)

Intervals

The order of creation procedures is essential when multi-branch constructs are using intervals. The multi-branch constructs rely on the relative order of creation procedures listed in the creation clause of the class. For example, if days of week are modelled using the following once class

once class DAY_OF_WEEK create Sunday, Monday, Tuesday, Wednesday, Thursday, Friday, Saturday ...

then it could be used in the interval:

inspect day when {DAY_OF_WEEK}.Monday .. {DAY_OF_WEEK}.Friday then is_weekend := False else is_weekend := True end

SCOOP

The mechanism of once classes is orthogonal to concurrency. Therefore, if no once keys are specified, they follow the default (once-per-thread) behavior, i.e. every thread in a SCOOP application has its own set of unique instances, and instances between two different threads are different. If instances have to be unique throughout the whole system regardless of threading, the corresponding once key ("PROCESS") should be specified. In this case the once value is instantiated in its own SCOOP region and only once.

Consider the following code snippet:

local d1, d2: separate DIRECTION do create d1.up create d2.up print (d1 = d2) end

With the declaration above, it prints False because instances are created in different regions using the once-per-thread creation procedure up.

If the declaration of this procedure is changed to look like

up once ("PROCESS") y_scroll := -3 end

the code prints True instead, because the single instance of the class is created for all threads and SCOOP regions.

Other use cases

Iteration

The ability to iterate over all values of a once class can be added using manifest arrays:

instances: ITERABLE [DAY_OF_WEEK] -- All days of week. once Result := <<{DAY_OF_WEEK}.Sunday, {DAY_OF_WEEK}.Monday, {DAY_OF_WEEK}.Tuesday, {DAY_OF_WEEK}.Wednesday, {DAY_OF_WEEK}.Thursday, {DAY_OF_WEEK}.Friday, {DAY_OF_WEEK}.Saturday>> ensure class end

Then, assuming that there is a feature name in the class DAY_OF_WEEK that returns the name of the specified day, the following symbolic loop would print all days of week:

⟳ d: {DAY_OF_WEEK}.instances ¦ print (d.name + " ") ⟲

Singleton

A commonly known programming pattern called "singleton" can be coded using some other means of Eiffel, but with once classes it becomes straightforward: just use a once class with a single creation procedure.