65.9K
CodeProject 正在变化。 阅读更多。
Home

TypeScript 中的函数式编程

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.91/5 (7投票s)

2023 年 10 月 20 日

CPOL

17分钟阅读

viewsIcon

17994

downloadIcon

214

通过代数、数字、欧几里得平面和分形进行 TypeScript 函数式编程

目录

  1. 引言
  2. TypeScript 环境
  3. 通过函数表示数据
    1. Sets
    2. 二元运算
    3. 更进一步
  4. 欧几里得平面
    1. 绘制圆盘
    2. 绘制水平和垂直半平面
    3. 函数
    4. 更进一步
  5. 分形
    1. 复数与绘图
    2. 曼德勃罗集分形
    3. 更进一步
  6. 历史

引言

在 TypeScript 中,函数只不过是对象。因此,函数可以被构造、作为参数传递、从函数返回或赋值给变量。因此,TypeScript 拥有一等函数。更确切地说,TypeScript 支持以下内容:

  • 高阶函数的参数
  • 高阶函数的返回值
  • 嵌套函数
  • 匿名函数
  • 闭包
  • 部分应用(ECMAScript 5)

本文不讨论函数式编程的基础知识,因为您可以在互联网上找到大量关于此主题的资源。相反,它将讨论 TypeScript 中应用于代数、数字、欧几里得平面和分形的函数式编程。本文提供的示例将从简单到复杂,但始终以简单、直接和易于理解的方式进行说明。

TypeScript 环境

要运行源代码,您需要安装Node.js。安装 Node.js 后,下载源代码存档,解压缩,在终端中转到您解压缩的源代码文件夹,设置 TypeScript 环境并使用以下命令安装所有必需的依赖项:

npm install

要运行数字演示,请运行以下命令:

npm run numbers

要运行欧几里得平面演示,请运行以下命令:

npm run plane

要运行分形演示,请运行以下命令:

npm run fractals

通过函数表示数据

S 为任意元素集合 abc ...(例如,桌子上的书或欧几里得平面的点),设 S' 为这些元素的任意子集(例如,桌子上的绿色书或半径为 1、圆心在欧几里得平面原点的圆内的点)。

集合 S'特征函数 S'(x) 是一个函数,它将 truefalseS 的每个元素 x 关联起来。

S'(x) = true if x is in S'
S'(x) = false if x is not in S'

S 为桌子上的书的集合,设 S' 为桌子上的绿色书的集合。设 ab 是两本绿色书,设 cd 是桌子上的两本红色书。则:

S'(a) = S'(b) = true
S'(c) = S'(d) = false

S 为欧几里得平面上的点集,设 S' 为以原点 (0, 0) 为圆心、半径为 1 的圆内的点集(单位圆)。设 ab 是单位圆内的两个点,设 cd 是以欧几里得平面原点为中心、半径为 2 的圆内的两个点。则:

S'(a) = S'(b) = true
S'(c) = S'(d) = false

因此,任何集合 S' 都可以通过其特征函数来表示。该函数以元素作为参数,如果该元素在 S' 中则返回 true,否则返回 false。换句话说,集合(抽象数据类型)可以通过 TypeScript 中的函数来表示。

type Set<T> = (x: T) => boolean

在接下来的章节中,我们将看到如何用函数式的方式通过 TypeScript 表示集合代数中的一些基本集合,然后我们将定义集合上的通用二元运算。然后,我们将把这些运算应用于数字,然后应用于欧几里得平面的子集。集合是抽象数据结构,数字的子集和欧几里得平面的子集是抽象数据结构的表示,最后,二元运算是作用于任何抽象数据结构表示的通用逻辑。

Sets

本节介绍通过 TypeScript 表示集合代数中的一些基本集合。

空集

E 为空集,Empty 为其特征函数。在集合代数中,E 是唯一不包含任何元素的集合。因此,Empty 可以定义如下:

Empty(x) = false if x is in E
Empty(x) = false if x is not in E

因此,E 在 TypeScript 中的表示可以定义如下:

const empty = <T>() => (e: T) => false

在集合代数中,Empty 表示如下:

因此,运行下面的代码:

