feat: Add generic types and Complex numbers (#21)
All checks were successful
/ test (push) Successful in 18s
All checks were successful
/ test (push) Successful in 18s
Generic types can be called with argument(s) to produce a new type object, and if all types supplied as arguments are concrete, then the result will be a concrete type. The test of a generic type must determine if the entity is an instance of any specialization of the type; and it must also have a `refine` method that takes such an instance and returns its fully-specialized concrete type. It must also have a method `specializesTo` that takes a concrete type and returns whether that concrete type is a specialization of this generic type. This commit also defines a generic Complex number type, that can have any type as its Component type (including another Complex number, to create e.g. quaternions), and defines the conversion/constructor function `complex`. Reviewed-on: #21 Co-authored-by: Glen Whitney <glen@studioinfinity.org> Co-committed-by: Glen Whitney <glen@studioinfinity.org>
This commit is contained in:
parent
70ce01d12b
commit
491e207fad
11 changed files with 224 additions and 7 deletions
75
src/complex/Complex.js
Normal file
75
src/complex/Complex.js
Normal 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})`)
|
||||
}
|
||||
})
|
41
src/complex/__test__/Complex.spec.js
Normal file
41
src/complex/__test__/Complex.spec.js
Normal 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)))
|
||||
})
|
||||
})
|
12
src/complex/__test__/complex.spec.js
Normal file
12
src/complex/__test__/complex.spec.js
Normal 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
2
src/complex/all.js
Normal file
|
@ -0,0 +1,2 @@
|
|||
export * as typeDefinition from './Complex.js'
|
||||
export * as type from './type.js'
|
28
src/complex/type.js
Normal file
28
src/complex/type.js
Normal 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}))
|
||||
})
|
||||
|
||||
|
|
@ -1,13 +1,62 @@
|
|||
export class Type {
|
||||
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()
|
||||
|
||||
// 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.name || `[Type ${this.test}]`
|
||||
return this.typeName || `[Type ${this.test}]`
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -23,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 {
|
||||
|
|
|
@ -84,7 +84,12 @@ export class TypeDispatcher {
|
|||
throw new TypeError(`attempt to merge unusable type '${val}'`)
|
||||
}
|
||||
this.types[key] = val
|
||||
val.name = 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
|
||||
}
|
||||
|
||||
|
|
|
@ -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])
|
||||
|
|
|
@ -3,6 +3,7 @@ import {TypeDispatcher} from '../TypeDispatcher.js'
|
|||
import * as booleans from '#boolean/all.js'
|
||||
import * as generics from '#generic/all.js'
|
||||
import * as numbers from '#number/all.js'
|
||||
import {NumberT} from '#number/NumberT.js'
|
||||
import {onType, ResolutionError} from "#core/helpers.js"
|
||||
import {Any} from "#core/TypePatterns.js"
|
||||
import {Returns, NotAType} from "#core/Type.js"
|
||||
|
@ -68,4 +69,7 @@ describe('TypeDispatcher', () => {
|
|||
const doomed = new TypeDispatcher()
|
||||
assert.throws(() => doomed.merge({NaT: NotAType}), TypeError)
|
||||
})
|
||||
it('supports generic types', () => {
|
||||
assert.throws(() => NumberT(0), TypeError)
|
||||
})
|
||||
})
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue