Ecosyste.ms: Awesome

An open API service indexing awesome lists of open source software.

Awesome Lists | Featured Topics | Projects

https://github.com/dgp1130/ctor-exp


https://github.com/dgp1130/ctor-exp

Last synced: 12 days ago
JSON representation

Awesome Lists containing this project

README

        

# ctor-exp

An experimental implementation of `ctor`, a new paradigm for constructing
objects as described in [Construct Better](https://blog.dwac.dev/posts/ctor/).
This is implemented as a simple TypeScript library because it's type system is
~~abusable~~ powerful enough to provide most of the critical features of
`ctor`. Someone smarter than me could probably do better with a custom
compiler or plugin, but this is good enough for an experimental implementation.

This is not intended for production usage or anything beyond basic
experimentation. **DO NOT USE THIS IN REAL CODE**. You have been warned.

Retrofitting an existing language's constructor semantics to fit a new paradigm
is a losing battle and should not be done, I am merely doing it here for
explanatory purposes and to give developers something they can actually use in
order to evaluate how effective this system actually is.

## Installation

Install the package via NPM and import/require it accordingly. The only two
symbols exported are `ctor`, and `from()` (described below).

```shell
npm install ctor-exp
```

## Usage

This explains with very rough examples how to use `ctor` as a constructor
engine.

### Basic Construction

Classes should be defined with public constructors that follow the format
(examples are TypeScript, JavaScript can be used by simply dropping the types):

```typescript
class Foo {
// Declare class fields.
private foo: string;
public bar: number;
private readonly baz: boolean;
private other: string = 'something';

// Boilerplate, uninteresting constructor. In a real implementation this
// would be automatically generated by the compiler, but for this experiment
// it must be hand-written. See Invariants section for more info on the
// precise requirements of constructors.
public constructor({ foo, bar, baz }: {
foo: string,
bar: number,
baz: boolean,
}) {
// Directly assign constructor parameters to class fields, do not do any
// additional computation in the constructor.
this.foo = foo;
this.bar = bar;
this.baz = baz;
// Fields unrelated to the constructor (like `other`) can be left out.
}
}
```

Now that the boilerplate constructor exists, we can use `ctor` to construct
it. We can introduce a factory to construct this type:

```typescript
import { ctor } from 'ctor-exp';

class Foo {
// Snip - Class fields and constructor...

// Factory to use when creating a `Foo` object.
public static from(foo: string, bar: number): Foo {
const baz = foo.length === bar; // Do some work..

// Construct `Foo` when ready!
return ctor.new(Foo, { foo, bar, baz }).construct();
}
}
```

Using this system we have simple, boilerplate constructors and all the
initialization logic is performed in separate factories.

### Inheritance

What has been shown so far looks like a lot of boilerplate for simple
constructor patterns that could be easily followed in any existing programming
language. However, the tricky part is inheritance, so let's extend something!

```typescript
import { ctor, from, Implementation } from 'ctor-exp';

class Foo {
public readonly foo: string;

// Boilerplate, uninteresting constructor.
public constructor({ foo }: { foo: string }) {
this.foo = foo;
}

// Factory for creating a `ctor`, extendable by subclasses.
public static from(foo: string): ctor {
const oof = foo.split('').reverse().join(''); // Do some work...

// Return the `ctor`, just don't `.construct()` it yet.
return ctor.new(Foo, { foo: oof });
}
}

// Extend an implementation of `Foo`, generated by `ctor`. This is necessary
// to properly construct this extended class with `ctor`.
class Bar extends Implementation() {
public readonly bar: string;

// Also boilerplate, equally uninteresting constructor.
// Only difference is an empty `super()` call. No need to provide any
// parameters to `Foo`, `ctor` will do that for you.
public constructor({ bar }: { bar: string }) {
super();
this.bar = bar;
}

// Factory for creating a `Bar`, composing `Foo.from()`.
public static from(foo: string, bar: string): Bar {
// Get a `ctor` by calling `Foo`'s factory.
const fooCtor = Foo.from(foo);

// Constrct `Bar` by extending the return `ctor`.
return from(fooCtor).new(Bar, { bar }).construct();
}
}
```

Using `from()`, we're able to cleanly compose and reuse the `ctor` object
returned from `Foo.createFoo()`.

You should *never* extend a class directly. Always extend
`Implementation()` instead.

If you ever want to call a superclass method, rather than using
`super.method()`, you should use `this._super.method()`. This is just a quirk of
how the library is implemented on top of the existing JavaScript class paradigm.

### Mixins

Because all classes extend `Implementation()`, it means that
they only have a type reference to their superclass, rather than a value
reference. This is important because it means that all classes do not actually
have direct knowledge of their superclasses, only knowledge of the interface of
the superclass. This is particularly useful for
[mixins](https://en.wikipedia.org/wiki/Mixin).

```typescript
import { ctor, from, Implementation } from 'ctor-exp';

// Declare a mixin which extends any object.
class Mixin extends Implementation() {
private readonly data: string;

// Another boilerplate, uninteresting constructor.
// Also need an empty `super()` call for mixins.
public constructor({ data }: { data: string }) {
super();
this.data = data;
}

public mixin(): string {
return this.data;
}

// Accept a parent `ctor` object of any type and extend it with `from()`.
// Can provide any constructor arguments to mixin and then return an
// intersection of the parent type and `Mixin`.
public static from(parent: ctor, data: string):
ctor {
// Extend normally, except with the `.mixin()` function to allow any
// superclass type!
return from(parent).mixin(Mixin, { data });
}
}

// Parent class is unrelated to `Mixin` and has no knowledge of it.
class Parent {
public parent(): string {
return 'parent';
}

public static from(): ctor {
return ctor.new(Parent);
}
}

// Child extends `Parent` with `Mixin` included.
class Child extends Implementation() {
public child(): string {
return this.parent() + this.mixin();
}

public static from(mixinData: string): Child {
// Get a `ctor` as normal.
const parentCtor = Parent.from();

// Extend with `Mixin`.
const mixinCtor = Mixin.from(parentCtor, mixinData);

// Further extend with `Child` and construct.
return from(mixinCtor).new(Child).construct();
}
}
```

This allows mixins to be defined without knowledge of a superclass and to be
extended and constructed just like normal inheritance! You can also have type
constrained mixins, which enforce a particular interface on their superclass.

```typescript
import { ctor, from, Implementation } from 'ctor-exp';

interface MixinParent {
parent(): string;
}

// Extend an unknown implementation of some interface.
class Mixin extends Implementation() {
public mixin(): string {
return this.parent(); // Parent interface is usable.
}

// Define `parentCtor` as a `ctor` and mixin normally.
public static from(parentCtor: ctor):
ctor {
return from(parentCtor).mixin(Mixin);
}
}

// A parent class implementing the required interface to support `Mixin`.
class GoodParent implements MixinParent {
public parent(): string {
return 'parent';
}

public static from(): ctor {
return ctor.new(GoodParent);
}
}

// A child class which uses a valid superclass `GoodParent` with `Mixin`.
class GoodChild extends Implementation() {
public static from(): GoodChild {
const parentCtor = GoodParent.from();
// `parentCtor` satisfies `ctor`
const mixinCtor = Mixin.from(parentCtor);
return from(mixinCtor).new(GoodChild);
}
}

// A parent class which does **not** implement the required interface to support
// `Mixin`.
class BadParent {
public static from(): ctor {
return ctor.new(BadParent);
}
}

// A child class which uses an invalid superclass `BadParent` with `Mixin`.
class BadChild extends Implementation() {
public static from(): BadChild {
const parentCtor = BadParent.from();
// COMPILE ERROR: `parentCtor` does not satisfy `ctor`.
const mixinCtor = Mixin.from(parentCtor);
return from(mixinCtor).new(BadChild);
}
}
```

These mixins look and act just like traditional classes. There is no need for
a function which transforms a class definition to include mixin funcitonality
as is normally necessary in JavaScript.

## Examples

The idea of using a "dumb" constructor that
only assigns class fields combined with composeable factories that perform the
real business logic in a form which cleanly supports inheritance allows an
easier implementation of many common problems in computer science.

Check out some examples which take simple use cases and show how they can be
surprisingly tricky using traditional constructors. Then look at the `ctor`
implementation to see how much simpler these solutions can be.

* [Basic use](./src/ctor_test.ts)
* [Factory composition](./src/factories_test.ts)
* [Dependency injection of `ctor`](./src/injection_test.ts)
* [Deep cloning objects](./src/clone_test.ts)
* [Serialization/deserialization](./src/serialization_test.ts)
* [Constructor coupling](./src/coupling_test.ts)
* [Mixins](./src/mixin_test.ts)
* [An optimized set using multiple superclass implementations](./src/optimized_set_test.ts)
* [Framework](./src/framework_test.ts)

## Invariants

Since this constructor paradigm is intended for a brand new programming
language, not all of its features/restrictions can be implemented in this
experimental library. As a result, there are a few invariants to keep in mind
when using it to ensure that you are using it in a way that would be supported
by a real compiler. Some of these restrictions are partially enforced by the
type systems, others are not.

* Constructors **must** merely assign parameters to class fields and should
not contain any additional logic. Such logic should be implemented in
factories.
* Constructors *should* use named parameters, but that is not strictly
necessary in this implementation.
* Subclasses **must** extend `Implementation` and should **never**
extend a `SuperClass` directly.
* When calling a superclass method, you **must** use `this._super.method()`
and never use `super.method()`, as it won't have the method you are calling.
* Mixins with type constraints on their superclass **must** use factories
which accept a parent `ctor` which explicitly `extends` the superclass
type.
* The library may incorrectly allow superclass implementations which do
not satisfy the superclass type if you forget to do this.
* Do not call `new Foo()` directly on a subclass. It is reasonable to use
`new` on a class which does not extend another parent class, however
subclasses must always be constructed with
`from(parentCtor).new(/* ... */)`.
* `ctor.new(Foo, /* ... */)` should only be used within `Foo` itself (via
methods on the class).
* Invoking `ctor.new()` is an implementation detail of the class being
constructed.
* Subclasses should **not** call `ctor.new(ParentClass, /* ... */)`, they
should call a factory which returns `ctor`.
* Calling `.construct()` is perfectly reasonable from any context.
* `from(ctor)` should only be used to immediately call
`.new(SubClass, /* ... */)` on its result.
* The type returned by `from()` is an implementation detail of `ctor`,
which should not be observed by the program.