console.log('\nEmpty set:')
console.log('Is 7 in {}?', common.empty<number>()(7))

得到如下结果:

全集

S 为一个集合,设 S' 为包含所有元素的 S 的子集,设 All 为其特征函数。在集合代数中,S' 是包含所有元素的整个集合。因此,All 可以这样定义:

All(x) = true if x is in S

因此,S' 在 TypeScript 中的表示可以定义如下:

const all = <T>() => (e: T) => true

在集合代数中,All 表示如下:

因此,运行下面的代码:

console.log('\nSet All:')
console.log('Is 7 in integers set?', common.all<number>()(7))

得到如下结果:

单元素集

E 为单元素集,Singleton 为其特征函数。在集合代数中,E 也称为单元集或 1-元组,是一个恰好包含一个元素 e 的集合。因此,Singleton 可以定义如下:

Singleton(x) = true if x is e
Singleton(x) = false if x is not e

因此,E 在 TypeScript 中的表示可以定义如下:

const singleton = <T>(x: T) => (y: T) => x === y

因此,运行下面的代码:

console.log('\nSingleton set:')
console.log('Is 7 in the singleton set {0}?', common.singleton(0)(7))
console.log('Is 7 in the singleton set {7}?', common.singleton(7)(7))

得到如下结果:

其他集合

本节介绍整数集的一些子集。

偶数

E 为偶数集,Even 为其特征函数。在数学中,偶数是 2 的倍数。因此,Even 可以定义如下:

Even(x) = true if x is a multiple of 2
Even(x) = false if x is not a multiple of 2

因此,E 在 TypeScript 中的表示可以定义如下:

const even = (x: number) => x % 2 === 0

因此,运行下面的代码:

console.log('\nEven numbers set:')
console.log('Is 99 in even numbers set?', numbers.even(99))
console.log('Is 998 in even numbers set?', numbers.even(998))

得到如下结果:

奇数

E 为奇数集,Odd 为其特征函数。在数学中,奇数是不能被 2 整除的数。因此,Odd 可以定义如下:

Odd(x) = true if x is not a multiple of 2
Odd(x) = false if x is a multiple of 2

因此,E 在 TypeScript 中的表示可以定义如下:

const odd = (x: number) => x % 2 === 1

因此,运行下面的代码:

console.log('\nOdd numbers set:')
console.log('Is 99 in odd numbers set?', numbers.odd(99))
console.log('Is 998 in odd numbers set?', numbers.odd(998))

得到如下结果:

3 的倍数

E 为 3 的倍数集,MultipleOfThree 为其特征函数。在数学中,3 的倍数是可被 3 整除的数。因此,MultipleOfThree 可以定义如下:

MultipleOfThree(x) = true if x is divisible by 3
MultipleOfThree(x) = false if x is not divisible by 3

因此,E 在 TypeScript 中的表示可以定义如下:

const multipleOfThree = (x: number) => x % 3 === 0

因此,运行下面的代码:

console.log('\nMultiples of 3 set:')
console.log('Is 99 in multiples of 3 set?', numbers.multipleOfThree(99))
console.log('Is 998 in multiples of 3 set?', numbers.multipleOfThree(998))

得到如下结果:

5 的倍数

E 为 5 的倍数集,MultipleOfFive 为其特征函数。在数学中,5 的倍数是可被 5 整除的数。因此,MultipleOfFive 可以定义如下:

MultipleOfFive(x) = true if x is divisible by 5
MultipleOfFive(x) = false if x is not divisible by 5

因此,E 在 TypeScript 中的表示可以定义如下:

const multipleOfFive = (x: number) => x % 5 === 0

因此,运行下面的代码:

console.log('\nMultiples of 5 set:')
console.log('Is 15 in multiples of 5 set?', numbers.multipleOfFive(15))
console.log('Is 998 in multiples of 5 set?', numbers.multipleOfFive(998))

得到如下结果:

素数

很久以前,当我玩Project Euler 问题时,我不得不解决以下问题:

By listing the first six prime numbers: 2, 3, 5, 7, 11, and 13, 
we can see that the 6th prime is 13.
What is the 10 001st prime number?

