Effection

Effection is a library for managing side effects in Racket. It supports a certain programming style that’s almost pure, but which also has the ability to introduce handlers for custom side effects.

The notion of purity Effection uses is chosen deliberately, and it’s meant to facilitate commutative extensibility mechanisms by way of quasi-deterministic concurrency.

The notions of side effect in Effection are also chosen according to elaborate reasoning, although at this point they’re very experimental. The most remarkable feature of Effection’s side effects is that the dynamically scoped regions they’re observable in can have dynamically scoped holes inside, which for instance can take undesired side effects out of scope for controlled periods of time.

This is all a work in progress. The only pieces of Effection that are fully implemented at this point are some pure utilities that will come in handy for commutatively merging values.

2Order

A “cline” is based on a total ordering on values in its domain, or in other words a binary relation that is reflexive, transitive, and antisymmetric. Its antisymmetry is as fine-grained as possible: If any two values in a cline’s domain are related by that cline in both directions, only Effection-unsafe code will be able to distinguish the two values.

However, a cline does not merely expose this total ordering. Within the cline’s domain, there may be equivalence classes of values for which every two nonequal values will not have their relative order exposed to Effection-safe code. When Effection-safe code uses compare-by-cline to compare two values by a cline, it can get several results:

Returns whether the given value is an opaque value that represents the result of a comparison where the first value turned out to be secretly strictly less than or secretly strictly greater than the second value.

2.2Names, Dexes, and Dexables

Returns whether the given value is a name. In Effection, a "name" is something like a partial application of comparison by a dex. Any value can be converted to a name using name-of if any dex for that value is at hand (and it always converts to the same name regardless of which dex is chosen), and names themselves can be compared using dex-name.

All presently existing dexes allow this comparison to be fine-grained enough that it trivializes their equational theory. For instance, (dex-default(dex-give-up)(dex-give-up)) and (dex-give-up) can be distinguished this way despite otherwise having equivalent behavior.

Given a dexable function, returns a dex that works by invoking that function with each value to get (justdex) or (nothing), verifying that the two dex values are the same, and then proceeding to tail-call that dex value.

When compared by dex-dex, all dex-by-own-method values are ordering-eq if their dexable-get-method values’ dexes and values are.

Returns a dex that compares instances of the structure type named by struct-id, and whose field values can be compared by the dexes produced by the dex-expr expressions.

Each field-position-nat must be a distinct number indicating which field should be checked by the associated dex, and there must be an entry for every field.

For the sake of nontermination, error, and performance concerns, this dex computes by attempting the given dexes in the order they appear in this call. If a dex before the last one determines a non-ordering-eq result, the following dexes are only checked to be sure their domains contain the respective field values. Otherwise, the last dex, if any, is attempted as a tail call.

A struct type is only permitted for struct-id if it’s fully immutable and has no super-type.

When compared by dex-dex, all dex-struct-by-field-position values are ordering-eq if they’re for the same structure type descriptor, if they have field-position-nat values in the same sequence, and if their dex-expr values are ordering-eq.

Returns a dex that compares instances of the structure type named by struct-id, and whose field values can be compared by the dexes produced by the dex-expr expressions.

For the sake of nontermination, error, and performance concerns, this dex computes by attempting the given dexes in the order they appear in this call. The last dex, if any, is attempted as a tail call.

A struct type is only permitted for struct-id if it’s fully immutable and has no super-type.

All presently existing clines allow this comparison to be fine-grained enough that it trivializes their equational theory. For instance, (cline-default(cline-give-up)(cline-give-up)) and (cline-give-up) can be distinguished this way despite otherwise having equivalent behavior.

Given two clines, returns a cline over the union of their domains. The resulting cline’s ascending order consists of the first cline’s ascending order in its domain, followed by the second cline’s ascending order outside the first cline’s domain.

For the sake of nontermination, error, and performance concerns, this attempts to compute the result using cline-for-trying-first before it moves on to cline-for-trying-second.

