Compare commits

...

11 Commits

Author SHA1 Message Date
Glen Whitney 6d63d23498 refactor: Streamline types and signature specfications
The main mechanism for simplification was simply to assume that
  ZeroType<T> and OneType<T> will always be in T. That removed a lot
  of specialized typing, and presumably will be true in practice.

  Otherwise, removes extraneous type definitions and adds/clarifies
  a number of comments to hopefully make the scheme as clear as possible.
2022-12-24 11:16:58 -05:00
Glen Whitney 072b2a1f79 refactor: Streamline publishing operations
Avoids clumsy naming properties by making the names the keys in
  an interface to which the signatures of all operations must be
  published. This also reduces the number of different symbols and
  avoids long lists of imports in the modules implementing multiple
  operations, which were redundant with the list of functions
  exported from such modules.
2022-12-24 10:09:14 -05:00
Glen Whitney 74e2aef524 refactor: tighter universal interface types 2022-12-24 00:41:35 -05:00
Jos de Jong a5848125e4 fix and test absquare for quaternion 2022-12-23 17:18:24 +01:00
Jos de Jong 60ce6212b4 convert code to type aliases 2022-12-23 13:52:56 +01:00
Jos de Jong 04024a2a8d fix a TS issue 2022-12-23 12:22:41 +01:00
Jos de Jong cbd1719227 experiment: convert all implementations to plain types 2022-12-23 11:27:39 +01:00
Glen Whitney 8c06c8f36e feat: Add generic operation `square` and numeric `unequal` (#4)
Co-authored-by: Jos de Jong <wjosdejong@gmail.com>
Co-authored-by: Glen Whitney <glen@studioinfinity.org>
Reviewed-on: #4
2022-12-22 16:12:36 +00:00
Glen Whitney fbec410c42 feat: Implement complex arithmetic through sqrt
Together with any auxiliary functions needed for that goal. Also
  strives to ensure the same functions are being defined for
  number and for `Complex<T>`.
2022-12-22 00:14:58 -05:00
Glen Whitney d55776655f refactor: Convenience type operator for specifying concrete signatures 2022-12-21 11:41:25 -05:00
Glen Whitney 1eb73be2fa refactor: entirely new scheme for specifying return types 2022-12-21 00:18:42 -05:00
23 changed files with 393 additions and 71 deletions

3
.gitignore vendored
View File

@ -129,6 +129,9 @@ dist
# Stores VSCode versions used for testing VSCode extensions
.vscode-test
# Webstiorm
.idea
# yarn v2
.yarn/cache
.yarn/unplugged

View File

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

102
src/Complex/arithmetic.ts Normal file
View File

@ -0,0 +1,102 @@
import {Complex} from './type.js'
import type {
Dependencies, OpType, OpReturns, RealType, ZeroType
} from '../interfaces/type.js'
declare module "../interfaces/type" {
interface Operations<T> {
// TODO: Make Dispatcher collapse operations that match
// after removing any `_...` suffixes; the following should be
// additional dispatches for add and divide, not separate
// operations, in the final mathjs bundle.
add_real: {params: [T, RealType<T>], returns: T}
divide_real: {params: [T, RealType<T>], returns: T}
}
}
export const add =
<T>(dep: Dependencies<'add' | 'complex', T>): OpType<'add', Complex<T>> =>
(w, z) => dep.complex(dep.add(w.re, z.re), dep.add(w.im, z.im))
export const add_real =
<T>(dep: Dependencies<'add_real' | 'complex', T>):
OpType<'add_real', Complex<T>> =>
(z, r) => dep.complex(dep.add_real(z.re, r), z.im)
export const unaryMinus =
<T>(dep: Dependencies<'unaryMinus' | 'complex', T>):
OpType<'unaryMinus', Complex<T>> =>
z => dep.complex(dep.unaryMinus(z.re), dep.unaryMinus(z.im))
export const conj =
<T>(dep: Dependencies<'unaryMinus' | 'conj' | 'complex', T>):
OpType<'conj', Complex<T>> =>
z => dep.complex(dep.conj(z.re), dep.unaryMinus(z.im))
export const subtract =
<T>(dep: Dependencies<'subtract' | 'complex', T>):
OpType<'subtract', Complex<T>> =>
(w, z) => dep.complex(dep.subtract(w.re, z.re), dep.subtract(w.im, z.im))
export const multiply =
<T>(dep: Dependencies<
'add' | 'subtract' | 'multiply' | 'conj' | 'complex', T>):
OpType<'multiply', Complex<T>> =>
(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 dep.complex(realpart, imagpart)
}
export const absquare =
<T>(dep: Dependencies<'absquare', T>
& Dependencies<'add', OpReturns<'absquare', T>>):
OpType<'absquare', Complex<T>> =>
z => dep.add(dep.absquare(z.re), dep.absquare(z.im))
export const divideByReal =
<T>(dep: Dependencies<'divide_real' | 'complex', T>):
OpType<'divide_real', Complex<T>> =>
(z, r) => dep.complex(dep.divide_real(z.re, r), dep.divide_real(z.im, r))
export const reciprocal =
<T>(dep: Dependencies<'conj' | 'absquare' | 'divide_real', Complex<T>>):
OpType<'reciprocal', Complex<T>> =>
z => dep.divide_real(dep.conj(z), dep.absquare(z))
export const divide =
<T>(dep: Dependencies<'multiply' | 'reciprocal', Complex<T>>):
OpType<'divide', Complex<T>> =>
(w, z) => dep.multiply(w, dep.reciprocal(z))
// The dependencies are slightly tricky here, because there are three types
// involved: Complex<T>, T, and RealType<T>, all of which might be different,
// and we have to get it straight which operations we need on each type, and
// in fact, we need `add_real` on both T and Complex<T>, hence the dependency
// with a custom name, not generated via Dependencies<...>
export const sqrt =
<T>(dep: Dependencies<'add' | 'equal' | 'conservativeSqrt' | 'unaryMinus',
RealType<T>>
& Dependencies<'zero' | 'add_real' | 'complex', T>
& Dependencies<'absquare' | 're' | 'divide_real', Complex<T>>
& {add_complex_real: OpType<'add_real', Complex<T>>}):
OpType<'sqrt', Complex<T>> =>
z => {
const myabs = dep.conservativeSqrt(dep.absquare(z))
const r = dep.re(z)
const negr = dep.unaryMinus(r)
if (dep.equal(myabs, negr)) {
// pure imaginary square root; z.im already zero
return dep.complex(
dep.zero(z.re), dep.add_real(z.im, dep.conservativeSqrt(negr)))
}
const num = dep.add_complex_real(z, myabs)
const denomsq = dep.add(dep.add(myabs, myabs), dep.add(r, r))
const denom = dep.conservativeSqrt(denomsq)
return dep.divide_real(num, denom)
}
export const conservativeSqrt = sqrt

9
src/Complex/predicate.ts Normal file
View File

@ -0,0 +1,9 @@
import {Complex} from './type.js'
import type {Dependencies, OpType} from '../interfaces/type.js'
export const isReal =
<T>(dep: Dependencies<'add' | 'equal' | 'isReal', T>):
OpType<'isReal', Complex<T>> =>
z => dep.isReal(z.re) && dep.equal(z.re, dep.add(z.re, z.im))
export const isSquare: OpType<'isSquare', Complex<any>> = z => true // FIXME: not correct for Complex<bigint> once we get there

View File

@ -0,0 +1,6 @@
import {Complex} from './type.js'
import {Dependencies, OpType} from '../interfaces/type.js'
export const equal =
<T>(dep: Dependencies<'equal', T>): OpType<'equal', Complex<T>> =>
(w, z) => dep.equal(w.re, z.re) && dep.equal(w.im, z.im)

View File

@ -1,22 +1,63 @@
import {joinTypes, typeOfDependency, Dependency} from '../core/Dispatcher.js'
import {joinTypes, typeOfDependency} from '../core/Dispatcher.js'
import type {
ZeroType, OneType, NaNType, Dependencies, OpType, OpReturns
} from '../interfaces/type.js'
export type Complex<T> = {re: T; im: T;}
export type Complex<T> = { re: T; im: T; }
export const Complex_type = {
test: <T>(dep: {testT: (z: unknown) => z is T}) =>
test: <T>(dep: { testT: (z: unknown) => z is T }) =>
(z: unknown): z is Complex<T> =>
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<unknown>) => joinTypes(dep.typeOf(z.re), dep.typeOf(z.im)),
from: {
T: <T>(dep: Dependency<'zero', [T]>) => (t: T) =>
({re: t, im: dep.zero(t)}),
Complex: <U,T>(dep: {convert: (from: U) => T}) =>
(z: Complex<U>) => ({re: dep.convert(z.re), im: dep.convert(z.im)})
T: <T>(dep: Dependencies<'zero', T>) => (t: T) =>
({ re: t, im: dep.zero(t) }),
Complex: <U, T>(dep: { convert: (from: U) => T }) =>
(z: Complex<U>) => ({ re: dep.convert(z.re), im: dep.convert(z.im) })
}
}
export const complex_unary = <T>(dep: Dependency<'zero', [T]>) =>
(t: T) => ({re: t, im: dep.zero(t)})
export const complex_binary = <T>(t: T, u: T) => ({re: t, im: u})
declare module "../interfaces/type" {
interface AssociatedTypes<T> {
Complex: T extends Complex<infer R> ? {
type: Complex<R>
zero: Complex<ZeroType<R>>
one: Complex<OneType<R> | ZeroType<R>>
nan: Complex<NaNType<R>>
real: RealType<R>
} : never
}
interface Operations<T> {
complex: {params: [T] | [T,T], returns: Complex<T>}
}
}
export const complex =
<T>(dep: Dependencies<'zero', T>): OpType<'complex', T> =>
(a, b) => ({re: a, im: b || dep.zero(a)})
export const zero =
<T>(dep: Dependencies<'zero', T>
& Dependencies<'complex', OpReturns<'zero', T>>):
OpType<'zero', Complex<T>> =>
z => dep.complex(dep.zero(z.re), dep.zero(z.im))
export const one =
<T>(dep: Dependencies<'one' | 'zero', T>
& Dependencies<'complex', OpReturns<'one' | 'zero', T>>):
OpType<'one', Complex<T>> =>
z => dep.complex(dep.one(z.re), dep.zero(z.im))
export const nan =
<T>(dep: Dependencies<'nan', T>
& Dependencies<'complex', OpReturns<'nan', T>>):
OpType<'nan', Complex<T>> =>
z => dep.complex(dep.nan(z.re), dep.nan(z.im))
export const re =
<T>(dep: Dependencies<'re', T>): OpType<'re', Complex<T>> =>
z => dep.re(z.re)