为了解决这个问题,我首先需要编写一个快速算法来检查给定的数字是否为素数。一旦算法编写完成,我编写了一个迭代算法,该算法迭代素数直到找到第 10001 个素数。

E 为素数集,Prime 为其特征函数。在数学中,素数是大于 1 且除了 1 和自身以外没有正因数的自然数。因此,Prime 可以定义如下:

Prime(x) = true if x is prime
Prime(x) = false if x is not prime

因此,E 在 TypeScript 中的表示可以定义如下:

const prime = (x: number) => {
  if (x <= 1) return false
  if (x < 4) return true
  if (x % 2 === 0) return false
  if (x < 9) return true
  if (x % 3 === 0) return false
  const sqrt = Math.sqrt(x)
  for (let i = 5; i <= sqrt; i += 6) {
    if (x % i === 0) return false
    if (x % (i + 2) === 0) return false
  }
  return true
}

因此,运行下面的代码来解决我们的问题:

console.log('\nPrimes set:')
console.log('Is 2 in primes set?', numbers.prime(2))
console.log('Is 4 in primes set?', numbers.prime(4))
console.log('The 10 001st prime number is', numbers.getPrime(10001))

其中 getPrime 定义如下:

const getPrime = (p: number) => {
  for (let i = 1, count = 0; ; i++) {
    if (prime(i)) count++
    if (count === p) return i
  }
}

得到如下结果:

二元运算

本节介绍了一些用于从给定集合构建新集合以及操作集合的基本二元运算。下方是集合代数中的维恩图

Union

EF 为两个集合。EF并集,记为 E U F,是属于 EF 的所有元素的集合。

Union并集运算。因此,Union 运算可以在 TypeScript 中实现如下:

const union = <T>(e: Set<T>, f: Set<T>) => (x: T) => e(x) || f(x)

运行下面的代码:

console.log('\nUnion:')
console.log('Is 7 in the union of Even and Odd Integers Set?', core.union(numbers.even, numbers.odd)(7))

得到如下结果:

交集

EF 为两个集合。EF交集,记为 E n F,是同时属于 EF 的所有元素的集合。

Intersection交集运算。因此,Intersection 运算可以在 TypeScript 中实现如下:

const intersection = <T>(e: Set<T>, f: Set<T>) => (x: T) => e(x) && f(x)

运行下面的代码:

console.log('\nIntersection:')
const multiplesOfThreeAndFive = core.intersection(numbers.multipleOfThree, numbers.multipleOfFive)
console.log('Is 15 a multiple of 3 and 5?', multiplesOfThreeAndFive(15))
console.log('Is 10 a multiple of 3 and 5?', multiplesOfThreeAndFive(10))

得到如下结果:

笛卡尔积

EF 为两个集合。EF笛卡尔积,记为 E × F,是所有有序对 (e, f) 的集合,其中 e 属于 Ef 属于 F

CartesianProduct笛卡尔积运算。因此,CartesianProduct 运算可以在 TypeScript 中实现如下:

const cartesianProduct = <T1, T2>(e: Set<T1>, f: Set<T2>) => (x: T1, y: T2) => e(x) && f(y)

运行下面的代码:

console.log('\nCartesian Product:')
const cp = core.cartesianProduct(numbers.multipleOfThree, numbers.multipleOfFive)
console.log('Is (9, 15) in MultipleOfThree x MultipleOfFive? ', cp(9, 15))

得到如下结果:

补集

EF 为两个集合。EF相对补集,记为 E \ F,是属于 E 但不属于 F 的所有元素的集合。

Complement相对补集运算。因此,Complement 运算可以在 TypeScript 中实现如下:

const complement = <T>(e: Set<T>, f: Set<T>) => (x: T) => e(x) && !f(x)

运行下面的代码:

console.log('\nComplement:')
const c = core.complement(numbers.multipleOfThree, numbers.multipleOfFive)
console.log('Is 15 in MultipleOfThree \\ MultipleOfFive set? ', c(15))
console.log('Is 9 in MultipleOfThree \\ MultipleOfFive set? ', c(9))

得到如下结果:

对称差集

EF 为两个集合。EF对称差集,记为 E Δ F,是属于 EF 但不属于 EF 交集的元素集合。

