Class Tools
Table of Contents
- Introduction
- Library Installation
- Memoization
- Currying
- Partial Application
- Lazy Fields
- Fixed-Point
- Known Limitations
- Further Reading
Introduction
This library provides a number of utility decorators to enable the use of features commonly found in functional languages for use with classes.
Note that the license for this library is AGPL-3.0-only. You should know what that means before using this library. If you would like an exception to this license per section 7 contact the author.
Library Installation
As a dependency run the command:
npm install @final-hill/class-tools
You can also use a specific version:
npm install @final-hill/class-tools@1.0.0
For use in a webpage:
<script src="https://unpkg.com/@final-hill/class-tools"></script>
With a specific version:
<script src="https://unpkg.com/@final-hill/class-tools@1.0.0></script>
Memoization
The @memo
decorator memoizes
(caches) the results of the associated method call.
import {memo} from '@final-hill/class-tools';
class Fib {
@memo
calcMemo(n: number): number {
return n < 2 ? n : this.calcMemo(n - 1) + this.calcMemo(n - 2);
}
calc(n: number): number {
return n < 2 ? n : this.calc(n - 1) + this.calc(n - 2);
}
}
fib.calc(30) // 832040; 9ms
fib.calcMemo(30) // 832040; less than 1ms
Currying
The @curry
decorator converts the associated method into a method
that supports currying the parameters.
import {curry} from '@final-hill/class-tools';
class Adder {
@curry
add(a: number, b: number): number {
return a + b;
}
}
const adder = new Adder(),
addOne = adder.add(1);
addOne(3) // 4
addOne()(3) // 4
Partial Application
The @partial
decorator converts the associated method into
one that supports partial application of its parameters
import {partial, _} from '@final-hill/class-tools';
class A {
@partial
m(a: number, b: number, c: number): number { return a + b + c; }
}
const a = new A();
a.m(1,2,3) === 6
a.m(_,2,3)(1) === 6
a.m(1,_,3)(2) === 6
a.m(1,2,_)(3) === 6
a.m(1,_,_)(2,3) === 6
a.m(_,2,_)(1,3) === 6
a.m(_,_,3)(1,2) === 6
a.m(_,_,_)(1,2,3) === 6
a.m(_,_,_)(_,2,_)(1,3) === 6
Lazy Fields
The @lazy
decorator converts the associated getter into a lazily
initialized field. Practically this means that the body of the getter
will only executed once on its first use. Subsequent usages will
return the cached result of the first call.
import {lazy} from '@final-hill/class-tools';
class Counter {
static usage = 0;
constructor(){
Counter.usage++;
}
}
class Foo {
@lazy
get bar(): Counter { return new Counter(); }
}
const foo = new Foo();
void foo.bar;
void foo.bar;
void foo.bar;
Counter.usage // 1
Fixed-Point
The @fix
decorator can be assigned to methods in order to
control the behavior of its recursion and find its
least fixed-point.
It provides options to limit runaway recursion as well as
handle self-referential calls while returning a value.
The bottom
option defines the value to return when the recursive call
bottoms out. In other words, if the current method has been recursively
called with the same arguments then it is replaced with the value given.
import {fix} from '@final-hill/class-tools';
class Foo {
@fix({bottom: 0})
bar(): number {
return this.bar();
}
}
new Foo().bar() === 0;
Recursive calls may always vary in their arguments leading
to runaway recursion in a different way. The limit
option
prevents infinite recursion by replacing the nth call with
the value defined by the bottom
option:
import {fix} from '@final-hill/class-tools';
class Foo {
@fix({bottom: 0, limit: 10})
bar(n: number): number {
return 1 + this.bar(n + 1);
}
}
new Foo().bar(0) === 10;
The bottom
option can also be defined as a function if
a computed result is desired:
import {fix} from '@final-hill/class-tools';
class Foo {
@fix({bottom: (n: number) => n**2})
bar(n: number): number {
if(n <= 3) {
return 1 + this.bar(n + 1);
} else {
return this.bar(n); // bottom(4) == 4**2 == 16
}
}
}
new Foo().bar(0) === 20;
Known Limitations
When using TypeScript a decorator can not change the type of the associated class feature. This is a limitation of the language.
Depending on your usage you may need to perform explicit casting or utilize the // @ts-ignore
option. For example:
import {partial, _} from '@final-hill/class-tools';
class A {
@partial
m(a: number, b: number, c: number): number { return a + b + c; }
}
const a = new A();
a.m(1,2,3) === 6
// @ts-ignore
a.m(_,2,3)(1) === 6
// @ts-ignore
a.m(1,_,3)(2) === 6
// @ts-ignore
a.m(1,2,_)(3) === 6