diff --git a/.gitignore b/.gitignore index 0dc139f..df7c548 100644 --- a/.gitignore +++ b/.gitignore @@ -129,6 +129,9 @@ dist # Stores VSCode versions used for testing VSCode extensions .vscode-test +# Webstiorm +.idea + # yarn v2 .yarn/cache .yarn/unplugged diff --git a/src/Complex/all.ts b/src/Complex/all.ts index 9d417b8..3ff6311 100644 --- a/src/Complex/all.ts +++ b/src/Complex/all.ts @@ -1,8 +1,6 @@ -import {ForType} from '../core/Dispatcher.js' import * as Complex from './native.js' +import * as complex from './arithmetic.js' + +export { complex } export {Complex} - -declare module "../core/Dispatcher" { - interface ImplementationTypes extends ForType<'Complex', typeof Complex> {} -} diff --git a/src/Complex/arithmetic.ts b/src/Complex/arithmetic.ts new file mode 100644 index 0000000..a9d0d42 --- /dev/null +++ b/src/Complex/arithmetic.ts @@ -0,0 +1,129 @@ +import {Complex, complex_binary, FnComplexUnary} from './type.js' +import type { + FnAbsSquare, + FnAdd, + FnAddReal, + FnConj, FnConservativeSqrt, FnDivide, + FnDivideByReal, FnIsReal, FnIsSquare, + FnMultiply, FnNaN, FnRe, FnReciprocal, FnSqrt, + FnSubtract, + FnUnaryMinus, FnZero +} from '../interfaces/arithmetic' + +export const add = + (dep: { + add: FnAdd + }): FnAdd> => + (w, z) => complex_binary(dep.add(w.re, z.re), dep.add(w.im, z.im)) + +export const addReal = + (dep: { + addReal: FnAddReal + }): FnAddReal, T> => + (z, r) => complex_binary(dep.addReal(z.re, r), z.im) + +export const unaryMinus = + (dep: { + unaryMinus: FnUnaryMinus + }): FnUnaryMinus> => + (z) => complex_binary(dep.unaryMinus(z.re), dep.unaryMinus(z.im)) + +export const conj = + (dep: { + unaryMinus: FnUnaryMinus, + conj: FnConj + }) : FnConj> => + (z) => complex_binary(dep.conj(z.re), dep.unaryMinus(z.im)) + +export const subtract = + (dep: { + subtract: FnSubtract + }): FnSubtract> => + (w, z) => complex_binary(dep.subtract(w.re, z.re), dep.subtract(w.im, z.im)) + +export const multiply = + (dep: { + add: FnAdd, + subtract: FnSubtract, + multiply: FnMultiply, + conj: FnConj + }) => + (w, z) => { + const mult = dep.multiply + const realpart = dep.subtract(mult(w.re, z.re), mult(dep.conj(w.im), z.im)) + const imagpart = dep.add(mult(dep.conj(w.re), z.im), mult(w.im, z.re)) + return complex_binary(realpart, imagpart) + } + +export const absquare = + (dep: { + add: FnAdd, + absquare: FnAbsSquare + }): FnAbsSquare, U> => + (z) => dep.add(dep.absquare(z.re), dep.absquare(z.im)) + +export const divideByReal = + (dep: { + divideByReal: FnDivideByReal + }): FnDivideByReal, T> => + (z, r) => complex_binary(dep.divideByReal(z.re, r), dep.divideByReal(z.im, r)) + +export const reciprocal = + (dep: { + conj: FnConj>, + absquare: FnAbsSquare, T>, + divideByReal: FnDivideByReal, T> + }): FnReciprocal> => + (z) => dep.divideByReal(dep.conj(z), dep.absquare(z)) + +export const divide = + (dep: { + multiply: FnMultiply>, + reciprocal: FnReciprocal>, + }): FnDivide> => + (w, z) => dep.multiply(w, dep.reciprocal(z)) + +export const complexSqrt = + (dep: { + conservativeSqrt: FnConservativeSqrt, + isSquare: FnIsSquare, + complex: FnComplexUnary, + unaryMinus: FnUnaryMinus, + zero: FnZero, + nan: FnNaN> + }) => + (r) => { + if (dep.isSquare(r)) return dep.complex(dep.conservativeSqrt(r)) + const negative = dep.unaryMinus(r) + if (dep.isSquare(negative)) { + return complex_binary( + dep.zero(r), dep.conservativeSqrt(negative)) + } + // neither the real number or its negative is a square; could happen + // for example with bigint. So there is no square root. So we have to + // return the NaN of the type. + return dep.nan(dep.complex(r)) + } + +export const sqrt = + (dep: { + isReal: FnIsReal>, + complexSqrt: FnSqrt, + conservativeSqrt: FnConservativeSqrt, + absquare: FnAbsSquare, T>, + addReal: FnAddReal, T>, + divideByReal: FnDivideByReal, T>, + add: FnAdd, + re: FnRe, T>, + }) => + (z: Complex): Complex | T => { + if (dep.isReal(z)) return dep.complexSqrt(z.re) + const myabs = dep.conservativeSqrt(dep.absquare(z)) + const num = dep.addReal(z, myabs) + const r = dep.re(z) + const denomsq = dep.add(dep.add(myabs, myabs), dep.add(r, r)) + const denom = dep.conservativeSqrt(denomsq) + return dep.divideByReal(num, denom) + } + +export const conservativeSqrt = sqrt diff --git a/src/Complex/predicate.ts b/src/Complex/predicate.ts new file mode 100644 index 0000000..557ea83 --- /dev/null +++ b/src/Complex/predicate.ts @@ -0,0 +1,14 @@ +import { Complex } from './type.js' +import {FnEqual} from '../interfaces/relational' +import {FnAdd, FnIsReal, FnIsSquare} from '../interfaces/arithmetic' + +export const isReal = + (dep: { + equal: FnEqual, + add: FnAdd, + isReal: FnIsReal + }): FnIsReal> => + (z) => dep.isReal(z.re) && dep.equal(z.re, dep.add(z.re, z.im)) + +export const isSquare = + (): FnIsSquare> => (z) => true // FIXME: not correct for Complex once we get there diff --git a/src/Complex/relational.ts b/src/Complex/relational.ts new file mode 100644 index 0000000..9807d71 --- /dev/null +++ b/src/Complex/relational.ts @@ -0,0 +1,8 @@ +import { Complex } from './type.js' +import {FnEqual} from '../interfaces/relational' + +export const equal = + (dep: { + equal: FnEqual + }): FnEqual> => + (w, z) => dep.equal(w.re, z.re) && dep.equal(w.im, z.im) diff --git a/src/Complex/type.ts b/src/Complex/type.ts index affbedc..3b4e82d 100644 --- a/src/Complex/type.ts +++ b/src/Complex/type.ts @@ -1,22 +1,58 @@ -import {joinTypes, typeOfDependency, Dependency} from '../core/Dispatcher.js' +import { + joinTypes, typeOfDependency, Dependency, +} from '../core/Dispatcher.js' +import type {FnNaN, FnOne, FnRe, FnZero} from '../interfaces/arithmetic.js' -export type Complex = {re: T; im: T;} +export type Complex = { re: T; im: T; } export const Complex_type = { - test: (dep: {testT: (z: unknown) => z is T}) => + test: (dep: { testT: (z: unknown) => z is T }) => (z: unknown): z is Complex => - typeof z === 'object' && 're' in z && 'im' in z - && dep.testT(z.re) && dep.testT(z.im), + typeof z === 'object' && z != null && 're' in z && 'im' in z + && dep.testT(z['re']) && dep.testT(z['im']), infer: (dep: typeOfDependency) => (z: Complex) => joinTypes(dep.typeOf(z.re), dep.typeOf(z.im)), from: { T: (dep: Dependency<'zero', [T]>) => (t: T) => - ({re: t, im: dep.zero(t)}), - Complex: (dep: {convert: (from: U) => T}) => - (z: Complex) => ({re: dep.convert(z.re), im: dep.convert(z.im)}) + ({ re: t, im: dep.zero(t) }), + Complex: (dep: { convert: (from: U) => T }) => + (z: Complex) => ({ re: dep.convert(z.re), im: dep.convert(z.im) }) } } -export const complex_unary = (dep: Dependency<'zero', [T]>) => - (t: T) => ({re: t, im: dep.zero(t)}) -export const complex_binary = (t: T, u: T) => ({re: t, im: u}) +export type FnComplexUnary = (t: T) => Complex + +export const complex_unary = + (dep: { + zero: FnZero + }): FnComplexUnary => + (t) => ({ re: t, im: dep.zero(t) }) + +export type FnComplexBinary = (re: T, im: T) => Complex + +export const complex_binary = (t: T, u: T): Complex => ({ re: t, im: u }) + +export const zero = + (dep: { + zero: FnZero + }): FnZero> => + (z) => complex_binary(dep.zero(z.re), dep.zero(z.im)) + +export const one = + (dep: { + zero: FnZero, + one: FnOne + }): FnOne> => + (z) => complex_binary(dep.one(z.re), dep.zero(z.im)) + +export const nan = + (dep: { + nan: FnNaN + }): FnNaN> => + (z) => complex_binary(dep.nan(z.re), dep.nan(z.im)) + +export const re = + (dep: { + re: FnRe + }): FnRe, T> => + (z) => dep.re(z.re) diff --git a/src/all.ts b/src/all.ts index 192c7be..e2e83f1 100644 --- a/src/all.ts +++ b/src/all.ts @@ -1,2 +1,3 @@ export * from './numbers/all.js' export * from './Complex/all.js' +export * from './generic/all.js' diff --git a/src/core/Config.ts b/src/core/Config.ts index 3765328..c1eb24c 100644 --- a/src/core/Config.ts +++ b/src/core/Config.ts @@ -1,4 +1,5 @@ export type Config = { + epsilon: number predictable: boolean } diff --git a/src/core/Dispatcher.ts b/src/core/Dispatcher.ts index bed8f0d..9b005d9 100644 --- a/src/core/Dispatcher.ts +++ b/src/core/Dispatcher.ts @@ -9,16 +9,62 @@ type TypeName = string type Parameter = TypeName -type Signature = Parameter[] +type InputSignature = Parameter[] +type DependenciesType = Record -export interface ImplementationTypes {} export type typeOfDependency = {typeOf: (x: unknown) => TypeName} -// Helper for collecting implementations -// (Really just suffixes the type name onto the keys of exports) -export type ForType = keyof Exports extends string - ? {[K in keyof Exports as `${K}_${T}`]: Exports[K]} - : never +// All of the implementations must publish descriptions of their +// return types into the following interface, using the format +// described just below: +export interface ReturnTypes {} + +/***** + To describe one implementation for a hypothetical operation `foo`, there + should be a property of the interface whose name starts with `foo` and whose + next character, if any, is an underscore. The type of this property + must be the return type of that implementation when Params matches the + parameter types of the implementation, and `never` otherwise. + Thus to describe an implementation that takes a number and a string and + returns a boolean, for example, you could write + ``` + declare module "Dispatcher" { + interface ReturnTypes { + foo_example: Params extends [number, string] ? boolean : never + } + } + ``` + If there is another, generic implementation that takes one argument + of any type and returns a Vector of that type, you can say + ``` + ... + foo_generic: Params extends [infer T] ? Vector : never + ... + ``` + In practice, each subdirectory corresponding to a type, like Complex, + defines an interface, like `ComplexReturn` for the implementations + in that subdirectory, which can mostly be defined without suffixes because + there's typically just a single implementation within that domain. + Then the module responsible for collating all of the implementations for + that type inserts all of the properties of that interface into `ReturnTypes` + suitably suffixed to avoid collisions. + + One might think that simply defining an implementation for `foo` + of type `(n: number, s: string) => boolean` would provide all of the same + information as the type of the key `foo_example` in the ReturnTypes + interface above, but in practice TypeScript has challenges in extracting + types relating to functions. (In particular, there is no + way to get the specialized return type of a generic function when it is + called on aguments whose specific types match the generic parameters.) + Hence the need for this additional mechanism to specify return types, in + a way readily suited for TypeScript type computations. +*****/ + +// Helpers for specifying signatures + +// A basic signature with concrete types +export type Signature = + CandidateParams extends ActualParams ? Returns : never //dummy implementation for now export function joinTypes(a: TypeName, b: TypeName) { @@ -26,27 +72,27 @@ export function joinTypes(a: TypeName, b: TypeName) { return 'any' } -/** - * Build up to Dependency type lookup - */ -type DependenciesType = Record +// Used to filter keys that match a given operation name +type BeginsWith = Name | `${Name}_${string}` -type FinalShape = - FuncType extends (arg: DependenciesType) => Function - ? ReturnType : FuncType +// Look up the return type of an implementation based on its name +// and the parameters it takes +export type ImpReturns = + {[K in keyof ReturnTypes]: K extends BeginsWith + ? ReturnTypes[K] : never}[keyof ReturnTypes] -type BeginsWith = `${Name}${string}` - -type DependencyTypes = - {[K in keyof Ob]: K extends BeginsWith - ? FinalShape extends (...args: Params) => any - ? FinalShape - : never - : never} +// The type of an implementation (with dependencies satisfied, +// based on its name and the parameters it takes +export type ImpType = + (...args: Params) => ImpReturns +// The type of a dependency on an implementation based on its name +// and the parameters it takes (just a simple object with one property +// named the same as the operation, of value type equal to the type of +// that implementation. These can be `&`ed together in case of multiple +// dependencies: export type Dependency = - {[N in Name]: - DependencyTypes[keyof ImplementationTypes]} + {[N in Name]: ImpType} // Now types used in the Dispatcher class itself @@ -64,9 +110,9 @@ type SpecificationsGroup = Record export class Dispatcher { installSpecification( name: string, - signature: Signature, + signature: InputSignature, returns: TypeName, - dependencies: Record, + dependencies: Record, behavior: Function // possible todo: constrain this type based // on the signature, return type, and dependencies. Not sure if // that's really possible, though. diff --git a/src/generic/all.ts b/src/generic/all.ts new file mode 100644 index 0000000..1b1b8a4 --- /dev/null +++ b/src/generic/all.ts @@ -0,0 +1,3 @@ +import * as generic from './arithmetic.js' + +export { generic } diff --git a/src/generic/arithmetic.ts b/src/generic/arithmetic.ts new file mode 100644 index 0000000..a1379df --- /dev/null +++ b/src/generic/arithmetic.ts @@ -0,0 +1,7 @@ +import type { FnMultiply, FnSquare } from "../interfaces/arithmetic" + +export const square = + (dep: { + multiply: FnMultiply + }): FnSquare => + (z) => dep.multiply(z, z) diff --git a/src/index.ts b/src/index.ts index bb83486..297b271 100644 --- a/src/index.ts +++ b/src/index.ts @@ -2,3 +2,19 @@ import {Dispatcher} from './core/Dispatcher.js' import * as Specifications from './all.js' export default new Dispatcher(Specifications) + +import {Complex} from './Complex/type.js' +import {absquare as absquare_complex} from './Complex/arithmetic.js' + +const mockRealAdd = (a: number, b: number) => a+b +const mockComplexAbsquare = (z: Complex) => z.re*z.re + z.im*z.im + +const quatAbsquare = absquare_complex({ + add: mockRealAdd, + absquare: mockComplexAbsquare +}) + +const myabs = quatAbsquare({re: {re: 0, im: 1}, im: {re:2, im: 3}}) +const typeTest: typeof myabs = 7 // check myabs is just a number + +console.log('Result is', myabs) diff --git a/src/interfaces/arithmetic.ts b/src/interfaces/arithmetic.ts new file mode 100644 index 0000000..91b59bf --- /dev/null +++ b/src/interfaces/arithmetic.ts @@ -0,0 +1,28 @@ +// shared interfaces + +import { Complex } from "../Complex/type" + +// Note: right now I've added an 'Fn*' prefix, +// so it is clear that the type hold a function type definition +// This is not necessary though, it is just a naming convention. +export type FnAdd = (a: T, b: T) => T +export type FnAddReal = (a: T, b: U) => T +export type FnUnaryMinus = (a: T) => T +export type FnConj = (a: T) => T +export type FnSubtract = (a: T, b: T) => T +export type FnMultiply = (a: T, b: T) => T +export type FnAbsSquare = (a: T) => U +export type FnReciprocal = (a: T) => T +export type FnDivide = (a: T, b: T) => T +export type FnDivideByReal = (a: T, b: U) => T +export type FnConservativeSqrt = (a: T) => T +export type FnSqrt = (a: T) => T | Complex +export type FnSquare = (z: T) => T + +export type FnIsReal = (a: T) => boolean +export type FnIsSquare = (a: T) => boolean + +export type FnZero = (a: T) => T +export type FnOne = (a: T) => T +export type FnNaN = (a: T) => T +export type FnRe = (a: T) => U diff --git a/src/interfaces/relational.ts b/src/interfaces/relational.ts new file mode 100644 index 0000000..2865f77 --- /dev/null +++ b/src/interfaces/relational.ts @@ -0,0 +1,3 @@ + +export type FnEqual = (a: T, b: T) => boolean +export type FnUnequal = (a: T, b: T) => boolean diff --git a/src/numbers/all.ts b/src/numbers/all.ts index 5aea220..deb4a8e 100644 --- a/src/numbers/all.ts +++ b/src/numbers/all.ts @@ -1,8 +1,3 @@ -import {ForType} from '../core/Dispatcher.js' import * as numbers from './native.js' export {numbers} - -declare module "../core/Dispatcher" { - interface ImplementationTypes extends ForType<'numbers', typeof numbers> {} -} diff --git a/src/numbers/arithmetic.ts b/src/numbers/arithmetic.ts index e78d9ec..7142797 100644 --- a/src/numbers/arithmetic.ts +++ b/src/numbers/arithmetic.ts @@ -1,20 +1,29 @@ -import {configDependency} from '../core/Config.js' -import {Dependency} from '../core/Dispatcher.js' +import { Config } from '../core/Config.js' +import type { FnComplexBinary } from '../Complex/type.js' +import { FnAdd, FnConj, FnSubtract, FnUnaryMinus, FnMultiply, FnAbsSquare, FnReciprocal, FnDivide, FnConservativeSqrt, FnSqrt } from '../interfaces/arithmetic.js' + +export const add: FnAdd = (a, b) => a + b +export const addReal = add +export const unaryMinus: FnUnaryMinus = (a) => -a +export const conj: FnConj = (a) => a +export const subtract: FnSubtract = (a, b) => a - b +export const multiply: FnMultiply = (a, b) => a * b +export const absquare: FnAbsSquare = (a) => a * a +export const reciprocal: FnReciprocal = (a) => 1 / a +export const divide: FnDivide = (a, b) => a / b +export const divideByReal = divide + +export const conservativeSqrt: FnConservativeSqrt = (a) => isNaN(a) ? NaN : Math.sqrt(a) -export const add = (a: number, b: number) => a + b -export const unaryMinus = (a: number) => -a -export const subtract = (a: number, b: number) => a - b -export const multiply = (a: number, b: number) => a * b -export const divide = (a: number, b: number) => a / b export const sqrt = - (dep: configDependency - & Dependency<'complex', [number, number]>) => { - if (dep.config.predictable || !dep.complex) { - return (a: number) => isNaN(a) ? NaN : Math.sqrt(a) - } - return (a: number) => { - if (isNaN(a)) return NaN - if (a >= 0) return Math.sqrt(a) - return dep.complex(0, Math.sqrt(unaryMinus(a))) - } - } + (dep: { + config: Config, + complex: FnComplexBinary + }): FnSqrt => { + if (dep.config.predictable || !dep.complex) return conservativeSqrt + return a => { + if (isNaN(a)) return NaN + if (a >= 0) return Math.sqrt(a) + return dep.complex(0, Math.sqrt(unaryMinus(a))) + } + } diff --git a/src/numbers/predicate.ts b/src/numbers/predicate.ts new file mode 100644 index 0000000..4015c55 --- /dev/null +++ b/src/numbers/predicate.ts @@ -0,0 +1,4 @@ +import type { FnIsReal, FnIsSquare } from "../interfaces/arithmetic" + +export const isReal: FnIsReal = (a) => true +export const isSquare: FnIsSquare = (a) => a >= 0 diff --git a/src/numbers/relational.ts b/src/numbers/relational.ts new file mode 100644 index 0000000..ae9a63d --- /dev/null +++ b/src/numbers/relational.ts @@ -0,0 +1,27 @@ +import { Config } from '../core/Config.js' +import type { FnEqual, FnUnequal } from '../interfaces/relational.js' + +const DBL_EPSILON = Number.EPSILON || 2.2204460492503130808472633361816E-16 + +export const equal = + (dep: { + config: Config + }): FnEqual => (x, y) => { + const eps = dep.config.epsilon + if (eps === null || eps === undefined) return x === y + if (x === y) return true + if (isNaN(x) || isNaN(y)) return false + + if (isFinite(x) && isFinite(y)) { + const diff = Math.abs(x - y) + if (diff < DBL_EPSILON) return true + return diff <= Math.max(Math.abs(x), Math.abs(y)) * eps + } + + return false + } + +export const unequal = (dep: { + equal: FnEqual +}): FnUnequal => + (x, y) => !dep.equal(x, y) diff --git a/src/numbers/type.ts b/src/numbers/type.ts index 67dbd29..77336ef 100644 --- a/src/numbers/type.ts +++ b/src/numbers/type.ts @@ -1,7 +1,12 @@ +import type { FnNaN, FnOne, FnZero, FnRe } from "../interfaces/arithmetic" + export const number_type = { before: ['Complex'], test: (n: unknown): n is number => typeof n === 'number', - from: {string: s => +s} + from: { string: (s: string) => +s } } -export const zero = (a: number) => 0 +export const zero: FnZero = (a) => 0 +export const one: FnOne = (a) => 1 +export const nan: FnNaN = (a) => NaN +export const re: FnRe = (a) => a