SymmetricDifference对称差集运算。因此,SymmetricDifference 运算可以通过两种方式在 TypeScript 中实现。一种简单的方法是使用并集和补集运算,如下所示:

const symmetricDifferenceWithoutXor = <T>(e: Set<T>, f: Set<T>) => 
      (x: T) => union(complement<T>(e, f), complement(f, e))(x)

另一种方法是使用 XOR 二元运算,如下所示:

const symmetricDifferenceWithXor = <T>(e: Set<T>, f: Set<T>) => (x: T) => e(x) !== f(x)

运行下面的代码:

console.log('\nSymmetricDifference without XOR:')
const sdWithoutXor = core.symmetricDifferenceWithoutXor(numbers.prime, numbers.even)
console.log('Is 2 in the symetric difference of prime and even Sets? ', sdWithoutXor(2))
console.log('Is 4 in the symetric difference of prime and even Sets? ', sdWithoutXor(4))
console.log('Is 7 in the symetric difference of prime and even Sets? ', sdWithoutXor(7))

console.log('\nSymmetricDifference with XOR:')
const sdWithXor = core.symmetricDifferenceWithXor(numbers.prime, numbers.even)
console.log('Is 2 in the symetric difference of prime and even Sets? ', sdWithXor(2))
console.log('Is 4 in the symetric difference of prime and even Sets? ', sdWithXor(4))
console.log('Is 7 in the symetric difference of prime and even Sets? ', sdWithXor(7))

得到如下结果:

其他运算

本节介绍集合上其他有用的二元运算。

Contains

Contains 为检查元素是否属于集合的运算。此运算是一个函数,它以元素作为参数,如果元素在集合中则返回 true,否则返回 false

因此,此运算在 TypeScript 中定义如下:

const contains = <T>(e: Set<T>, x: T) => e(x)

因此,运行下面的代码:

console.log('\nContains:')
console.log('Is 7 in the singleton {0}? ', core.contains(common.singleton(0), 7))
console.log('Is 7 in the singleton {7}? ', core.contains(common.singleton(7), 7))

得到如下结果:

Add

Add 为向集合添加元素的运算。此运算是一个函数,它以元素作为参数并将其添加到集合中。

因此,此运算在 TypeScript 中定义如下:

const add = <T>(e: Set<T>, y: T) => (x: T) => x === y || e(x)

因此,运行下面的代码:

console.log('\nAdd:')
console.log('Is 7 in {0, 7}? ', core.add<number>(common.singleton(0), 7)(7))
console.log('Is 0 in {1, 0}? ', core.add<number>(common.singleton(1), 0)(0))
console.log('Is 7 in {19, 0}? ', core.add<number>(common.singleton(19), 0)(7))

得到如下结果:

移除

Remove 为从集合中移除元素的运算。此运算是一个函数,它以元素作为参数并将其从集合中移除。

因此,此运算在 TypeScript 中定义如下:

const remove = <T>(e: Set<T>, y: T) => (x: T) => x !== y && e(x)

因此,运行下面的代码:

console.log('\nRemove:')
console.log('Is 7 in {}? ', core.remove<number>(common.singleton(0), 0)(7))
console.log('Is 0 in {}? ', core.remove<number>(common.singleton(7), 7)(0))

得到如下结果:

对于想进一步深入的人

您可以看到通过函数式编程在 TypeScript 中进行集合代数有多么容易。在前面的章节中展示了最基本定义。但是,如果您想更进一步,可以考虑:

  • 集合上的关系
  • 抽象代数,例如幺半群、群、域、环、K-向量空间等
  • 容斥原理
  • 罗素悖论
  • 康托悖论
  • 对偶向量空间
  • 定理和推论

欧几里得平面

上一节中,在 TypeScript 中实现了集合上的基本概念。在本节中,我们将练习在欧几里得平面上实现的这些概念。

绘制圆盘

圆盘是边界由圆确定的平面子集。有两种类型的圆盘。闭圆盘包含构成其边界的圆上的点的圆盘,而开圆盘是不包含构成其边界的圆上的点的圆盘。

在本节中,我们将设置闭圆盘特征函数并在 HTML5 页面上绘制它。

