feat: define Complex type and conversions to it
All checks were successful
/ test (pull_request) Successful in 19s

This commit is contained in:
Glen Whitney 2025-04-21 11:31:38 -07:00
parent 6de6515d3c
commit 4ea0b38b6f
10 changed files with 215 additions and 11 deletions

75
src/complex/Complex.js Normal file
View file

@ -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})`)
}
})

View file

@ -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)))
})
})

View file

@ -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)
})
})

2
src/complex/all.js Normal file
View file

@ -0,0 +1,2 @@
export * as typeDefinition from './Complex.js'
export * as type from './type.js'

28
src/complex/type.js Normal file
View file

@ -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}))
})

View file

@ -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 {

View file

@ -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
}

View file

@ -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])

View file

@ -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

View file

@ -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
}