The invocation of cline-for-trying-second is a tail call.

When compared by dex-cline, all cline-default values are ordering-eq if their cline-for-trying-first values are and their cline-for-trying-second values are.

Given a dexable function, returns a cline that works by invoking that function with each value to get (justcline) or (nothing), verifying that the two cline values are the same, and then proceeding to tail-call that value.

When compared by dex-cline, all cline-by-own-method values are ordering-eq if their dexable-get-method values’ dexes and values are.

Returns a cline that compares instances of the structure type named by struct-id, and whose field values can be compared by the clines produced by the cline-expr expressions. The comparison is lexicographic, with the most significant comparisons being the cline-expr values that appear earliest in this call.

Each field-position-nat must be a distinct number indicating which field should be checked by the associated cline, and there must be an entry for every field.

For the sake of nontermination, error, and performance concerns, this cline computes by attempting the given clines in the order they appear in this call. The last cline, if any, is attempted as a tail call.

A struct type is only permitted for struct-id if it’s fully immutable and has no super-type.

When compared by dex-cline, all cline-struct-by-field-position values are ordering-eq if they’re for the same structure type descriptor, if they have field-position-nat values in the same sequence, and if their cline-expr values are ordering-eq.

Returns a cline that compares instances of the structure type named by struct-id, and whose field values can be compared by the clines produced by the cline-expr expressions. The comparison is lexicographic, with the most significant comparisons being the cline-expr values that appear earliest in this call.

For the sake of nontermination, error, and performance concerns, this cline computes by attempting the given clines in the order they appear in this call. If a cline before the last one determines a non-ordering-eq result, the following clines are only checked to be sure their domains contain the respective field values. Otherwise, the last cline, if any, is attempted as a tail call.

A struct type is only permitted for struct-id if it’s fully immutable and has no super-type.

2.4Merges and Fuses

Effection offers a non-exhaustive but extensive selection of "merges" and "fuses." These are values which can be compared for equality with like values (using dex-merge and dex-fuse), and they represent operations of two arguments (invocable using call-merge and call-fuse).

Merges represent operations that are commutative, associative, and idempotent, or in other words exactly the kind of operation that can operate on a (nonempty and finite) unordered set of inputs.

Fuses represent operations that are commutative and associative (and not necessarily idempotent). A fuse is ideal for operating on a (nonempty and finite) unordered multiset of inputs.

The idempotence of a merge operation is such enough that if the two inputs to the merge are order-eq by any dex, the result will be order-eq to them both by the same dex.

Given a merge/fuse and two values, combines those values according to the merge/fuse. The result is (nothing) if either value is outside the merge’s/fuse’s domain. Otherwise, the result is (justvalue) for some value that’s also in the domain.

For call-merge, if there is any dex for which the input values are ordering-eq, then the result will be ordering-eq to them both.

Given a dexable function, returns a fuse that works by invoking that function with each value to get (justmethod) or (nothing), verifying that the two method values are the same, and invoking that merge/fuse value to get a result of (justresult) or (nothing). If the result is (justresult), this does a final check before returning it: It invokes the method-getting function on the result to verify that it obtains the same method value that was obtained from the inputs. This ensures that the operation is associative.

When compared by dex-merge/dex-fuse, all merge-by-own-method/fuse-by-own-method values are ordering-eq if their dexable-get-method values’ dexes and values are.

Returns a merge/fuse that combines instances of the structure type named by struct-id, and whose field values can be combined by the merges/fuses produced by the field-method-expr expressions.

Each field-position-nat must be a distinct number indicating which field should be checked by the associated merge/fuse, and there must be an entry for every field.

A struct type is only permitted for struct-id if it’s fully immutable and has no super-type.

When compared by dex-merge/dex-fuse, all merge-struct-by-field-position/fuse-struct-by-field-position values are ordering-eq if they’re for the same structure type descriptor, if they have field-position-nat values in the same sequence, and if their field-method-expr values are ordering-eq.