为了设置特征函数,我们首先需要一个函数来计算平面上两点之间的欧几里得距离。该函数实现如下:

function distance(p1: Point, p2: Point) {
    return Math.sqrt((p1.x - p2.x) ** 2 + (p1.y - p2.y) ** 2)
}

其中 Point 定义如下:

class Point {
    x: number
    y: number

    constructor(x: number, y: number) {
        this.x = x
        this.y = y
    }
}

此公式基于勾股定理。

其中 c欧几里得距离(p1.X - p2.X)²(p1.Y - p2.Y)²

Disk 为闭圆盘的特征函数。在集合代数中,实数集上的闭圆盘的定义如下:

其中 ab 是圆心的坐标,R 是半径。

因此,Disk 在 TypeScript 中的实现如下:

const disk = (center: Point, radius: number) => (p: Point) => distance(p, center) <= radius

为了在 HTML5 页面中查看集合,我决定实现一个名为 draw 的函数,该函数在欧几里得平面上绘制一个集合。我选择了HTML5,因此使用了 canvas 元素进行绘制。

因此,我通过 draw 方法构建了下图所示的欧几里得平面

下方是平面的实现。

class Plane {
  width: number
  height: number

  constructor(width: number, height: number) {
    this.width = width
    this.height = height
  }

  draw(set: PlaneSet, canvasId: string) {
    const canvas = document.getElementById(canvasId) as HTMLCanvasElement
    if (!canvas) throw new Error(`Canvas with id ${canvasId} not found`)
    canvas.width = this.width
    canvas.height = this.height
    const context = canvas.getContext('2d') as CanvasRenderingContext2D
    const semiWidth = this.width / 2
    const semiHeight = this.height / 2
    const xMin = -semiWidth
    const xMax = semiWidth
    const yMin = -semiHeight
    const yMax = semiHeight
    for (let x = 0; x < this.width; x++) {
      const xp = xMin + (x * (xMax - xMin)) / this.width
      for (let y = 0; y < this.height; y++) {
        const yp = yMax - (y * (yMax - yMin)) / this.height
        if (set(new Point(xp, yp))) context.fillRect(x, y, 1, 1)
      }
    }
  }

  clear(canvasId: string) {
    const canvas = document.getElementById(canvasId) as HTMLCanvasElement
    if (!canvas) throw new Error(`Canvas with id ${canvasId} not found`)
    const context = canvas.getContext('2d') as CanvasRenderingContext2D
    context.clearRect(0, 0, this.width, this.height)
  }
}

draw 函数中,创建了一个与欧几里得平面容器具有相同宽度和高度的 canvas。然后,如果 canvas 中的每个像素点 (x,y) 属于 set,则将其替换为黑色点。xMinxMaxyMinyMax 是上图所示欧几里得平面的边界值。

运行下面的代码:

euclideanPlane = new Plane(200, 200)
euclideanPlane.draw(disk(new Point(0, 0), 50), 'disk')

其中 disk 是 canvas 的 id

<canvas id="disk"></canvas>

得到如下结果:

绘制水平和垂直半平面

水平垂直半平面是平面将欧几里得空间划分为的两部分之一。水平半平面是平面通过与Y轴垂直的直线将欧几里得空间划分为的两部分之一,如上图所示。垂直半平面是平面通过与X轴垂直的直线将欧几里得空间划分为的两部分之一。

在本节中,我们将设置水平垂直半平面的特征函数,在 HTML5 页面上绘制它们,并看看如果我们将其与圆盘子集组合会发生什么。

HorizontalHalfPlane水平半平面的特征函数HorizontalHalfPlane 在 TypeScript 中的实现如下:

const horizontalHalfPlane = (y: number, isLowerThan: boolean) => 
      (p: Point) => (isLowerThan ? p.y <= y : p.y >= y)

因此,运行下面的代码:

euclideanPlane.draw(horizontalHalfPlane(0, true),'hhp')

其中 hhp 是 canvas 的 id

<canvas id="hhp"></canvas>

得到如下结果:

VerticalHalfPlane垂直半平面的特征函数VerticalHalfPlane 在 TypeScript 中的实现如下:

