From 1c70da5f3bdff7301a40955fccbcb9c0dce8ffd7 Mon Sep 17 00:00:00 2001 From: Freywar Ulvnaudgari Date: Tue, 30 Jul 2024 22:36:22 +0300 Subject: [PATCH] Basic structures --- jest.config.js | 7 -- jest.config.ts | 9 ++ package.json | 1 + src/bool.ts | 30 +++++++ src/core.ts | 45 +++++++++- src/index.ts | 4 + src/list.ts | 50 +++++++++++ src/num.ts | 40 +++++++++ test/bool.spec.ts | 118 +++++++++++++++++++++++++ test/core.spec.ts | 18 ++-- test/list.spec.ts | 211 +++++++++++++++++++++++++++++++++++++++++++++ test/num.spec.ts | 169 ++++++++++++++++++++++++++++++++++++ test/tsconfig.json | 6 ++ test/util.ts | 96 +++++++++++++++++++++ tsconfig.json | 2 + yarn.lock | 95 +++++++++++++++++++- 16 files changed, 885 insertions(+), 16 deletions(-) delete mode 100644 jest.config.js create mode 100644 jest.config.ts create mode 100644 src/bool.ts create mode 100644 src/list.ts create mode 100644 src/num.ts create mode 100644 test/bool.spec.ts create mode 100644 test/list.spec.ts create mode 100644 test/num.spec.ts create mode 100644 test/tsconfig.json create mode 100644 test/util.ts diff --git a/jest.config.js b/jest.config.js deleted file mode 100644 index 25daa6c..0000000 --- a/jest.config.js +++ /dev/null @@ -1,7 +0,0 @@ -module.exports = { - testEnvironment: "node", - transform: { - "^.+.tsx?$": ["ts-jest",{}], - }, - -}; diff --git a/jest.config.ts b/jest.config.ts new file mode 100644 index 0000000..9a411ce --- /dev/null +++ b/jest.config.ts @@ -0,0 +1,9 @@ +import type { Config } from 'jest'; + +export default { + testEnvironment: "node", + transform: { + "^.+.tsx?$": ["ts-jest", { tsconfig: 'test/tsconfig.json' }], + }, + setupFilesAfterEnv: ["./test/util.ts"], +} as Config; diff --git a/package.json b/package.json index 008d691..730401b 100644 --- a/package.json +++ b/package.json @@ -14,6 +14,7 @@ "@jest/globals": "^29.7.0", "jest": "^29.7.0", "ts-jest": "^29.2.3", + "ts-node": "^10.9.2", "typescript": "^5.5.4" } } diff --git a/src/bool.ts b/src/bool.ts new file mode 100644 index 0000000..fb448da --- /dev/null +++ b/src/bool.ts @@ -0,0 +1,30 @@ +import { Lazy, _ } from './core'; + +export type Bool = (t: Lazy) => (f: Lazy) => Lazy; + +export const True: Lazy + = _(t => _ => t) as Lazy; + +export const False: Lazy + = _(_ => f => f); + +export const iif: (b: Lazy) => (t: Lazy) => (f: Lazy) => Lazy + = b => t => f => b.then(b => b(t)(f)); + +export const not: (b: Lazy) => Lazy + = b => b.then(b => b(False)(True)); + +export const and: (l: Lazy) => (r: Lazy) => Lazy + = l => r => l.then(l => l(r)(False)); + +export const or: (l: Lazy) => (r: Lazy) => Lazy + = l => r => l.then(l => l(True)(r)); + +export const xor: (l: Lazy) => (r: Lazy) => Lazy + = l => r => l.then(l => l(not(r))(r)); + +export const fromBoolean: (b: boolean) => Lazy + = b => b ? True : False; + +export const toBoolean: (b: Lazy) => Lazy + = b => b.then(b => b(_(true))(_(false))); diff --git a/src/core.ts b/src/core.ts index b596076..fa98ca7 100644 --- a/src/core.ts +++ b/src/core.ts @@ -1 +1,44 @@ -export const id = (v: T) => v; +export class Lazy { + protected _value?: T; + + constructor(private _eval: () => T) { } + + public evaluated: boolean = false; + + public get value(): T { + if (this.evaluated) { + return this._value!; + } + this.evaluated = true; + return this._value = this._eval(); + } + + public then(f: (v: T) => Lazy): Lazy { + return new Lazy(() => f(this.value).value); + } +} + +type Choice = (t: Lazy) => (f: Lazy) => Lazy; + +export class LazyChoice extends Lazy { + public call(l: LazyChoice): (r: LazyChoice) => LazyChoice; + public call(l: Lazy): (r: Lazy) => Lazy; + public call(l: Lazy | LazyChoice): (r: Lazy | LazyChoice) => Lazy | LazyChoice { + return l instanceof LazyChoice + ? (r: LazyChoice) => new LazyChoice(() => this.value(l)(r).value) + : (r: Lazy) => this.then(c => c(l)(r)); + } +} + + +export const _ = (v: T): Lazy => new Lazy(() => v); +export const $ = (v: Lazy): T => v.value; + +export const undef: Lazy = new Lazy(() => { + throw new Error('undefined'); +}); + +export const id: (v: Lazy) => Lazy + = v => v; +export const cnst: (v: Lazy) => (_: Lazy) => Lazy + = v => _ => v; diff --git a/src/index.ts b/src/index.ts index e69de29..f140b65 100644 --- a/src/index.ts +++ b/src/index.ts @@ -0,0 +1,4 @@ +export * from './core'; +export * from './bool'; +export * from './num'; +export * from './list'; diff --git a/src/list.ts b/src/list.ts new file mode 100644 index 0000000..f59b200 --- /dev/null +++ b/src/list.ts @@ -0,0 +1,50 @@ +import { Lazy, _, cnst, id, undef } from './core'; +import { fromNumber, Num, toNumber } from './num'; +import { Bool, iif } from './bool'; + +export type List = (z: Lazy) => (f: (x: Lazy) => (xs: Lazy>) => Lazy) => Lazy; + +export const Nil: Lazy> + = _>(z => _ => z); + +export const Cons: (x: Lazy) => (xs: Lazy>) => Lazy> + = (x: Lazy) => (xs: Lazy>) => _>(_ => f => f(x)(xs)); + +export const cons: (x: Lazy) => (xs: Lazy>) => Lazy> + = Cons; + +export const head: (xs: Lazy>) => Lazy + = xs => xs.then(xs => xs(undef)(cnst)); + +export const tail: (xs: Lazy>) => Lazy> + = xs => xs.then(xs => xs(undef)(_ => id)); + +export const foldl: (f: (a: Lazy) => (x: Lazy) => Lazy) => (z: Lazy) => (xs: Lazy>) => Lazy + = f => z => xs => xs.then(xs => xs(z)(x => xs => foldl(f)(f(z)(x))(xs))); + +export const foldlz: (f: (a: Lazy) => (x: Lazy) => Lazy) => (xs: Lazy>) => Lazy + = f => xs => xs.then(xs => xs(undef)(foldl(f))); + +export const foldr: (f: (x: Lazy) => (a: Lazy) => Lazy) => (z: Lazy) => (xs: Lazy>) => Lazy + = f => z => xs => xs.then(xs => xs(z)(x => xs => f(x)(foldr(f)(z)(xs)))); + +export const foldrz: (f: (x: Lazy) => (a: Lazy) => Lazy) => (xs: Lazy>) => Lazy + = _ => _ => undef; + +export const map: (f: (x: Lazy) => Lazy) => (xs: Lazy>) => Lazy> + = f => xs => xs.then(xs => xs(Nil)(x => xs => Cons(f(x))(map(f)(xs)))); + +export const filter: (f: (x: Lazy) => Lazy) => (xs: Lazy>) => Lazy> + = (f: (x: Lazy) => Lazy) => (xs: Lazy>) => xs.then(xs => xs(Nil)(x => xs => iif>(f(x))(Cons(x)(filter(f)(xs)))(filter(f)(xs)))); + +export const fromArray: (xs: T[]) => Lazy> + = xs => xs.length === 0 ? Nil : new Lazy(() => Cons(_(xs[0]))(fromArray(xs.slice(1))).value); + +export const fromNumberArray: (xs: number[]) => Lazy> + = xs => xs.length === 0 ? Nil : new Lazy(() => Cons(fromNumber(xs[0]))(fromNumberArray(xs.slice(1))).value); + +export const toArray: (xs: Lazy>) => Lazy + = (xs: Lazy>) => xs.then(xs => xs(_([] as T[]))(x => xs => _([x.value, ...toArray(xs).value]))); + +export const toNumberArray: (xs: Lazy>) => Lazy + = xs => xs.then(xs => xs(_([] as number[]))(x => xs => _([toNumber(x).value, ...toNumberArray(xs).value]))); diff --git a/src/num.ts b/src/num.ts new file mode 100644 index 0000000..19d23ae --- /dev/null +++ b/src/num.ts @@ -0,0 +1,40 @@ +import { Bool, not, True } from './bool'; +import { Lazy, _, id } from './core'; + +export type Num = (z: Lazy) => (n: (p: Lazy) => Lazy) => Lazy; + +export const Zero: Lazy + = _(z => _ => z); + +export const Succ: (p: Lazy) => Lazy + = p => _(_ => n => n(p)); + +export const ifz: (n: Lazy) => (t: Lazy) => (f: Lazy) => Lazy + = n => t => f => n.then(n => n(t)(_ => f)); + +export const succ: (n: Lazy) => Lazy + = Succ; + +export const pred: (n: Lazy) => Lazy + = n => n.then(n => n(Zero)(id)); + +export const even: (n: Lazy) => Lazy + = n => n.then(n => n(True)(odd)); + +export const odd: (n: Lazy) => Lazy + = n => not(even(n)); + +export const sum: (l: Lazy) => (r: Lazy) => Lazy + = l => r => r.then(r => r(l)(sum(Succ(l)))); + +export const sub: (l: Lazy) => (r: Lazy) => Lazy + = l => r => r.then(r => r(l)(pr => l.then(l => l(Zero)(pl => sub(pl)(pr))))); + +export const mul: (l: Lazy) => (r: Lazy) => Lazy + = l => r => l.then(l => l(Zero)(pl => sum(r)(mul(pl)(r)))); + +export const fromNumber: (n: number) => Lazy + = n => !n ? Zero : new Lazy(() => Succ(fromNumber(n - 1)).value); + +export const toNumber: (n: Lazy) => Lazy + = n => n.then(n => n(_(0))(p => toNumber(p).then(p => _(1 + p)))); diff --git a/test/bool.spec.ts b/test/bool.spec.ts new file mode 100644 index 0000000..748286d --- /dev/null +++ b/test/bool.spec.ts @@ -0,0 +1,118 @@ +import { describe, expect, it } from '@jest/globals'; +import { _ } from '../src/core'; +import { True, False, iif, and, not, or, xor, toBoolean, fromBoolean } from '../src/bool'; + +describe('Bool', () => { + describe('toBoolean', () => { + it('converts True', () => { + expect(toBoolean).toDeferEvaluationOf(True); + expect(toBoolean(True)).toEvaluateTo(true); + }); + + it('converts False', () => { + expect(toBoolean).toDeferEvaluationOf(False); + expect(toBoolean(False)).toEvaluateTo(false); + }); + }); + + describe('fromBoolean', () => { + it('converts true', () => { + expect(toBoolean(fromBoolean(true))).toEvaluateTo(true); + }); + + it('converts false', () => { + expect(toBoolean(fromBoolean(false))).toEvaluateTo(false); + }); + }); + + describe('iif', () => { + it('returns true argument', () => { + expect(iif).toDeferEvaluationOf(False, expect.skipped(true), false); + expect(iif(True)(_(true))(_(false))).toEvaluateTo(true); + }); + + it('returns false argument', () => { + expect(iif).toDeferEvaluationOf(True, true, expect.skipped(false)); + expect(iif(False)(_(true))(_(false))).toEvaluateTo(false); + }); + }); + + describe('not', () => { + it('not False is True', () => { + expect(not).toDeferEvaluationOf(False); + expect(toBoolean(not(False))).toEvaluateTo(true); + + }); + + it('not True is False', () => { + expect(not).toDeferEvaluationOf(True); + expect(toBoolean(not(True))).toEvaluateTo(false); + }); + }); + + describe('and', () => { + it('False and False is False', () => { + expect(and).toDeferEvaluationOf(False, expect.skipped(False)); + expect(toBoolean(and(False)(False))).toEvaluateTo(false); + }); + + it('False and True is False', () => { + expect(and).toDeferEvaluationOf(False, expect.skipped(True)); + expect(toBoolean(and(False)(True))).toEvaluateTo(false); + }); + + it('True and False is False', () => { + expect(and).toDeferEvaluationOf(True, False); + expect(toBoolean(and(True)(False))).toEvaluateTo(false); + }); + + it('True and True is True', () => { + expect(and).toDeferEvaluationOf(True, True); + expect(toBoolean(and(True)(True))).toEvaluateTo(true); + }); + }); + + describe('or', () => { + it('False or False is False', () => { + expect(or).toDeferEvaluationOf(False, False); + expect(toBoolean(or(False)(False))).toEvaluateTo(false); + }); + + it('False or True is True', () => { + expect(or).toDeferEvaluationOf(False, True); + expect(toBoolean(or(False)(True))).toEvaluateTo(true); + }); + + it('True or False is True', () => { + expect(or).toDeferEvaluationOf(True, expect.skipped(False)); + expect(toBoolean(or(True)(False))).toEvaluateTo(true); + }); + + it('True or True is True', () => { + expect(or).toDeferEvaluationOf(True, expect.skipped(True)); + expect(toBoolean(or(True)(True))).toEvaluateTo(true); + }); + }); + + describe('xor', () => { + it('False xor False is False', () => { + expect(xor).toDeferEvaluationOf(False, False); + expect(toBoolean(xor(False)(False))).toEvaluateTo(false); + }); + + it('False xor True is True', () => { + expect(xor).toDeferEvaluationOf(False, True); + expect(toBoolean(xor(False)(True))).toEvaluateTo(true); + }); + + it('True xor False is True', () => { + expect(xor).toDeferEvaluationOf(True, False); + expect(toBoolean(xor(True)(False))).toEvaluateTo(true); + }); + + it('True xor True is False', () => { + expect(xor).toDeferEvaluationOf(True, True); + expect(toBoolean(xor(True)(True))).toEvaluateTo(false); + }); + }); +}); diff --git a/test/core.spec.ts b/test/core.spec.ts index 484ea0c..6d6d1a2 100644 --- a/test/core.spec.ts +++ b/test/core.spec.ts @@ -1,10 +1,16 @@ -import { describe, expect, test } from '@jest/globals'; -import { id } from '../src/core'; +import { describe, expect, it } from '@jest/globals'; +import { _, id, cnst } from '../src/core'; describe('id', () => { - test('returns same value', () => { - expect(id(null)).toBe(null); - expect(id(1)).toBe(1); - expect(id(global)).toBe(global); + it('returns first argument', () => { + expect(id).toDeferEvaluationOf(null); + expect(id(_(null))).toEvaluateTo(null); + }); +}); + +describe('cnst', () => { + it('returns first argument after receiving second', () => { + expect(cnst).toDeferEvaluationOf(null, expect.skipped(null)); + expect(cnst(_(null))(_('whatever'))).toEvaluateTo(null); }); }); diff --git a/test/list.spec.ts b/test/list.spec.ts new file mode 100644 index 0000000..1ec5ba1 --- /dev/null +++ b/test/list.spec.ts @@ -0,0 +1,211 @@ +import { describe, expect, it } from '@jest/globals'; +import { $, _, Lazy } from '../src/core'; +import { fromBoolean, iif } from '../src/bool'; +import { toNumber, Zero, sum, succ, Num, Succ, odd } from '../src/num'; +import { Cons, filter, foldl, foldlz, foldr, foldrz, fromArray, fromNumberArray, head, List, map, Nil, tail, toArray, toNumberArray } from '../src/list'; + +describe('List', () => { + const iterate: (n: number) => Lazy> = n => new Lazy(() => Cons(_(n))(iterate(n + 1)).value); + const niterate: (n: Lazy) => Lazy> = n => new Lazy(() => Cons(n)(niterate(Succ(n))).value); + + describe('toArray', () => { + it('converts []', () => { + expect(toArray).toDeferEvaluationOf(Nil); + expect(toArray(Nil)).toEvaluateTo([]); + }); + + it('converts [0]', () => { + expect(toArray).toDeferEvaluationOf(Cons(_(0))(Nil)); + expect(toArray(Cons(_(0))(Nil))).toEvaluateTo([0]); + }); + + it('converts [0, 1]', () => { + expect(toArray).toDeferEvaluationOf(Cons(_(0))(Cons(_(1))(Nil))); + expect(toArray(Cons(_(0))(Cons(_(1))(Nil)))).toEvaluateTo([0, 1]); + }); + }); + + describe('fromArray', () => { + it('converts []', () => { + expect(toArray(fromArray([]))).toEvaluateTo([]); + }); + + it('converts [0]', () => { + expect(toArray(fromArray([0]))).toEvaluateTo([0]); + }); + + it('converts 2', () => { + expect(toArray(fromArray([0, 1]))).toEvaluateTo([0, 1]); + }); + }); + + describe('head', () => { + it('returns nothing from empty list', () => { + expect(() => head(Nil)).not.toThrow(); + expect(() => $(head(Nil))).toThrow(); + }); + + it('returns only item', () => { + expect(head).toDeferEvaluationOf(Cons(_(0))(Nil)); + expect(head(Cons(_(0))(Nil))).toEvaluateTo(0); + }); + + it('returns first item', () => { + expect(head).toDeferEvaluationOf(Cons(_(0))(Cons(_(1))(Nil))); + expect(head(Cons(_(0))(Cons(_(1))(Nil)))).toEvaluateTo(0); + }); + + it('handles infinite lists', () => { + expect(head(iterate(0))).toEvaluateTo(0); + }); + }); + + describe('tail', () => { + it('drops nothing from empty list', () => { + expect(() => tail(Nil)).not.toThrow(); + expect(() => $(toArray(tail(Nil)))).toThrow(); + }); + + it('drops only item', () => { + expect(tail).toDeferEvaluationOf(Cons(_(0))(Nil)); + expect(toArray(tail(Cons(_(0))(Nil)))).toEvaluateTo([]); + }); + + it('drops first item', () => { + expect(tail).toDeferEvaluationOf(Cons(_(0))(Cons(_(1))(Nil))); + expect(toArray(tail(Cons(_(0))(Cons(_(1))(Nil))))).toEvaluateTo([1]); + }); + + it('handles infinite lists', () => { + expect(head(tail(iterate(0)))).toEvaluateTo(1); + }); + }); + + describe('foldl', () => { + it('folds empty list', () => { + expect(foldl(sum)).toDeferEvaluationOf(Zero, fromNumberArray([])); + expect(toNumber(foldl(sum)(Zero)(fromNumberArray([])))).toEvaluateTo(0); + }); + + it('folds single item', () => { + expect(foldl(sum)).toDeferEvaluationOf(Zero, fromNumberArray([1])); + expect(toNumber(foldl(sum)(Zero)(fromNumberArray([1])))).toEvaluateTo(1); + }); + + it('folds several items', () => { + expect(foldl(sum)).toDeferEvaluationOf(Zero, fromNumberArray([1, 2])); + expect(toNumber(foldl(sum)(Zero)(fromNumberArray([1, 2])))).toEvaluateTo(3); + }); + }); + + describe('foldlz', () => { + it('does not fold empty list', () => { + expect(() => foldlz(sum)(Nil)).not.toThrow(); + expect(() => $(toNumber(foldlz(sum)(Nil)))).toThrow(); + }); + + it('folds single item', () => { + expect(foldlz(sum)).toDeferEvaluationOf(fromNumberArray([1])); + expect(toNumber(foldlz(sum)(fromNumberArray([1])))).toEvaluateTo(1); + }); + + it('folds several items', () => { + expect(foldlz(sum)).toDeferEvaluationOf(fromNumberArray([1, 2])); + expect(toNumber(foldlz(sum)(fromNumberArray([1, 2])))).toEvaluateTo(3); + }); + }); + + describe('foldr', () => { + it('folds empty list', () => { + expect(foldr(sum)).toDeferEvaluationOf(Zero, fromNumberArray([])); + expect(toNumber(foldr(sum)(Zero)(fromNumberArray([])))).toEvaluateTo(0); + }); + + it('folds single item', () => { + expect(foldr(sum)).toDeferEvaluationOf(Zero, fromNumberArray([1])); + expect(toNumber(foldr(sum)(Zero)(fromNumberArray([1])))).toEvaluateTo(1); + }); + + it('folds several items', () => { + expect(foldr(sum)).toDeferEvaluationOf(Zero, fromNumberArray([1, 2])); + expect(toNumber(foldr(sum)(Zero)(fromNumberArray([1, 2])))).toEvaluateTo(3); + }); + + it('handles infinite lists', () => { + expect(foldr(x => a => + iif + (x.then(x => fromBoolean(x === 0))) + (x) + (x.then(x => a.then(a => _(x + a)))) + )(_(0))(iterate(-5))).toEvaluateTo(-15); + }); + }); + + describe('foldrz', () => { + it('does not fold empty list', () => { + expect(() => foldrz(sum)(Nil)).not.toThrow(); + expect(() => $(toNumber(foldrz(sum)(Nil)))).toThrow(); + }); + + it.failing('folds single item', () => { + expect(foldrz(sum)).toDeferEvaluationOf(fromNumberArray([1])); + expect(toNumber(foldrz(sum)(fromNumberArray([1])))).toEvaluateTo(1); + }); + + it.failing('folds several items', () => { + expect(foldrz(sum)).toDeferEvaluationOf(fromNumberArray([1, 2])); + expect(toNumber(foldrz(sum)(fromNumberArray([1, 2])))).toEvaluateTo(3); + }); + + it.failing('handles infinite lists', () => { + expect(foldrz(x => a => + iif + (x.then(x => fromBoolean(x === 0))) + (x) + (x.then(x => a.then(a => _(x + a)))) + )(iterate(-5))).toEvaluateTo(-10); + }); + }); + + describe('map', () => { + it('maps empty list', () => { + expect(map(succ)).toDeferEvaluationOf(fromNumberArray([])); + expect(toNumberArray(map(succ)(fromNumberArray([])))).toEvaluateTo([]); + }); + + it('maps only item', () => { + expect(map(succ)).toDeferEvaluationOf(fromNumberArray([0])); + expect(toNumberArray(map(succ)(fromNumberArray([0])))).toEvaluateTo([1]); + }); + + it('maps several items', () => { + expect(map(succ)).toDeferEvaluationOf(fromNumberArray([0, 1])); + expect(toNumberArray(map(succ)(fromNumberArray([0, 1])))).toEvaluateTo([1, 2]); + }); + + it('handles infinite lists', () => { + expect(toNumber(head(map(succ)(niterate(Zero))))).toEvaluateTo(1); + }); + }); + + describe('filter', () => { + it('filter empty list', () => { + expect(filter(odd)).toDeferEvaluationOf(fromNumberArray([])); + expect(toNumberArray(filter(odd)(fromNumberArray([])))).toEvaluateTo([]); + }); + + it('filters only item', () => { + expect(filter(odd)).toDeferEvaluationOf(fromNumberArray([0])); + expect(toNumberArray(filter(odd)(fromNumberArray([])))).toEvaluateTo([]); + }); + + it('filters several items', () => { + expect(filter(odd)).toDeferEvaluationOf(fromNumberArray([0, 1])); + expect(toNumberArray(filter(odd)(fromNumberArray([0, 1])))).toEvaluateTo([1]); + }); + + it('handles infinite lists', () => { + expect(toNumber(head(filter(odd)(niterate(Zero))))).toEvaluateTo(1); + }); + }); +}); diff --git a/test/num.spec.ts b/test/num.spec.ts new file mode 100644 index 0000000..3670630 --- /dev/null +++ b/test/num.spec.ts @@ -0,0 +1,169 @@ +import { describe, expect, it } from '@jest/globals'; +import { _, Lazy } from '../src/core'; +import { Num, Zero, Succ, sum, succ, pred, ifz, fromNumber, toNumber, sub, mul } from '../src/num'; + +describe('Num', () => { + describe('toNumber', () => { + it('converts 0', () => { + expect(toNumber).toDeferEvaluationOf(Zero); + expect(toNumber(Zero)).toEvaluateTo(0); + }); + + it('converts 1', () => { + expect(toNumber).toDeferEvaluationOf(Succ(Zero)); + expect(toNumber(Succ(Zero))).toEvaluateTo(1); + }); + + it('converts 2', () => { + expect(toNumber).toDeferEvaluationOf(Succ(Succ(Zero))); + expect(toNumber(Succ(Succ(Zero)))).toEvaluateTo(2); + }); + }); + + describe('fromNumber', () => { + it('converts 0', () => { + expect(toNumber(fromNumber(0))).toEvaluateTo(0); + }); + + it('converts 1', () => { + expect(toNumber(fromNumber(1))).toEvaluateTo(1); + }); + + it('converts 2', () => { + expect(toNumber(fromNumber(2))).toEvaluateTo(2); + }); + }); + + describe('ifz', () => { + it('returns zero branch', () => { + expect(ifz).toDeferEvaluationOf(fromNumber(0), true, false); + expect(ifz(fromNumber(0))(_(true))(_(false))).toEvaluateTo(true); + }); + + it('returns non-zero branch', () => { + expect(ifz).toDeferEvaluationOf(fromNumber(1), true, false); + expect(ifz(fromNumber(1))(_(true))(_(false))).toEvaluateTo(false); + }); + + it('returns non-zero branch', () => { + expect(ifz).toDeferEvaluationOf(fromNumber(100), true, false); + expect(ifz(fromNumber(10))(_(true))(_(false))).toEvaluateTo(false); + }); + + it('deeply lazy', () => { + const alot: Lazy = fromNumber(1000000); + expect(ifz(Succ(alot))(_(true))(_(false))).toEvaluateTo(false); + expect(alot.evaluated).toBe(false); + }); + }); + + describe('succ', () => { + it('succ 0 is 1', () => { + expect(succ).toDeferEvaluationOf(fromNumber(0)); + expect(toNumber(succ(fromNumber(0)))).toEvaluateTo(1); + }); + + it('succ 1 is 2', () => { + expect(succ).toDeferEvaluationOf(fromNumber(0)); + expect(toNumber(succ(fromNumber(1)))).toEvaluateTo(2); + }); + + it('succ 10 is 11', () => { + expect(succ).toDeferEvaluationOf(fromNumber(10)); + expect(toNumber(succ(fromNumber(10)))).toEvaluateTo(11); + }); + }); + + describe('pred', () => { + it('pred 0 is 0', () => { + expect(pred).toDeferEvaluationOf(fromNumber(0)); + expect(toNumber(pred(fromNumber(0)))).toEvaluateTo(0); + }); + + it('pred 1 is 0', () => { + expect(pred).toDeferEvaluationOf(fromNumber(0)); + expect(toNumber(pred(fromNumber(1)))).toEvaluateTo(0); + }); + + it('pred 10 is 9', () => { + expect(pred).toDeferEvaluationOf(fromNumber(10)); + expect(toNumber(pred(fromNumber(10)))).toEvaluateTo(9); + }); + }); + + describe('sum', () => { + it('0 sum 0 is 0', () => { + expect(sum).toDeferEvaluationOf(fromNumber(0), fromNumber(0)); + expect(toNumber(sum(fromNumber(0))(fromNumber(0)))).toEvaluateTo(0); + }); + + it('0 sum 1 is 1', () => { + expect(sum).toDeferEvaluationOf(fromNumber(0), fromNumber(1)); + expect(toNumber(sum(fromNumber(0))(fromNumber(1)))).toEvaluateTo(1); + }); + + it('1 sum 0 is 1', () => { + expect(sum).toDeferEvaluationOf(fromNumber(1), fromNumber(0)); + expect(toNumber(sum(fromNumber(1))(fromNumber(0)))).toEvaluateTo(1); + }); + + it('3 sum 4 is 7', () => { + expect(sum).toDeferEvaluationOf(fromNumber(3), fromNumber(4)); + expect(toNumber(sum(fromNumber(3))(fromNumber(4)))).toEvaluateTo(7); + }); + }); + + describe('sub', () => { + it('0 sub 0 is 0', () => { + expect(sub).toDeferEvaluationOf(fromNumber(0), fromNumber(0)); + expect(toNumber(sub(fromNumber(0))(fromNumber(0)))).toEvaluateTo(0); + }); + + it('0 sub 1 is 0', () => { + expect(sub).toDeferEvaluationOf(fromNumber(0), fromNumber(1)); + expect(toNumber(sub(fromNumber(0))(fromNumber(1)))).toEvaluateTo(0); + }); + + it('1 sub 0 is 1', () => { + expect(sub).toDeferEvaluationOf(fromNumber(1), fromNumber(0)); + expect(toNumber(sub(fromNumber(1))(fromNumber(0)))).toEvaluateTo(1); + }); + + it('7 sub 4 is 3', () => { + expect(sub).toDeferEvaluationOf(fromNumber(7), fromNumber(4)); + expect(toNumber(sub(fromNumber(7))(fromNumber(4)))).toEvaluateTo(3); + }); + }); + + describe('mul', () => { + it('0 mul 0 is 0', () => { + expect(mul).toDeferEvaluationOf(fromNumber(0), fromNumber(0)); + expect(toNumber(mul(fromNumber(0))(fromNumber(0)))).toEvaluateTo(0); + }); + + it('0 mul 10 is 0', () => { + expect(mul).toDeferEvaluationOf(fromNumber(0), expect.skipped(fromNumber(10))); + expect(toNumber(mul(fromNumber(0))(fromNumber(10)))).toEvaluateTo(0); + }); + + it('10 mul 0 is 0', () => { + expect(mul).toDeferEvaluationOf(fromNumber(10), fromNumber(0)); + expect(toNumber(mul(fromNumber(1))(fromNumber(0)))).toEvaluateTo(0); + }); + + it('1 mul 10 is 10', () => { + expect(mul).toDeferEvaluationOf(fromNumber(1), fromNumber(10)); + expect(toNumber(mul(fromNumber(1))(fromNumber(10)))).toEvaluateTo(10); + }); + + it('10 mul 1 is 10', () => { + expect(mul).toDeferEvaluationOf(fromNumber(10), fromNumber(1)); + expect(toNumber(mul(fromNumber(10))(fromNumber(1)))).toEvaluateTo(10); + }); + + it('3 mul 4 is 12', () => { + expect(mul).toDeferEvaluationOf(fromNumber(3), fromNumber(4)); + expect(toNumber(mul(fromNumber(3))(fromNumber(4)))).toEvaluateTo(12); + }); + }); +}); diff --git a/test/tsconfig.json b/test/tsconfig.json new file mode 100644 index 0000000..7c1c8cc --- /dev/null +++ b/test/tsconfig.json @@ -0,0 +1,6 @@ +{ + "extends": "../tsconfig.json", + "include": [ + "**/*.ts", + ], +} diff --git a/test/util.ts b/test/util.ts new file mode 100644 index 0000000..efce4d8 --- /dev/null +++ b/test/util.ts @@ -0,0 +1,96 @@ +import { expect } from '@jest/globals'; +import type { MatcherFunction } from 'expect'; +import { $, _, Lazy } from '../src/core'; + +class Deferrable { + constructor( + public readonly when: 'always' | 'deferred' | 'never', + public readonly what: Lazy) { } +} + +const toEvaluateTo: MatcherFunction<[expected: unknown]> = + function (actual, expected) { + if (!(actual instanceof Lazy)) { + throw new TypeError('Actual must be of type Lazy!'); + } + if (this.equals($(actual), expected)) { + return { + message: () => `expected ${this.utils.printReceived($(actual))} not to evaluate to ${this.utils.printExpected(expected)}`, + pass: true, + }; + } else { + return { + message: () => `expected ${this.utils.printReceived($(actual))} to evaluate to ${this.utils.printExpected(expected)}`, + pass: false, + }; + } + }; + +const toDeferEvaluationOf: MatcherFunction<(unknown | Deferrable)[]> = + function (actual, ...args) { + if (typeof actual !== 'function') { + throw new TypeError('Actual must be of type function!'); + } + if (this.isNot) { + throw new TypeError('`toDeferEvaluationOf` can not be `not`ted, use `always`/`deferred`/`never` on arguments!'); + } + const largs: Deferrable[] = args.map(v => + v instanceof Deferrable ? v + : v instanceof Lazy ? new Deferrable('deferred', _(v.value)) + : new Deferrable('deferred', _(v))); + const cargs: Deferrable[] = []; + let result = actual; + for (const la of largs) { + result = result(la.what); + cargs.push(la); + const mismatchingArg: Deferrable | undefined = cargs.find(v => v.when === 'always' ? !v.what.evaluated : v.what.evaluated); + if (mismatchingArg) { + return { + message: () => + mismatchingArg.when === 'always' + ? `expected ${this.utils.printExpected(actual.name)} to evaluate ${this.utils.printReceived(`${largs.indexOf(mismatchingArg) + 1}th argument`)} immediately` + : `expected ${this.utils.printExpected(actual.name)} not to evaluate ${this.utils.printReceived(`${largs.indexOf(mismatchingArg) + 1}th argument`)} immediately`, + pass: false, + }; + } + } + if (!(result instanceof Lazy)) { + throw new TypeError('Actual did not resolve to value, more arguments expected.'); + } + result.value; + const mismatchingArg: Deferrable | undefined = cargs.find(v => v.when === 'never' && v.what.evaluated); + if (mismatchingArg) { + return { + message: () => `expected ${this.utils.printExpected(actual.name)} not to evaluate ${this.utils.printReceived(`${largs.indexOf(mismatchingArg) + 1}th argument`)}`, + pass: false, + }; + } + + return { + message: () => `expected ${this.utils.printExpected('toDeferEvaluationOf')} to ${this.utils.printReceived('fail')}`, + pass: true, + }; + }; + +expect.extend({ + toEvaluateTo, + toDeferEvaluationOf, +}); + +expect.evaluated = (v) => new Deferrable('always', _(v)); +expect.deferred = (v) => new Deferrable('deferred', _(v)); +expect.skipped = (v) => new Deferrable('never', _(v)); + +declare module 'expect' { + interface AsymmetricMatchers { + evaluated(value: T): Deferrable; + deferred(value: T): Deferrable; + skipped(value: T): Deferrable; + toEvaluateTo(value: any): void; + toDeferEvaluationOf(...args: any[]): void; + } + interface Matchers { + toEvaluateTo(value: any): R; + toDeferEvaluationOf(...args: any[]): R; + } +} diff --git a/tsconfig.json b/tsconfig.json index 9c93f5c..f281384 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,9 +1,11 @@ { "compilerOptions": { + "baseUrl": ".", "outDir": "build", "target": "ES2022", "module": "ES2022", "moduleResolution": "Node", + "esModuleInterop": true, "lib": [ "ES2022" ], diff --git a/yarn.lock b/yarn.lock index 7ee046b..5939a11 100644 --- a/yarn.lock +++ b/yarn.lock @@ -301,6 +301,13 @@ resolved "https://registry.yarnpkg.com/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz#75a2e8b51cb758a7553d6804a5932d7aace75c39" integrity sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw== +"@cspotcode/source-map-support@^0.8.0": + version "0.8.1" + resolved "https://registry.yarnpkg.com/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz#00629c35a688e05a88b1cda684fb9d5e73f000a1" + integrity sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw== + dependencies: + "@jridgewell/trace-mapping" "0.3.9" + "@istanbuljs/load-nyc-config@^1.0.0": version "1.1.0" resolved "https://registry.yarnpkg.com/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz#fd3db1d59ecf7cf121e80650bb86712f9b55eced" @@ -518,7 +525,7 @@ "@jridgewell/sourcemap-codec" "^1.4.10" "@jridgewell/trace-mapping" "^0.3.24" -"@jridgewell/resolve-uri@^3.1.0": +"@jridgewell/resolve-uri@^3.0.3", "@jridgewell/resolve-uri@^3.1.0": version "3.1.2" resolved "https://registry.yarnpkg.com/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz#7a0ee601f60f99a20c7c7c5ff0c80388c1189bd6" integrity sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw== @@ -533,6 +540,14 @@ resolved "https://registry.yarnpkg.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz#3188bcb273a414b0d215fd22a58540b989b9409a" integrity sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ== +"@jridgewell/trace-mapping@0.3.9": + version "0.3.9" + resolved "https://registry.yarnpkg.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz#6534fd5933a53ba7cbf3a17615e273a0d1273ff9" + integrity sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ== + dependencies: + "@jridgewell/resolve-uri" "^3.0.3" + "@jridgewell/sourcemap-codec" "^1.4.10" + "@jridgewell/trace-mapping@^0.3.12", "@jridgewell/trace-mapping@^0.3.18", "@jridgewell/trace-mapping@^0.3.24", "@jridgewell/trace-mapping@^0.3.25": version "0.3.25" resolved "https://registry.yarnpkg.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz#15f190e98895f3fc23276ee14bc76b675c2e50f0" @@ -560,6 +575,26 @@ dependencies: "@sinonjs/commons" "^3.0.0" +"@tsconfig/node10@^1.0.7": + version "1.0.11" + resolved "https://registry.yarnpkg.com/@tsconfig/node10/-/node10-1.0.11.tgz#6ee46400685f130e278128c7b38b7e031ff5b2f2" + integrity sha512-DcRjDCujK/kCk/cUe8Xz8ZSpm8mS3mNNpta+jGCA6USEDfktlNvm1+IuZ9eTcDbNk41BHwpHHeW+N1lKCz4zOw== + +"@tsconfig/node12@^1.0.7": + version "1.0.11" + resolved "https://registry.yarnpkg.com/@tsconfig/node12/-/node12-1.0.11.tgz#ee3def1f27d9ed66dac6e46a295cffb0152e058d" + integrity sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag== + +"@tsconfig/node14@^1.0.0": + version "1.0.3" + resolved "https://registry.yarnpkg.com/@tsconfig/node14/-/node14-1.0.3.tgz#e4386316284f00b98435bf40f72f75a09dabf6c1" + integrity sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow== + +"@tsconfig/node16@^1.0.2": + version "1.0.4" + resolved "https://registry.yarnpkg.com/@tsconfig/node16/-/node16-1.0.4.tgz#0b92dcc0cc1c81f6f306a381f28e31b1a56536e9" + integrity sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA== + "@types/babel__core@^7.1.14": version "7.20.5" resolved "https://registry.yarnpkg.com/@types/babel__core/-/babel__core-7.20.5.tgz#3df15f27ba85319caa07ba08d0721889bb39c017" @@ -643,6 +678,18 @@ dependencies: "@types/yargs-parser" "*" +acorn-walk@^8.1.1: + version "8.3.3" + resolved "https://registry.yarnpkg.com/acorn-walk/-/acorn-walk-8.3.3.tgz#9caeac29eefaa0c41e3d4c65137de4d6f34df43e" + integrity sha512-MxXdReSRhGO7VlFe1bRG/oI7/mdLV9B9JJT0N8vZOhF7gFRR5l3M8W9G8JxmKV+JC5mGqJ0QvqfSOLsCPa4nUw== + dependencies: + acorn "^8.11.0" + +acorn@^8.11.0, acorn@^8.4.1: + version "8.12.1" + resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.12.1.tgz#71616bdccbe25e27a54439e0046e89ca76df2248" + integrity sha512-tcpGyI9zbizT9JbV6oYE477V6mTlXvvi0T0G3SNIYE2apm/G5huBa1+K89VGeovbg+jycCrfhl3ADxErOuO6Jg== + ansi-escapes@^4.2.1: version "4.3.2" resolved "https://registry.yarnpkg.com/ansi-escapes/-/ansi-escapes-4.3.2.tgz#6b2291d1db7d98b6521d5f1efa42d0f3a9feb65e" @@ -682,6 +729,11 @@ anymatch@^3.0.3: normalize-path "^3.0.0" picomatch "^2.0.4" +arg@^4.1.0: + version "4.1.3" + resolved "https://registry.yarnpkg.com/arg/-/arg-4.1.3.tgz#269fc7ad5b8e42cb63c896d5666017261c144089" + integrity sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA== + argparse@^1.0.7: version "1.0.10" resolved "https://registry.yarnpkg.com/argparse/-/argparse-1.0.10.tgz#bcd6791ea5ae09725e17e5ad988134cd40b3d911" @@ -928,6 +980,11 @@ create-jest@^29.7.0: jest-util "^29.7.0" prompts "^2.0.1" +create-require@^1.1.0: + version "1.1.1" + resolved "https://registry.yarnpkg.com/create-require/-/create-require-1.1.1.tgz#c1d7e8f1e5f6cfc9ff65f9cd352d37348756c333" + integrity sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ== + cross-spawn@^7.0.3: version "7.0.3" resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.3.tgz#f73a85b9d5d41d045551c177e2882d4ac85728a6" @@ -964,6 +1021,11 @@ diff-sequences@^29.6.3: resolved "https://registry.yarnpkg.com/diff-sequences/-/diff-sequences-29.6.3.tgz#4deaf894d11407c51efc8418012f9e70b84ea921" integrity sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q== +diff@^4.0.1: + version "4.0.2" + resolved "https://registry.yarnpkg.com/diff/-/diff-4.0.2.tgz#60f3aecb89d5fae520c11aa19efc2bb982aade7d" + integrity sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A== + ejs@^3.1.10: version "3.1.10" resolved "https://registry.yarnpkg.com/ejs/-/ejs-3.1.10.tgz#69ab8358b14e896f80cc39e62087b88500c3ac3b" @@ -1715,7 +1777,7 @@ make-dir@^4.0.0: dependencies: semver "^7.5.3" -make-error@1.x: +make-error@1.x, make-error@^1.1.1: version "1.3.6" resolved "https://registry.yarnpkg.com/make-error/-/make-error-1.3.6.tgz#2eb2e37ea9b67c4891f684a1394799af484cf7a2" integrity sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw== @@ -2109,6 +2171,25 @@ ts-jest@^29.2.3: semver "^7.5.3" yargs-parser "^21.0.1" +ts-node@^10.9.2: + version "10.9.2" + resolved "https://registry.yarnpkg.com/ts-node/-/ts-node-10.9.2.tgz#70f021c9e185bccdca820e26dc413805c101c71f" + integrity sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ== + dependencies: + "@cspotcode/source-map-support" "^0.8.0" + "@tsconfig/node10" "^1.0.7" + "@tsconfig/node12" "^1.0.7" + "@tsconfig/node14" "^1.0.0" + "@tsconfig/node16" "^1.0.2" + acorn "^8.4.1" + acorn-walk "^8.1.1" + arg "^4.1.0" + create-require "^1.1.0" + diff "^4.0.1" + make-error "^1.1.1" + v8-compile-cache-lib "^3.0.1" + yn "3.1.1" + type-detect@4.0.8: version "4.0.8" resolved "https://registry.yarnpkg.com/type-detect/-/type-detect-4.0.8.tgz#7646fb5f18871cfbb7749e69bd39a6388eb7450c" @@ -2137,6 +2218,11 @@ update-browserslist-db@^1.1.0: escalade "^3.1.2" picocolors "^1.0.1" +v8-compile-cache-lib@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz#6336e8d71965cb3d35a1bbb7868445a7c05264bf" + integrity sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg== + v8-to-istanbul@^9.0.1: version "9.3.0" resolved "https://registry.yarnpkg.com/v8-to-istanbul/-/v8-to-istanbul-9.3.0.tgz#b9572abfa62bd556c16d75fdebc1a411d5ff3175" @@ -2210,6 +2296,11 @@ yargs@^17.3.1: y18n "^5.0.5" yargs-parser "^21.1.1" +yn@3.1.1: + version "3.1.1" + resolved "https://registry.yarnpkg.com/yn/-/yn-3.1.1.tgz#1e87401a09d767c1d5eab26a6e4c185182d2eb50" + integrity sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q== + yocto-queue@^0.1.0: version "0.1.0" resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-0.1.0.tgz#0294eb3dee05028d31ee1a5fa2c556a6aaf10a1b"