imperative vs functional APIs or: how to work with constant objects?

# Claus Reinke (13 years ago)

Javascript data structure operations: some of the APIs are imperative, some are functional, some are mixed. Having functional equivalents to all imperative data structure operations is necessary to support expression-oriented programming over constant data structures.

"imperative" here refers to operations that are designed to be used as statements, changing inputs by side-effect, while "functional" refers to operations designed to be used as expressions, leaving their inputs unchanged. A familiar example is the Array API, e.g.:

array.concat([1])    - returns a modified copy of array, expression

array.push(1)        - modifies array in place, "statement"

It does seem as if older API elements tend to be imperative while newer ones tend to be functional. However, not all imperative operations have functional counterparts yet, and with increasing focus on constant and frozen objects, the need for functional operations is going to increase.

In particular, I have been wondering about how freezing objects curtails what I am able to do with them. Having worked with pure functional language for a long time, I really like the advantages of frozen/constant objects, both for avoiding bugs and to enable optimizations, but I'm not used to not having a full set of operations to work with such objects:

Example 1: constant functions, prototypes and toString

The standard API for functions requires imperative operations
for both prototypical inheritance and for string representation.

const C() {}
// C.prototype.toString = function() { return 'C()' }
// C.toString = function() { return 'C()' }

Example 2: property update

The standard API for object property update is imperative.

const obj = Object.freeze({ age : 1, .. });
const older_obj = // a modified copy of obj, with age : 2??

The standard APIs for these fundamental operations are imperative and so are not available on frozen objects (this also applies to the record strawman, for instance).

How does one achieve the effects of these operations on frozen objects, by creating modified copies? For an expression-oriented style of programming where no object is ever modified in place, it seems one has to use a lower level API, perhaps by calling Object.create and Object.freeze to implement some form of Object.clone, then mixing in the update on the clone before freezing it. Or perhaps using proxies to achieve the effect..

The situation is somewhat similar to the declarative object initializers being worked on as object literal extensions, but those don't seem to help here.

What is the best way of doing functional/declarative/expression- oriented object modification in ES5 or ES/next? Is there really an imperative-preferred flaw in the current design?

Claus

# Mark S. Miller (13 years ago)

On Tue, Jun 21, 2011 at 9:47 AM, Claus Reinke <claus.reinke at talk21.com>wrote:

Javascript data structure operations: some of the APIs are imperative, some are functional, some are mixed. Having functional equivalents to all imperative data structure operations is necessary to support expression-oriented programming over constant data structures.

"imperative" here refers to operations that are designed to be used as statements, changing inputs by side-effect, while "functional" refers to operations designed to be used as expressions, leaving their inputs unchanged. A familiar example is the Array API, e.g.:

array.concat([1]) - returns a modified copy of array, expression

array.push(1) - modifies array in place, "statement"

It does seem as if older API elements tend to be imperative while newer ones tend to be functional. However, not all imperative operations have functional counterparts yet, and with increasing focus on constant and frozen objects, the need for functional operations is going to increase.

In particular, I have been wondering about how freezing objects curtails what I am able to do with them. Having worked with pure functional language for a long time, I really like the advantages of frozen/constant objects, both for avoiding bugs and to enable optimizations, but I'm not used to not having a full set of operations to work with such objects:

Example 1: constant functions, prototypes and toString

The standard API for functions requires imperative operations for both prototypical inheritance and for string representation.

const C() {} // C.prototype.toString = function() { return 'C()' } // C.toString = function() { return 'C()' }

Example 2: property update

The standard API for object property update is imperative.

const obj = Object.freeze({ age : 1, .. }); const older_obj = // a modified copy of obj, with age : 2??

Hi Claus, interesting idea. Given

The idea of {x: a, y: b, ...: c} – record extension and row capturesuccessor-ml.org/index.php?title=Functional_record_extension_and_row_capture – for structuring and destructuring comes to mind, but ES objects are a different animal compared to ML records. For example, which properties of c are copied into the object being initialized by {x: a, y: b, ...: c}? enumerable? own? etc.