const verticalHalfPlane = (x: number, isLowerThan: boolean) => 
      (p: Point) => (isLowerThan ? p.x <= x : p.x >= x)

因此,运行下面的代码:

euclideanPlane.draw(verticalHalfPlane(0, false),'vhp')

其中 vhd 是 canvas 的 id

<canvas id="vhd"></canvas>

得到如下结果:

在文章的第一部分,我们设置了集合上的基本二元运算。因此,例如,通过组合圆盘半平面的交集,我们可以绘制半圆盘子集。

因此,运行下面的示例:

euclideanPlane.draw(set.intersection(disk(new Point(0, 0), 50), 
                    verticalHalfPlane(0, false)), 'hd')

其中 hd 是 canvas 的 id

<canvas id="hd"></canvas>

得到如下结果:

函数

本节介绍欧几里得平面上集合的函数。

平移

translatePoint 为在平面上平移点的函数。在欧几里得几何中,translatePoint 是一个函数,它将给定点沿指定方向移动恒定距离。因此,TypeScript 中的实现如下:

const translatePoint = (deltax: number, deltay: number) => 
                       (p: Point) => new Point(p.x + deltax, p.y + deltay)

其中 (deltax, deltay) 是平移的恒定向量。

translate 为在平面上平移集合的函数。此函数在 TypeScript 中实现如下:

const translate = (e: PlaneSet, deltax: number, deltay: number) => 
                  (p: Point) => e(translatePoint(-deltax, -deltay)(p))

translate 接受 deltax(第一个欧几里得维度的增量距离)和 deltay(第二个欧几里得维度的增量距离)作为参数。如果点 P (x, y) 在集合 S 中被平移,则其坐标将变为 (x', y') = (x + deltax, y + deltay)。因此,点 (x' - deltax, y' - deltay) 始终属于集合 S。在集合代数中,translate 被称为同构,换句话说,所有平移的集合构成了平移群 T,它与空间本身同构。这解释了函数的主要逻辑。

因此,在我们的 HTML5 页面中运行下面的代码:

let translate_timer: ReturnType<typeof setInterval>
function translate_op() {
  let deltay = 0
  clearTimeout(scale_timer)
  clearTimeout(rotate_timer)
  translate_timer = setInterval(() => {
    deltay = deltay <= euclideanPlane.height ? deltay + 20 : 0
    euclideanPlane.draw(translate(disk(new Point(0, -50), 50), 0, deltay), 'ep_op')
  }, 1000)
}

其中 ep_op 是 canvas 的 id

<canvas id="ep_op"></canvas>

得到如下结果:

相似变换

scalePoint 为将任意点 M 映射到另一点 N 的函数,使得线段 SNSM 在同一条直线上,但按比例因子 λ 缩放。在集合代数中,Scale 的公式如下:

因此,TypeScript 中的实现如下:

const scalePoint = (lambdax: number, lambday: number, deltax: number, deltay: number) 
      => (p: Point) => new Point(lambdax * p.x + deltax, lambday * p.y + deltay)

其中 (deltax, deltay) 是平移的恒定向量,(lambdax, lambday) 是 lambda 向量。

scale 为在平面上对集合应用相似变换的函数。此函数在 TypeScript 中实现如下:

const scale = (e: PlaneSet, lambdax: number, lambday: number, deltax: number, 
               deltay: number) => (p: Point) => e(scalePoint(1 / lambdax, 1 / lambday, 
               -deltax / lambdax, -deltay / lambday)(p))

scale 接受 deltax(第一个欧几里得维度的增量距离)、deltay(第二个欧几里得维度的增量距离)和 (lambdax, lambday)(恒定因子向量 λ)作为参数。如果点 P (x, y) 通过 scale 在集合 S 中进行变换,则其坐标将变为 (x', y') = (lambdax * x + deltax, lambday * y + deltay)。因此,点 ((x' - deltax) / lambdax, (y' - deltay) / lambday) 始终属于集合 S,当然,前提是 lambda 不等于零向量。在集合代数中,scale 被称为同构,换句话说,所有相似变换的集合构成了相似变换群 H,它与空间本身 \ {0} 同构。这解释了函数的主要逻辑。