View File

@ -1,2 +1,3 @@
export * from './numbers/all.js'
export * from './Complex/all.js'
export * from './generic/all.js'

View File

@ -1,4 +1,5 @@
export type Config = {
epsilon: number
predictable: boolean
}

View File

@ -10,44 +10,18 @@
type TypeName = string
type Parameter = TypeName
type Signature = Parameter[]
type DependenciesType = Record<string, Function>
export interface ImplementationTypes {}
// A "canned" dependency for a builtin function:
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<T extends string, Exports> = keyof Exports extends string
? {[K in keyof Exports as `${K}_${T}`]: Exports[K]}
: never
// Utility needed in type definitions
//dummy implementation for now
export function joinTypes(a: TypeName, b: TypeName) {
if (a === b) return a
return 'any'
}
/**
* Build up to Dependency type lookup
*/
type DependenciesType = Record<string, Function>
type FinalShape<FuncType> =
FuncType extends (arg: DependenciesType) => Function
? ReturnType<FuncType> : FuncType
type BeginsWith<Name extends string> = `${Name}${string}`
type DependencyTypes<Ob, Name extends string, Params extends unknown[]> =
{[K in keyof Ob]: K extends BeginsWith<Name>
? FinalShape<Ob[K]> extends (...args: Params) => any
? FinalShape<Ob[K]>
: never
: never}
export type Dependency<Name extends string, Params extends unknown[]> =
{[N in Name]:
DependencyTypes<ImplementationTypes, N, Params>[keyof ImplementationTypes]}
// Now types used in the Dispatcher class itself
type TypeSpecification = {

1
src/generic/all.ts Normal file
View File

@ -0,0 +1 @@
export * as generic from './native.js'

View File

@ -0,0 +1,5 @@
import type {Dependencies, OpType} from '../interfaces/type.js'
export const square =
<T>(dep: Dependencies<'multiply', T>): OpType<'square', T> =>
z => dep.multiply(z, z)

2
src/generic/native.ts Normal file
View File

@ -0,0 +1,2 @@
export * from './arithmetic.js'
export * from './relational.js'

View File

@ -0,0 +1,5 @@
import {Dependencies, OpType} from '../interfaces/type.js'
export const unequal =
<T>(dep: Dependencies<'equal', T>): OpType<'unequal', T> =>
(x, y) => !dep.equal(x, y)

View File

@ -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<number>) => 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)