(from harmony:destructuring#issues)

you could say

const older_obj = { age: 2, ...: obj };

But as the note says, there are a bunch of open questions that need to be settled before this becomes a serious candidate. We should examine how this interacts with other aspects of extended object literals.

# Claus Reinke (13 years ago)

Javascript data structure operations: some of the APIs are imperative, some are functional, some are mixed. Having functional equivalents to all imperative data structure operations is necessary to support expression-oriented programming over constant data structures.

"imperative" here refers to operations that are designed to be used as statements, changing inputs by side-effect, while "functional" refers to operations designed to be used as expressions, leaving their inputs unchanged. ..

Hi Claus, interesting idea. Given

The idea of {x: a, y: b, ...: c} – record extension and row capturesuccessor-ml.org/index.php?title=Functional_record_extension_and_row_capture – for structuring and destructuring comes to mind, but ES objects are a different animal compared to ML records. For example, which properties of c are copied into the object being initialized by {x: a, y: b, ...: c}? enumerable? own? etc.

(from harmony:destructuring#issues)

you could say

const older_obj = { age: 2, ...: obj };

But as the note says, there are a bunch of open questions that need to be settled before this becomes a serious candidate. We should examine how this interacts with other aspects of extended object literals.

Hi Mark,

thanks for the pointer.

Yes, there used to be a large body of literature just on the various ways of factoring a record operations API, mostly driven by their use for semantics of object-oriented language variations and by their type system difficulties ('record calculi', 'semantics of inheritance/oo', 'typing record concatenation', ..).

Record/object extension is one of the recurring ideas, and since it is based on record/object literals + a base record/object, most of the design choices follow from the extended object literals.

My immediate concern is simply: given the choices already made for Javascript object operations, why don't we have a complete set of operations for both in-place and copying updates? The current situation at the object level is as if JS only had '+=' and '-=', but not '+' and '-', at the level of numbers.

As for the set of operations, JS has already chosen that updating a non-existing property extends the object, so update and extension are mixed in one operation. There is a separate operation for removing properties and there are tests for the existence of properties, both locally and in the prototype chain. If there were functional equivalents to the imperative base operations

obj.prop = value;
delete obj.prop;

then one could build more complex operations from there, instead of from scratch. delete already returns the object, but assignment returns the value, and overloading delete to produce an object copy if the input object is frozen would be confusing, so we need new syntax, and object extension with extended object literals could do.

Assuming that the extended object is copied as is, modified as specifiable by the extended object literal syntax, I'd assume

{ prop : value; ...: obj }

to be a (shallow) copy of obj, identical (including prototype and meta properties such as frozen/configurable/..) in all but prop.

If I don't want to copy prototype or meta-properties, I'd need to use records instead of objects, or get some help from the extended object literal syntax.

But one shouldn't try to fit everything into the object literal syntax, either - once it is accepted that copying objects is useful, there could be an Object.filter operation, eg, to filter out properties based on their meta-properties (enumerable, own, ..):

Object.filter(predicate,obj)

would return a (shallow) copy of obj with properties filtered according to some predicate function.

To avoid an additional operator for functional delete, one might add a delete directive to extended object literal syntax, and have

{ delete prop; ...: obj}

be a (shallow) copy of obj, identical in all but prop.

Wouldn't that be sufficient as a baseline? If not, what is missing?

Is extended object literal syntax capable of handling this slightly extended range of use? Btw, should there be a way to un-freeze object copies (typical application is to make a non-frozen copy of a frozen array, update it incrementally and in-place in a loop, then freeze the resulting array copy for further use)?

In sum, copying object modifications are useful for programmers, and taking them into consideration might ease the constraints for ES language design (not everything has to fit into the simple in-place update all-permitted-or-all forbidden scheme - copying updates offer a third option).

Claus