因此,在我们的 HTML5 页面中运行下面的代码:

let scale_timer: ReturnType<typeof setInterval>
function scale_op() {
  let deltay = 0
  let lambday = 0.05
  clearTimeout(translate_timer)
  clearTimeout(rotate_timer)
  scale_timer = setInterval(() => {
    deltay = deltay <= euclideanPlane.height ? deltay + 20 : 0
    lambday = deltay <= euclideanPlane.height ? lambday + 0.05 : 0.05
    euclideanPlane.draw(scale(disk(new Point(0, -50), 50), 1, lambday, 0, deltay), 'ep_op')
  }, 1000)
}

得到如下结果:

旋转

rotatePoint 为使点旋转角度 θ 的函数。在矩阵代数中,rotatePoint 的公式如下:

其中 (x', y') 是旋转后点的坐标,x'y' 的公式如下:

此公式的演示非常简单。可以看看这个旋转。

下方是演示。

因此,TypeScript 中的实现如下:

const rotatePoint = (theta: number) => (p: Point) => new Point(p.x * Math.cos(theta) 
      - p.y * Math.sin(theta), p.x * Math.sin(theta) + p.y * Math.cos(theta))

rotate 为在平面上以角度 θ 对集合应用旋转的函数。此函数在 TypeScript 中实现如下。

const rotate = (e: PlaneSet, theta: number) => (p: Point) => e(rotatePoint(-theta)(p))

rotate 是一个函数,它以 theta(旋转角度)作为参数。如果点 P (x, y) 通过 rotate 在集合 S 中进行变换,则其坐标将变为 (x', y') = (x * cos(theta) - y * sin(theta), x * sin(theta) + y * cos(theta))。因此,点 (x' * cos(theta) + y' * sin(theta), -x' * sin(theta) + y' * cos(theta)) 始终属于集合 S。在集合代数中,rotate 被称为同构,换句话说,所有旋转的集合构成了旋转群 R,它与空间本身同构。这解释了函数的主要逻辑。

因此,在我们的 HTML5 页面中运行下面的代码:

let rotate_timer: ReturnType<typeof setInterval>
function rotate_op() {
  let theta = 0
  clearTimeout(translate_timer)
  clearTimeout(scale_timer)
  rotate_timer = setInterval(() => {
    euclideanPlane.draw(rotate(horizontalHalfPlane(-90, true), theta), 'ep_op')
    theta = (theta + Math.PI / 2) % (2 * Math.PI)
  }, 1000)
}

得到如下结果:

对于想进一步深入的人

非常简单,不是吗?对于想进一步深入的人,您可以探索这些:

  • 椭圆
  • 三维欧几里得空间
  • 椭球
  • 抛物面
  • 双曲面
  • 球谐函数
  • 超椭球
  • 灶神星
  • 均质体
  • 焦点

分形

分形是具有通常超过其拓扑维度的分形维度的集合,并且可能介于整数之间。例如,曼德勃罗集是由一系列复二次多项式定义的 分形

Pc(z) = z^2 + c

其中 c 是一个复数。曼德勃罗集分形定义为所有点 c 的集合,使得上述序列不发散到无穷大。在集合代数中,它的表述如下:

分形(抽象数据类型)可以始终如下在 TypeScript 中表示:

type Fractal = (z: Complex, c: Complex) => Complex

复数与绘图

为了能够绘制分形,我需要处理复数。因此,我创建了下面的 Complex 类。

class Complex {
    x: number
    y: number
    static zero = new Complex(0, 0)

    constructor(x: number, y: number) {
        this.x = x
        this.y = y
    }

    abs() {
        return Math.sqrt(this.x * this.x + this.y * this.y)
    }

    toString() {
        return this.x + ' + i * ' + this.y
    }
}

function add(z1: Complex, z2: Complex) {
    return new Complex(z1.x + z2.x, z1.y + z2.y)
}

function substract(z1: Complex, z2: Complex) {
    return new Complex(z1.x - z2.x, z1.y - z2.y)
}

function multiply(z1: Complex, z2: Complex) {
    return new Complex(z1.x * z2.x - z1.y * z2.y, z1.x * z2.y + z1.y * z2.x)
}