View File

@ -0,0 +1,23 @@
import type {Complex} from '../Complex/type.js'
import type {RealType} from './type.js'
type UnaryOperator<T> = {params: [T], returns: T}
type BinaryOperator<T> = {params: [T, T], returns: T}
declare module "./type" {
interface Operations<T> {
add: BinaryOperator<T>
unaryMinus: UnaryOperator<T>
conj: UnaryOperator<T>
subtract: BinaryOperator<T>
multiply: BinaryOperator<T>
square: UnaryOperator<T>
absquare: {params: [T], returns: RealType<T>}
reciprocal: UnaryOperator<T>
divide: BinaryOperator<T>
conservativeSqrt: UnaryOperator<T>
sqrt: {
params: [T],
returns: T extends Complex<any> ? T : T | Complex<T>
}
}
}

View File

@ -0,0 +1,9 @@
// Warning: a module must have something besides just a "declare module"
// section; otherwise it is ignored.
export type UnaryPredicate<T> = {params: [T], returns: boolean}
declare module "./type" {
interface Operations<T> {
isReal: UnaryPredicate<T>
isSquare: UnaryPredicate<T>
}
}

View File

@ -0,0 +1,9 @@
// Warning: a module must have something besides just a "declare module"
// section; otherwise it is ignored.
export type BinaryPredicate<T> = {params: [T, T], returns: boolean}
declare module "./type" {
interface Operations<T> {
equal: BinaryPredicate<T>
unequal: BinaryPredicate<T>
}
}

