From 4ea0b38b6f98105a8e6b7d75dbc1fdcd2b922354 Mon Sep 17 00:00:00 2001 From: Glen Whitney Date: Mon, 21 Apr 2025 11:31:38 -0700 Subject: [PATCH] feat: define Complex type and conversions to it --- src/complex/Complex.js | 75 ++++++++++++++++++++++++++++ src/complex/__test__/Complex.spec.js | 41 +++++++++++++++ src/complex/__test__/complex.spec.js | 12 +++++ src/complex/all.js | 2 + src/complex/type.js | 28 +++++++++++ src/core/Type.js | 54 +++++++++++++++++--- src/core/TypeDispatcher.js | 7 ++- src/core/TypePatterns.js | 2 +- src/nanomath.js | 3 +- src/number/__test__/NumberT.spec.js | 2 +- 10 files changed, 215 insertions(+), 11 deletions(-) create mode 100644 src/complex/Complex.js create mode 100644 src/complex/__test__/Complex.spec.js create mode 100644 src/complex/__test__/complex.spec.js create mode 100644 src/complex/all.js create mode 100644 src/complex/type.js diff --git a/src/complex/Complex.js b/src/complex/Complex.js new file mode 100644 index 0000000..7292113 --- /dev/null +++ b/src/complex/Complex.js @@ -0,0 +1,75 @@ +import {Type} from '#core/Type.js' +import {onType} from '#core/helpers.js' + +const isComplex = z => z && typeof z === 'object' && 're' in z && 'im' in z + +const specializesTo = CType => CType.complex + +function complexSpecialize(ComponentType) { + const compTest = ComponentType.test + const specTest = z => isComplex(z) && compTest(z.re) && compTest(z.im) + const typeName = `Complex(${ComponentType})` + if (ComponentType.concrete) { + const fromSpec = [ + ComponentType, math => r => ({re: r, im: ComponentType.zero})] + for (const [matchType, fctry] of ComponentType.from.patterns) { + fromSpec.push(this.specialize(matchType.type), math => { + const compConv = fctry(math) + return z => ({re: compConv(z.re), im: compConv(z.im)}) + }) + } + const typeOptions = {from: onType(...fromSpec), typeName} + if ('zero' in ComponentType) { + typeOptions.zero = {re: ComponentType.zero, im: ComponentType.zero} + if ('one' in ComponentType) { + typeOptions.one = {re: ComponentType.one, im: ComponentType.zero} + } + } + if ('nan' in ComponentType) { + typeOptions.nan = {re: ComponentType.nan, im: ComponentType.nan} + } + const complexCompType = new Type(specTest, typeOptions) + complexCompType.Component = ComponentType + complexCompType.complex = true + return complexCompType + } + // wrapping a generic type in Complex + const cplx = this + + const innerSpecialize = (...args) => { + const innerType = ComponentType.specialize(...args) + return cplx.specialize(innerType) + } + + const innerSpecializesTo = CType => specializesTo(CType) + && ComponentType.specializesTo(CType.Component) + + const innerRefine = (z, typer) => { + const reType = ComponentType.refine(z.re, typer) + const imType = ComponentType.refine(z.im, typer) + if (reType === imType) return cplx.specialize(reType) + throw new TypeError( + 'mixed-type Complex numbers disallowed ' + + `(real: ${reType}, imaginary: ${imType})`) + } + + return new Type(specTest, { + specialize: innerSpecialize, + specializesTo: innerSpecializesTo, + refine: innerRefine, + typeName, + }) +} + +export const Complex = new Type(isComplex, { + specialize: complexSpecialize, + specializesTo, + refine: function(z, typer) { + const reType = typer(z.re) + const imType = typer(z.im) + if (reType === imType) return this.specialize(reType) + throw new TypeError( + 'mixed-type Complex numbers disallowed ' + + `(real: ${reType}, imaginary: ${imType})`) + } +}) diff --git a/src/complex/__test__/Complex.spec.js b/src/complex/__test__/Complex.spec.js new file mode 100644 index 0000000..ce232be --- /dev/null +++ b/src/complex/__test__/Complex.spec.js @@ -0,0 +1,41 @@ +import assert from 'assert' +import {Complex} from '../Complex.js' +import {BooleanT} from '#boolean/BooleanT.js' +import {NumberT} from '#number/NumberT.js' +import math from '#nanomath' + +describe('Complex Type', () => { + it('correctly recognizes complex numbers', () => { + assert(Complex.test({re: 3, im: 7})) + assert(!Complex.test(3)) + }) + it('can fully type complex numbers', () => { + assert.strictEqual(math.typeOf({re: 3, im: 7}), Complex(NumberT)) + assert.strictEqual(math.typeOf({re: true, im: false}), Complex(BooleanT)) + assert.strictEqual( + math.typeOf({re: {re: 1, im: 3}, im: {re: 0, im: -2}}), + Complex(Complex(NumberT))) + }) + it('can perform conversions to Complex types', () => { + const CplxNum = Complex(NumberT) + const convertImps = CplxNum.from + let cnvNtoCN + let cnvCBtoCN + for (const [pattern, convFactory] of convertImps.patterns) { + if (pattern.match([NumberT])[0] === 1) { + cnvNtoCN = convFactory(math) + } else if (pattern.match([Complex(BooleanT)])[0] === 1) { + cnvCBtoCN = convFactory(math) + } + } + assert.deepStrictEqual(cnvNtoCN(3.5), {re: 3.5, im: 0}) + assert.deepStrictEqual(cnvCBtoCN({re: false, im: true}), {re: 0, im: 1}) + }) + it('can refine when nested', () => { + const Quaternion = Complex(Complex) + const example = {re: {re: true, im: true}, im: {re: false, im: true}} + assert(Quaternion.test(example)) + const exampleType = Quaternion.refine(example, math.typeOf) + assert.strictEqual(exampleType, Complex(Complex(BooleanT))) + }) +}) diff --git a/src/complex/__test__/complex.spec.js b/src/complex/__test__/complex.spec.js new file mode 100644 index 0000000..a9d4725 --- /dev/null +++ b/src/complex/__test__/complex.spec.js @@ -0,0 +1,12 @@ +import assert from 'assert' +import math from '#nanomath' + +describe('complex type operations', () => { + it('converts to number', () => { + assert.deepStrictEqual(math.complex(3), {re: 3, im: 0}) + assert.deepStrictEqual(math.complex(NaN), {re: NaN, im: NaN}) + assert.deepStrictEqual(math.complex(3, -1), {re: 3, im: -1}) + assert.deepStrictEqual(math.complex(false, true), {re: false, im: true}) + assert.throws(() => math.complex(3, false), RangeError) + }) +}) diff --git a/src/complex/all.js b/src/complex/all.js new file mode 100644 index 0000000..9795516 --- /dev/null +++ b/src/complex/all.js @@ -0,0 +1,2 @@ +export * as typeDefinition from './Complex.js' +export * as type from './type.js' diff --git a/src/complex/type.js b/src/complex/type.js new file mode 100644 index 0000000..68531cb --- /dev/null +++ b/src/complex/type.js @@ -0,0 +1,28 @@ +import {Complex} from './Complex.js' +import {onType} from "#core/helpers.js" +import {Returns} from "#core/Type.js" +import {Any} from "#core/TypePatterns.js" + +export const complex = onType( + Any, (math, T) => { + const z = math.zero(T) + if (math.hasnan(T)) { + const isnan = math.isnan.resolve([T]) + const compnan = math.nan(Complex(T)) + return Returns(Complex(T), r => { + if (isnan(r)) return compnan + return {re: r, im: z} + }) + } + return Returns(Complex(T), r => ({re: r, im: z})) + }, + [Any, Any], (math, [T, U]) => { + if (T !== U) { + throw new RangeError( + 'mixed complex types disallowed ' + + `(real ${T}, imaginary ${U})`) + } + return Returns(Complex(T), (r, m) => ({re: r, im: m})) + }) + + diff --git a/src/core/Type.js b/src/core/Type.js index fec409f..62b048e 100644 --- a/src/core/Type.js +++ b/src/core/Type.js @@ -1,20 +1,60 @@ +import ArrayKeyedMap from 'array-keyed-map' + +// Generic types are callable, so we have no choice but to extend Function export class Type extends Function { constructor(f, options = {}) { super() - this.test = f - this.from = options.from ?? {patterns: []} // mock empty Implementations - if ('zero' in options) this.zero = options.zero - if ('one' in options) this.one = options.one - if ('nan' in options) this.nan = options.nan - return new Proxy(this, { + // The following proxy (to handle the call the way we want) is what we + // will actually return from the constructor. So make sure to only + // let the proxy out of this function, never `this`. + const rewired = new Proxy(this, { apply: (target, thisForCall, args) => { const callThrough = thisForCall ?? target if (callThrough.specialize) return callThrough.specialize(...args) throw new TypeError(`Type ${callThrough} is not generic`) }, + get: (target, prop, receiver) => { + if (prop === 'isAproxy') return true + return Reflect.get(target, prop, receiver) + } }) + + this.test = f + this.from = options.from ?? {patterns: []} // mock empty Implementations + if ('zero' in options) this.zero = options.zero + if ('one' in options) this.one = options.one + if ('nan' in options) this.nan = options.nan + if ('typeName' in options) this.typeName = options.typeName + if ('specialize' in options) { // this is a generic type + this._specializedTypes = new ArrayKeyedMap() + this.specialize = (...args) => { + if (!this._specializedTypes.has(args)) { + this._specializedTypes.set( + args, options.specialize.apply(rewired, args)) + } + return this._specializedTypes.get(args) + } + this.concrete = false + if (!options.specializesTo) { + throw new RangeError('All generic types must specify specializesTo') + } + this.specializesTo = options.specializesTo + if (!options.refine) { + throw new RangeError('All generic types must specify refine') + } + this.refine = options.refine + } else { // this is a concrete type + this.concrete = true + this.specializesTo = TypeT => { + return rewired === TypeT + } + this.refine = () => rewired + } + + return rewired } + toString() { return this.typeName || `[Type ${this.test}]` } @@ -32,7 +72,7 @@ export const Returns = (type, f) => (f.returns = type, f) export const whichType = typs => Returns(TypeOfTypes, item => { for (const type of Object.values(typs)) { if (!(type instanceof Type)) continue - if (type.test(item)) return type + if (type.test(item)) return type.refine(item, whichType(typs)) } let errorMsg = '' try { diff --git a/src/core/TypeDispatcher.js b/src/core/TypeDispatcher.js index ccfc3ad..6346b6f 100644 --- a/src/core/TypeDispatcher.js +++ b/src/core/TypeDispatcher.js @@ -84,7 +84,12 @@ export class TypeDispatcher { throw new TypeError(`attempt to merge unusable type '${val}'`) } this.types[key] = val - val.typeName = key + if ('typeName' in val) { + if (val.typeName !== key) { + throw new RangeError( + `Type ${val} cannot be merged under key ${key}`) + } + } else val.typeName = key continue } diff --git a/src/core/TypePatterns.js b/src/core/TypePatterns.js index efe05f8..a2e877f 100644 --- a/src/core/TypePatterns.js +++ b/src/core/TypePatterns.js @@ -19,7 +19,7 @@ class MatchTypePattern extends TypePattern { const position = options.position ?? 0 const actual = typeSequence[position] if (position < typeSequence.length) { - if (actual === this.type) return [position + 1, actual] + if (this.type.specializesTo(actual)) return [position + 1, actual] if (options.convert) { for (const [pattern, convertor] of this.type.from.patterns) { const [pos] = pattern.match([actual]) diff --git a/src/nanomath.js b/src/nanomath.js index 5d4d5be..c133cbf 100644 --- a/src/nanomath.js +++ b/src/nanomath.js @@ -2,8 +2,9 @@ import * as booleans from './boolean/all.js' import * as coretypes from './coretypes/all.js' import * as generics from './generic/all.js' import * as numbers from './number/all.js' +import * as complex from './complex/all.js' import {TypeDispatcher} from '#core/TypeDispatcher.js' -const math = new TypeDispatcher(booleans, coretypes, generics, numbers) +const math = new TypeDispatcher(booleans, coretypes, generics, numbers, complex) export default math diff --git a/src/number/__test__/NumberT.spec.js b/src/number/__test__/NumberT.spec.js index 008dcad..b028025 100644 --- a/src/number/__test__/NumberT.spec.js +++ b/src/number/__test__/NumberT.spec.js @@ -15,7 +15,7 @@ describe('NumberT Type', () => { const convertImps = NumberT.from let cnvBtoN for (const [pattern, convFactory] of convertImps.patterns) { - if (pattern.match([BooleanT])) { + if (pattern.match([BooleanT])[0] === 1) { cnvBtoN = convFactory(math) break }