曼德勃罗集分形

我创建了一个曼德勃罗集分形(抽象数据类型表示) P(z) = z^2 + c,如下所示。

const mandelbrot = (z: Complex, c: Complex) => add(multiply(z, z), c)

为了能够绘制复数,我创建了一个 ComplexPlane 类。下面是 TypeScript 中的实现。

class ComplexPlane {
  width: number
  height: number
  real_min: number
  real_max: number
  imaginary_min: number
  imaginary_max: number
  boundary: number
  fractalIterationsPerPixel: number
  canvasId: string

  constructor(
    width: number,
    height: number,
    real_min: number,
    real_max: number,
    imaginary_min: number,
    imaginary_max: number,
    boundary: number,
    fractalIterationsPerPixel: number,
    canvasId: string,
  ) {
    this.width = width
    this.height = height
    this.real_min = real_min
    this.real_max = real_max
    this.imaginary_min = imaginary_min
    this.imaginary_max = imaginary_max
    this.boundary = boundary
    this.fractalIterationsPerPixel = fractalIterationsPerPixel
    this.canvasId = canvasId
  }

  draw(fractal: Fractal) {
    const canvas = document.getElementById(this.canvasId) as HTMLCanvasElement
    canvas.width = this.width
    canvas.height = this.height
    const context = canvas.getContext('2d') as CanvasRenderingContext2D
    context.fillStyle = 'white'
    for (let x = 0; x < this.width; x++) {
      const xp = this.real_min + (x * (this.real_max - this.real_min)) / this.width
      for (let y = 0; y < this.height; y++) {
        const yp = this.imaginary_max - (y * (this.imaginary_max - this.imaginary_min)) / this.height
        const c = new Complex(xp, yp)
        let z = Complex.zero
        for (let k = 0; k < this.fractalIterationsPerPixel; k++) z = fractal(z, c)
        if (z.abs() < this.boundary) context.fillRect(x, y, 1, 1)
      }
    }
  }

  /*
   * Display 'Please wait...' at the center of the canvas
   *
  */
  pleaseWait() {
    const canvas = document.getElementById(this.canvasId) as HTMLCanvasElement
    canvas.width = this.width
    canvas.height = this.height
    const context = canvas.getContext('2d') as CanvasRenderingContext2D
    context.fillStyle = 'white'
    context.fillText('Please wait...', this.width / 2 - 30, this.height / 2)
  }
}

因此,运行下面的代码:

const complexPlane = new ComplexPlane(300, 300, -1.5, 1.5, -1.5, 1.5, 1.5, 20, 'fractal')

const mandelbrot = (z: Complex, c: Complex) => add(multiply(z, z), c)

complexPlane.pleaseWait()

setTimeout(() => complexPlane.draw(mandelbrot), 500)

其中 fractal 是 canvas 的 id

<canvas id="fractal"></canvas>

得到如下结果:

对于想进一步深入的人

对于想进一步深入的人,您可以探索这些:

  • 牛顿分形
  • 朱利亚分形
  • 其他分形

就是这样!希望您阅读愉快。

历史

  • 2023 年 10 月 20 日
    • 首次发布
  • 2023 年 10 月 27 日
    • 添加了 Babel 编译器
    • 添加了 eslint 预设
    • 更新了 Complex
    • 更新了 Fractal 类型
    • 更新了平面绘制和动画
    • 更新了分形绘制
  • 2024 年 4 月 11 日
    • 将 @babel/cli 从 7.23.4 升级到 7.24.1
    • 将 @babel/preset-env 从 7.23.3 升级到 7.24.4
    • 将 @types/node 从 20.10.0 升级到 20.12.7
    • 将 @typescript-eslint/eslint-plugin 从 6.12.0 升级到 7.6.0
    • 将 @typescript-eslint/parser 从 6.12.0 升级到 7.6.0
    • 将 eslint 从 8.54.0 升级到 9.0.0
    • 将 eslint-plugin-import 从 2.29.0 升级到 2.29.1
    • 将 npm-check-updates 从 16.14.11 升级到 16.14.18
    • 将 typescript 从 5.2.2 升级到 5.4.5
© . All rights reserved.