74
src/interfaces/type.ts Normal file
View File

@ -0,0 +1,74 @@
/*****
* Every typocomath type has some associated types; they need
* to be published in the following interface. The key is the
* name of the type, and within the subinterface for that key,
* the type of the 'type' property is the actual TypeScript type
* we are associating the other properties to. Note the interface
* is generic with one parameter, corresponding to the fact that
* typocomath currently only allows generic types with a single
* generic parameter. This way, AssociatedTypes<SubType> can give the
* associated types for a generic type instantiated with SubType.
* That's not necessary for the 'undefined' type (or if you look in the
* `numbers` subdirectory, the 'number' type) or any concrete type,
* but that's OK, the generic parameter doesn't hurt in those cases.
****/
export interface AssociatedTypes<T> {
undefined: {
type: undefined
zero: undefined
one: undefined
nan: undefined
real: undefined
}
}
type AssociatedTypeNames = keyof AssociatedTypes<unknown>['undefined']
type ALookup<T, Name extends AssociatedTypeNames> = {
[K in keyof AssociatedTypes<T>]:
T extends AssociatedTypes<T>[K]['type'] ? AssociatedTypes<T>[K][Name] : never
}[keyof AssociatedTypes<T>]
// For everything to compile, zero and one must be subtypes of T:
export type ZeroType<T> = ALookup<T, 'zero'> & T
export type OneType<T> = ALookup<T, 'one'> & T
// But I believe 'nan' really might not be, like I think we will have to use
// 'undefined' for the nan of 'bigint', as it has nothing at all like NaN,
// so don't force it:
export type NaNType<T> = ALookup<T, 'nan'>
export type RealType<T> = ALookup<T, 'real'>
/*****
* The global signature patterns for all operations need to be published in the
* following interface. Each key is the name of an operation (but note that
* the Dispatcher will automatically merge operations that have the same
* name when the first underscore `_` and everything thereafter is stripped).
* The type of each key should be an interface with two properties: 'params'
* whose type is the type of the parameter list for the operation, and
* 'returns' whose type is the return type of the operation on those
* parameters. These types are generic in a parameter type T which should
* be interpreted as the type that the operation is supposed to "primarily"
* operate on, although note that some of the parameters and/or return types
* may depend on T rather than be exactly T.
* So note that the example 're' below provides essentially the same
* information that e.g.
* `type ReOp<T> = (t: T) => RealType<T>`
* would, but in a way that is much easier to manipulate in TypeScript,
* and it records the name of the operation as 're' also by virtue of the
* key 're' in the interface.
****/
export interface Operations<T> {
zero: {params: [T], returns: ZeroType<T>}
one: {params: [T], returns: OneType<T>}
// nan needs to be able to operate on its own output for everything
// else to compile. That's why its parameter type is widened:
nan: {params: [T | NaNType<T>], returns: NaNType<T>}
re: {params: [T], returns: RealType<T>}
}
type OpKey = keyof Operations<unknown>
export type OpReturns<Name extends OpKey, T> = Operations<T>[Name]['returns']
export type OpType<Name extends OpKey, T> =
(...args: Operations<T>[Name]['params']) => OpReturns<Name, T>
export type Dependencies<Name extends OpKey, T> = {[K in Name]: OpType<K, T>}

View File

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

View File

@ -1,20 +1,25 @@
import {configDependency} from '../core/Config.js'
import {Dependency} from '../core/Dispatcher.js'
import type {configDependency} from '../core/Config.js'
import type {Dependencies, OpType} from '../interfaces/type.js'
export const add: OpType<'add', number> = (a, b) => a + b
export const unaryMinus: OpType<'unaryMinus', number> = a => -a
export const conj: OpType<'conj', number> = a => a
export const subtract: OpType<'subtract', number> = (a, b) => a - b
export const multiply: OpType<'multiply', number> = (a, b) => a * b
export const absquare: OpType<'absquare', number> = a => a * a
export const reciprocal: OpType<'reciprocal', number> = a => 1 / a
export const divide: OpType<'divide', number> = (a, b) => a / b
const basicSqrt = a => isNaN(a) ? NaN : Math.sqrt(a)
export const conservativeSqrt: OpType<'conservativeSqrt', number> = basicSqrt
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: configDependency & Dependencies<'complex', number>):
OpType<'sqrt', number> => {
if (dep.config.predictable || !dep.complex) return basicSqrt
return a => {
if (isNaN(a)) return NaN
if (a >= 0) return Math.sqrt(a)
return dep.complex(0, Math.sqrt(unaryMinus(a)))
}
}

4
src/numbers/predicate.ts Normal file
View File

@ -0,0 +1,4 @@
import type {OpType} from '../interfaces/type.js'
export const isReal: OpType<'isReal', number> = (a) => true
export const isSquare: OpType<'isSquare', number> = (a) => a >= 0

21
src/numbers/relational.ts Normal file
View File

@ -0,0 +1,21 @@
import {configDependency} from '../core/Config.js'
import {OpType} from '../interfaces/type.js'
const DBL_EPSILON = Number.EPSILON || 2.2204460492503130808472633361816E-16
export const equal =
(dep: configDependency): OpType<'equal', number> =>
(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
}

View File

@ -1,7 +1,25 @@
import type { OpType } from '../interfaces/type.js'
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
declare module "../interfaces/type" {
interface AssociatedTypes<T> {
numbers: {
type: number
zero: 0
one: 1
nan: typeof NaN
real: number
}
}
}
// I don't like the redundancy of repeating 'zero'; any way to eliminate that?
export const zero: OpType<'zero', number> = (a) => 0
export const one: OpType<'one', number> = (a) => 1
export const nan: OpType<'nan', number> = (a) => NaN
export const re: OpType<'re', number> = (a) => a