C#中的函数式编程
通过代数、数字、欧几里得平面和分形的C#函数式编程
目录
介绍
函数式编程是一种基于函数、它们的组合以及分解为函数的编程范式。
函数有两个可能的属性:
- 纯度:函数的结果严格依赖于它们的参数,没有其他外部影响。纯度导致了封装、局部化、稳定性和确定性。
- 一等公民身份:函数具有值的状态。函数可以命名、赋值、类型化、按需创建、作为函数的参数传递、是函数的结果,并存储在数据结构中。一等公民身份导致了灵活使用和组合性。
函数式编程就是利用这两个属性之一和/或另一个。
本文不会讨论函数式编程的基础知识,因为您可以在互联网上找到关于此主题的众多资源。相反,它将讨论应用于代数、数字、欧几里德平面和分形的C#函数式编程。本文提供的示例从简单到复杂,但都以简单、直接和易于理解的方式进行说明。
通过函数表示数据
设S
为a
、b
、c
等元素(例如桌子上的书、YouTube上的视频、欧几里德平面上的点)的一组元素,设S'
为这些元素的任何子集(例如桌子上的绿色书籍、YouTube上的文化视频、以原点为圆心半径为1的圆上的点)。
集合S'
的特征函数S'(x)
是一个将每个元素x
与true
或false
关联的函数。
如果x在S'中,则S'(x)= true。如果x不在S'中,则S'(x)= false。
假设S
为桌子上的书籍,S'
为桌子上的绿色书籍。设a
和b
为两本绿色书籍,c
和d
为桌子上的两本红色书籍。那么:
S'(a) = S'(b) = trueS'(c) = S'(d) = false
假设 S
是 YouTube 上的视频集合,假设 S'
是 YouTube 上的文化视频集合。假设 a
和 b
是 YouTube 上的两个文化视频,c
和 d
是 YouTube 上的两个非文化视频。那么:
S'(a) = S'(b) = trueS'(c) = S'(d) = false
假设 S
是欧几里得平面上的点的集合,假设 S'
是以欧几里得平面原点(0,0)为中心的半径为1的圆上的点的集合(单位圆)。假设 a
和 b
是单位圆上的两个点,假设 c
和 d
是以欧几里得平面原点为中心的半径为2的圆上的两个点。那么:
S'(a) = S'(b) = trueS'(c) = S'(d) = false
因此,任何集合 S'
都可以通过其特征函数进行表示。特征函数以一个元素作为参数,如果这个元素在 S'
中,则返回 true
,否则返回 false
。换句话说,一个集合(抽象数据类型)可以通过 C# 中的 Predicate
来表示。
Predicate<T> set;
在接下来的章节中,我们将看到如何用函数式的方式来表示集合代数中的一些基本集合,然后我们将定义集合上的通用二元操作。然后,我们将在数字和欧几里得平面的子集上应用这些操作。集合是抽象数据结构,数字的子集和欧几里得平面的子集是抽象数据结构的表示,二元操作是适用于任何抽象数据结构表示的通用逻辑。
集合
本节介绍了如何通过C#来表示集合代数中的一些基本集合。
空集
假设 E
是空集,Empty
是它的特征函数。在集合代数中,E
是唯一没有元素的集合。因此,Empty
可以定义如下:
Empty(x) = false 如果 x 在 E 中Empty(x) = false 如果 x 不在 E 中
因此,E
在 C# 中的表示可以定义如下:
public static Predicate<T> Empty<T>() => _ => false;
在集合代数中,空集表示如下:
因此,运行下面的代码:
Console.WriteLine("\n空集合:");Console.WriteLine("7 是否在 {{}} 中? {0}", Empty<int>()(7));
会得到以下结果:
全集
假设 S
是一个集合,S'
是包含所有元素的子集,All
是它的特征函数。在集合代数中,S'
是包含所有元素的全集。因此,All
可以定义如下:
All(x) = true 如果 x 在 S 中
因此,S'
在 C# 中的表示可以定义如下:
public static Predicate<T> All<T>() => _ => true;
在集合代数中,全集的表示如下:
因此,运行下面的代码:
Console.WriteLine("7 是否在整数集合中? {0}", All<int>()(7));
给出以下结果:
单例集
假设 E
是单例集,Singleton
是其特征函数。在集合代数中,E
也被称为单位集,或一元组,是一个只有一个元素 e
的集合。因此,Singleton
可以定义如下:
如果 x 是 e,那么 Singleton(x) = true如果 x 不是 e,那么 Singleton(x) = false
所以,C# 中表示 E
的定义如下:
public static Predicate<T> Singleton<T>(T e) where T : notnull => x => e.Equals(x);
因此,运行以下代码:
Console.WriteLine("在单例集 {{0}} 中是否存在 7? {0}", Singleton(0)(7));Console.WriteLine("在单例集 {{7}} 中是否存在 7? {0}", Singleton(7)(7));
将得到以下结果:
其他集合
本节介绍整数集的子集。
偶数
假设 E
是偶数集,Even
是其特征函数。在数学中,偶数是可以被 2 整除的数。因此,Even
可以定义如下:
如果 x 是 2 的倍数,那么 Even(x) = true如果 x 不是 2 的倍数,那么 Even(x) = false
所以,C# 中表示 E
的定义如下:
Predicate<int> even = i => i % 2 == 0;
因此,运行以下代码:
Console.WriteLine("数字 {0} 是偶数吗? {1}", 99, even(99));Console.WriteLine("数字 {0} 是偶数吗? {1}", 998, even(998));
将得到以下结果:
奇数
假设 E
是奇数集,Odd
是其特征函数。在数学中,奇数是不可以被 2 整除的数。因此,Odd
可以定义如下:
如果 x 不是 2 的倍数,那么 Odd(x) = true如果 x 是 2 的倍数,那么 Odd(x) = false
所以,C# 中表示 E
的定义如下:
Predicate<int> odd = i => i % 2 == 1;
因此,运行以下代码:
Console.WriteLine("数字 {0} 是奇数吗? {1}", 99, odd(99));Console.WriteLine("数字 {0} 是奇数吗? {1}", 998, odd(998));
将得到以下结果:
3 的倍数
假设 E
是 3 的倍数集,MultipleOfThree
是其特征函数。在数学中,一个数如果可以被 3 整除,那么它就是 3 的倍数。因此,MultipleOfThree
可以定义如下:
如果 x 可以被 3 整除,那么 MultipleOfThree(x) = true如果 x 不可被 3 整除,那么 MultipleOfThree(x) = false
所以,C# 中表示 E
的定义如下:
Predicate<int> multipleOfThree = i => i % 3 == 0;
因此,运行以下代码:
Console.WriteLine("数字 {0} 是 3 的倍数吗? {1}", 99, multipleOfThree(99));Console.WriteLine("数字 {0} 是 3 的倍数吗? {1}", 998, multipleOfThree(998));
将得到以下结果:
5 的倍数
让 E
是5的倍数集合,MultipleOfFive
是它的特征函数。在数学中,5的倍数是可被5整除的数字。因此,MultipleOfFive
可以定义如下:
MultipleOfFive(x) = true,如果 x 可被5整除MultipleOfFive(x) = false,如果 x 不能被5整除
因此,在C#中,可以如下定义 E
的表示:
Predicate<int> multipleOfFive = i => i % 5 == 0;
因此,运行以下代码:
Console.WriteLine("是否为5的倍数?{1}", 15, multipleOfFive(15));Console.WriteLine("是否为5的倍数?{1}", 998, multipleOfFive(998));
将得到以下结果:
素数
很久以前,当我在解决 Project Euler 问题时,我需要解决以下问题:
列出前六个素数:2, 3, 5, 7, 11 和 13,我们可以看到第6个素数是13。第10001个素数是多少?
为了解决这个问题,我首先需要编写一个快速算法来检查一个给定的数字是否为素数。一旦算法编写完毕,我编写了一个迭代算法,遍历素数直到找到第10001个素数。然而,是否真的需要下一个迭代算法?你会看到的。
检查一个给定的数字是否为素数的算法是素数集合的特征函数。
让 E
是素数集合,Prime
是它的特征函数。在数学中,素数是大于1且除了1和自身没有其他正因数的自然数。因此,Prime
可以定义如下:
Prime(x) = true,如果 x 是素数Prime(x) = false,如果 x 不是素数
因此,在C#中,可以如下定义 E
的表示:
Predicate<int> prime = IsPrime;
其中 IsPrime
是一个检查一个给定的数字是否为素数的方法。
static bool IsPrime(int i){ if (i == 1) return false; // 1 不是素数 if (i < 4) return true; // 2 和 3 是素数 if ((i >> 1) * 2 == i) return false; // 2的倍数不是素数 if (i < 9) return true; // 5 和 7 是素数 if (i % 3 == 0) return false; // 3的倍数不是素数 // 如果找到一个小于或等于 sqrt(i) 的除数,则 i 不是素数 int sqrt = (int)Math.Sqrt(i); for (int d = 5; d <= sqrt; d += 6) { if (i % d == 0) return false; if (i % (d + 2) == 0) return false; } // 否则 i 是素数 return true;}
因此,运行以下代码以解决我们的问题:
int p = Primes(prime).Skip(10000).First();Console.WriteLine("第10001个素数是{0}", p);
其中 Primes
定义如下:
static IEnumerable <int> Primes(Predicate<int> prime){ yield return 2; int p = 3; while (true) { if (prime(p)) yield return p; p += 2; }}
将得到以下结果:
二进制运算
本节介绍了从给定集合构建新集合并操作集合的若干基本操作。以下是集合代数中的文氏图。
并集
设 E
和 F
是两个集合。并集 E U F
表示同时属于集合 E
和集合 F
的元素的集合。
设 Union
是并集操作。因此,在 C# 中可以实现 Union
操作如下:
public static Predicate<T> Union<T>(this Predicate<T> e, Predicate<T> f) => x => e(x) || f(x);
如你所见,Union
是作用在一个集合的特征函数上的扩展函数。所有操作将被定义为作用在一个集合的特征函数上的扩展函数。因此,运行如下代码:
Console.WriteLine("7 是否属于偶数和奇数集合的并集? {0}", Even.Union(Odd)(7));
将得到以下结果:
交集
设 E
和 F
是两个集合。交集 E n F
表示同时属于集合 E
和集合 F
的元素的集合。
设 Intersection
是交集操作。因此,在 C# 中可以实现 Intersection
操作如下:
public static Predicate<T> Intersection<T>(this Predicate<T> e, Predicate<T> f) => x => e(x) && f(x);
如你所见,Intersection
是作用在一个集合的特征函数上的扩展函数。因此,运行如下代码:
Predicate<int> multiplesOfThreeAndFive = multipleOfThree.Intersection(multipleOfFive);Console.WriteLine("15 是否是 3 和 5 的倍数? {0}", multiplesOfThreeAndFive(15));Console.WriteLine("10 是否是 3 和 5 的倍数? {0}", multiplesOfThreeAndFive(10));
将得到以下结果:
笛卡尔积
设 E
和 F
是两个集合。笛卡尔积 E × F
表示所有有序对 (e, f)
的集合,其中 e
是集合 E
的成员,f
是集合 F
的成员。
设 CartesianProduct
是笛卡尔积操作。因此,在 C# 中可以实现 CartesianProduct
操作如下:
public static Func<T1, T2, bool> CartesianProduct<T1, T2>(this Predicate<T1> e, Predicate<T2> f) => (x, y) => e(x) && f(y);
如你所见,CartesianProduct
是作用在一个集合的特征函数上的扩展函数。因此,运行如下代码:
Func<int, int, bool> cartesianProduct = multipleOfThree.CartesianProduct(multipleOfFive);Console.WriteLine("(9, 15) 是否属于 MultipleOfThree x MultipleOfFive? {0}", cartesianProduct(9, 15));
将得到以下结果:
补集
设 E
和 F
是两个集合。相对补集 E \ F
表示属于集合 E
但不属于集合 F
的元素的集合。
让 Complement
是相对补集操作。因此,Complement
操作可以在 C# 中实现如下:
public static Predicate<T> Complement<T>(this Predicate<T> e, Predicate<T> f) => x => e(x) && !f(x);
正如你所看到的,Complement
是一个对集合的特征函数的扩展方法。因此,运行下面的代码:
Console.WriteLine("15 是否在 MultipleOfThree \\ MultipleOfFive 集合中? {0}", multipleOfThree.Complement(multipleOfFive)(15));Console.WriteLine("9 是否在 MultipleOfThree \\ MultipleOfFive 集合中? {0}", multipleOfThree.Complement(multipleOfFive)(9));
将得到以下结果:
对称差
假设 E
和 F
是两个集合。集合 E
和 F
的对称差,记为 E Δ F
,是只属于 E
或只属于 F
但不属于 E
和 F
的所有元素的集合。
让 SymmetricDifference
是对称差操作。因此,SymmetricDifference
操作在 C# 中有两种实现方式。一种常规的方式是使用并集和补集操作,如下所示:
public static Predicate<T> SymmetricDifferenceWithoutXor<T>(this Predicate<T> e, Predicate<T> f) => Union(e.Complement(f), f.Complement(e));
另一种方式是使用 XOR
二进制操作,如下所示:
public static Predicate<T> SymmetricDifferenceWithXor<T>(this Predicate<T> e, Predicate<T> f) => x => e(x) ^ f(x);
正如你所看到的,SymmetricDifferenceWithoutXor
和 SymmetricDifferenceWithXor
是对集合的特征函数的扩展方法。因此,运行下面的代码:
// 不使用 XOR 的对称差Console.WriteLine("\n不使用 XOR 的对称差:");Predicate<int> sdWithoutXor = prime.SymmetricDifferenceWithoutXor(even);Console.WriteLine("2 是否在 prime 和 even 集合的对称差中? {0}", sdWithoutXor(2));Console.WriteLine("4 是否在 prime 和 even 集合的对称差中? {0}", sdWithoutXor(4));Console.WriteLine("7 是否在 prime 和 even 集合的对称差中? {0}", sdWithoutXor(7));// 使用 XOR 的对称差Console.WriteLine("\n使用 XOR 的对称差:");Predicate<int> sdWithXor = prime.SymmetricDifferenceWithXor(even);Console.WriteLine("2 是否在 prime 和 even 集合的对称差中? {0}", sdWithXor(2));Console.WriteLine("4 是否在 prime 和 even 集合的对称差中? {0}", sdWithXor(4));Console.WriteLine("7 是否在 prime 和 even 集合的对称差中? {0}", sdWithXor(7));
将得到以下结果:
其他操作
本节介绍集合上的其他有用的二进制操作。
Contains
让 Contains
是检查一个元素是否在集合中的操作。该操作是对集合的特征函数的扩展函数,它以一个元素作为参数,如果该元素在集合中则返回 true
,否则返回 false
。
因此,在 C# 中,该操作的定义如下:
public static bool Contains<T>(this Predicate<T> e, T x) => e(x);
因此,运行下面的代码:
Console.WriteLine("7 是否在单例集合 {{0}} 中? {0}", Singleton(0).Contains(7));Console.WriteLine("7 是否在单例集合 {{7}} 中? {0}", Singleton(7).Contains(7));
给出以下结果:
添加
让添加
成为向集合中添加元素的操作。该操作是集合的特征函数上的扩展函数,它以一个元素为参数,并将其添加到集合中。
因此,在C#中,该操作的定义如下:
public static Predicate<T> Add<T>(this Predicate<T> s, T e) where T : notnull => x => x.Equals(e) || s(x);
因此,运行下面的代码:
Console.WriteLine("在 {{0, 7}} 中是否存在 7? {0}", Singleton(0).Add(7)(7));Console.WriteLine("在 {{1, 0}} 中是否存在 0? {0}", Singleton(1).Add(0)(0));Console.WriteLine("在 {{19, 0}} 中是否存在 7? {0}", Singleton(19).Add(0)(7));
给出以下结果:
移除
让移除
成为从集合中移除元素的操作。该操作是集合的特征函数上的扩展函数,它以一个元素为参数,并将其从集合中移除。
因此,在C#中,该操作的定义如下:
public static Predicate<T> Remove<T>(this Predicate<T> s, T e) where T : notnull => x => !x.Equals(e) && s(x);
因此,运行下面的代码:
Console.WriteLine("在 {{}} 中是否存在 7? {0}", Singleton(0).Remove(0)(7));Console.WriteLine("在 {{}} 中是否存在 0? {0}", Singleton(7).Remove(7)(0));
给出以下结果:
对于那些想要进一步学习的人
你可以看到我们可以通过函数式编程在C#中轻松进行一些集合代数运算。前面的章节展示了最基本的定义。但是,如果你想进一步学习,可以思考以下内容:
- 集合上的关系
- 抽象代数,例如幺半群、群、域、环、K-向量空间等
- 包含-排除原理
- 罗素悖论
- 康托尔悖论
- 对偶向量空间
- 定理和推论
欧几里得平面
在前一节中,集合的基本概念在C#中得到了实现。在本节中,我们将在平面点集(欧几里得平面)上实践实现的概念。
绘制一个圆盘
圆盘是平面的一个子集,由一个圆界定。有两种类型的圆盘。封闭圆盘是包含构成其边界的圆点的圆盘,开放圆盘是不包含构成其边界的圆点的圆盘。
在本节中,我们将建立闭合圆盘的特征函数并在WPF中进行绘制。
要建立特征函数,我们首先需要一个计算平面上两点之间的欧几里得距离的函数。该函数实现如下:
private static double EuclidianDistance(Point point1, Point point2) => Math.Sqrt(Math.Pow(point1.X - point2.X, 2) + Math.Pow(point1.Y - point2.Y, 2));
其中Point
是System.Windows
命名空间中定义的struct
。该公式基于勾股定理。
其中c
是欧几里得距离,a²
是(point1.X - point2.X)²
,b²
是(point1.Y - point2.Y)²
。
让Disk
成为封闭圆盘的特征函数。在集合代数中,实数集合上封闭圆盘的定义如下:
其中a
和b
是圆心的坐标,R
是半径。
因此,在C#中实现Disk
如下:
public static Predicate<Point> Disk(Point center, double radius) => p => EuclidianDistance(center, p) <= radius;
为了查看结果中的集合,我决定实现一个名为Draw
的函数,该函数在欧几里得平面上绘制一个集合。我选择了WPF,因此使用System.Windows.Controls.Image
作为画布,Bitmap
作为上下文。
因此,我通过Draw
方法构建了以下所示的欧几里得平面。
以下是该方法的实现。
public static void Draw(this Predicate<Point> set, Image plane){ var bitmap = new Bitmap((int)plane.Width, (int)plane.Height); // // 绘制图形 // double semiWidth = plane.Width / 2; double semiHeight = plane.Height / 2; double xMin = -semiWidth; double xMax = +semiWidth; double yMin = -semiHeight; double yMax = +semiHeight; for (int x = 0; x < bitmap.Height; x++) { double xp = xMin + x * (xMax - xMin) / plane.Width; for (int y = 0; y < bitmap.Width; y++) { double yp = yMax - y * (yMax - yMin) / plane.Height; if (set(new Point(xp, yp))) { bitmap.SetPixel(x, y, Color.Black); } } } plane.Source = Imaging.CreateBitmapSourceFromHBitmap( bitmap.GetHbitmap(), IntPtr.Zero, Int32Rect.Empty, BitmapSizeOptions.FromWidthAndHeight(bitmap.Width, bitmap.Height));}
在Draw
方法中,创建一个与欧几里得平面容器具有相同宽度和高度的bitmap
。然后,将bitmap
中像素点(x,y)
处的每个点替换为黑色点,如果它属于set
。在上述欧几里得平面图中,xMin
、xMax
、yMin
和yMax
是边界值。
正如你所看到的,Draw
是集合中点的Characteristic函数的扩展函数。因此,运行以下代码:
Plane.Disk(new Point(0, 0), 20).Draw(PlaneCanvas);
将得到以下结果:
绘制水平和垂直半平面
水平或垂直半平面是平面将欧几里得空间划分为两个部分的两个子集。水平半平面是平面通过与Y轴垂直的线将欧几里得空间划分为两个部分,就像上图中所示。垂直半平面是平面通过与X轴垂直的线将欧几里得空间划分为两个部分。
在本节中,我们将构建水平和垂直半平面的Characteristic函数,并在WPF中绘制它们,看看如果将它们与圆盘子集结合使用,会发生什么。
设HorizontalHalfPlane
为水平半平面的Characteristic函数。在C#中,HorizontalHalfPlane
的实现如下:
public static Predicate<Point> HorizontalHalfPlane(double y, bool lowerThan) => p => lowerThan ? p.Y <= y : p.Y >= y;
因此,运行以下代码:
Plane.HorizontalHalfPlane(0, true).Draw(PlaneCanvas);
将得到以下结果:
设VerticalHalfPlane
为垂直半平面的Characteristic函数。在C#中,VerticalHalfPlane
的实现如下:
public static Predicate<Point> VerticalHalfPlane(double x, bool lowerThan) => p => lowerThan ? p.X <= x : p.X >= x;
因此,运行以下代码:
Plane.VerticalHalfPlane(0, false).Draw(PlaneCanvas);
将得到以下结果:
在文章的第一部分中,我们建立了关于集合的基本二元运算。因此,通过将disk
和half-plane
的交集组合起来,我们可以画出半圆盘。
因此,运行以下示例:
Plane.VerticalHalfPlane(0, false).Intersection(Plane.Disk(new Point(0, 0), 20)).Draw(PlaneCanvas);
将得到以下结果:
函数
该部分介绍了欧几里德平面上的函数。
Translate
设Translate
为将平面上的一个点进行平移的函数。在欧几里德几何中,Translate
是将给定点按照指定的方向平移一个常数距离的函数。因此,C#的实现如下:
private static Func<Point, Point> Translate(double deltax, double deltay) => p => new Point(p.X + deltax, p.Y + deltay);
其中(deltax, deltay)
是平移的常矢量。
设TranslateSet
为将平面上的一个集合进行平移的函数。这个函数在C#中简单的实现如下:
public static Predicate<Point> TranslateSet (this Predicate<Point> set, double deltax, double deltay) => x => set(Translate(-deltax, -deltay)(x));
TranslateSet
是集合的扩展函数。它接受参数deltax
,即第一个欧几里德维度上的增量距离,以及deltay
,即第二个欧几里德维度上的增量距离。如果集合S中的一个点P (x, y)被平移,那么它的坐标将改变为(x’, y’) = (x + delatx, y + deltay)。因此,点(x’ – delatx, y’ – deltay)将始终属于集合S。在集合代数中,TranslateSet
被称为同构,换句话说,所有平移的集合构成了平移群T,它与空间本身同构。这解释了函数的主要逻辑。
因此,在我们的WPF应用程序中运行以下代码:
TranslateDiskAnimation();
其中TranslateDiskAnimation
的说明如下:
private const double Delta = 50;private double _diskDeltay;private readonly Predicate<Point> _disk = Plane.Disk(new Point(0, -170), 80);private void TranslateDiskAnimation(){ DispatcherTimer diskTimer = new DispatcherTimer { Interval = new TimeSpan(0, 0, 0, 1, 0) }; diskTimer.Tick += TranslateTimer_Tick; diskTimer.Start();}private void TranslateTimer_Tick(object? sender, EventArgs e){ _diskDeltay = _diskDeltay <= plan.Height ? _diskDeltay + Delta : Delta; Predicate<Point> translatedDisk = _diskDeltay <= plan.Height ? _disk.TranslateSet(0, _diskDeltay) : _disk; translatedDisk.Draw(PlaneCanvas);}
将得到以下结果:
相似变换
设Scale
为将任意点M映射到另一个点N的函数,使得线段SN与线段SM在同一直线上,但按照因子λ进行缩放。在集合代数中,Scale
的公式如下:
因此,C#的实现如下:
private static Func<Point, Point> Scale (double deltax, double deltay, double lambdax, double lambday) => p => new Point(lambdax * p.X + deltax, lambday * p.Y + deltay);
其中,(deltax, deltay)
是平移的恒定向量,(lambdax, lambday)
是向量λ。
假设ScaleSet
是在平面上应用等比缩放的函数。在C#中,该函数的简单实现如下:
public static Predicate<Point> ScaleSet (this Predicate<Point> set, double deltax, double deltay, double lambdax, double lambday) => x => set(Scale(-deltax / lambdax, -deltay / lambday, 1 / lambdax, 1 / lambday)(x));
ScaleSet
是应用在集合上的扩展函数。它接受参数deltax
,表示在第一个欧几里得维度上的位移距离,deltay
,表示在第二个欧几里得维度上的位移距离,以及向量(lambdax, lambday)
,表示常量因子向量λ。如果点P (x, y)通过ScaleSet
在集合S中进行变换,其坐标将变为(x’, y’) = (lambdax * x + deltax, lambday * y + deltay)。因此,点((x’- deltax) / lambdax, (y’ – deltay) / lambday)将始终属于集合S,前提是λ不等于向量0。在集合的代数中,ScaleSet
被称为同构,换句话说,所有等比缩放的集合构成了等比缩放群H,它与空间本身\ {0}同构。这解释了该函数的主要逻辑。
因此,在我们的WPF应用程序中运行以下代码:
ScaleDiskAnimation();
其中,ScaleDiskAnimation
的具体描述如下:
private const double Delta = 50;private double _lambdaFactor = 1;private double _diskScaleDeltay;private readonly Predicate<Point> _disk2 = Plane.Disk(new Point(0, -230), 20);private void ScaleDiskAnimation(){ DispatcherTimer scaleTimer = new DispatcherTimer { Interval = new TimeSpan(0, 0, 0, 1, 0) }; scaleTimer.Tick += ScaleTimer_Tick; scaleTimer.Start();}private void ScaleTimer_Tick(object? sender, EventArgs e){ _diskScaleDeltay = _diskScaleDeltay <= plan.Height ? _diskScaleDeltay + Delta : Delta; _lambdaFactor = _diskScaleDeltay <= plan.Height ? _lambdaFactor + 0.5 : 1; Predicate<Point> scaledDisk = _diskScaleDeltay <= plan.Height ? _disk2.ScaleSet(0, _diskScaleDeltay, _lambdaFactor, 1) : _disk2; scaledDisk.Draw(PlaneCanvas);}
将得到以下结果:
旋转
假设Rotation
是将点以角度θ旋转的函数。在矩阵代数中,Rotation
的形式如下:
其中(x’, y’)是旋转后点的坐标,x’和y’的计算公式如下:
这个公式的演示非常简单。看看这个旋转。
下面是演示:
因此,在C#中实现如下:
private static Func<Point, Point> Rotate(double theta) => p => new Point(p.X * Math.Cos(theta) - p.Y * Math.Sin(theta), p.X * Math.Sin(theta) + p.Y * Math.Cos(theta));
假设RotateSet
是在平面上应用旋转的函数,以角度θ为参数。在C#中,该函数的简单实现如下:
public static Predicate<Point> RotateSet(this Predicate<Point> set, double theta) => p => set(Rotate(-theta)(p));
RotateSet
是对集合进行扩展的函数。它的参数是旋转的角度 theta
。如果将点P(x,y)通过 RotateSet
转换到集合S中,那么它的坐标将会变成(x’,y’)=(x * cos(θ) – y * sin(θ),x * cos(θ) + y * sin(θ))。因此,点(x’ * cos(θ) + y’ * sin(θ),x’ * cos(θ) – y’ * sin(θ))将始终属于集合S。在集合代数中, RotateSet
被称为同构,换句话说,所有旋转的集合构成旋转群R,该群同构于空间本身。这解释了函数的主要逻辑。
因此,在我们的WPF应用程序中运行以下代码:
RotateHalfPlaneAnimation();
其中 RotateHalfPlaneAnimation
如下所述:
private double _theta; private const double TWO_PI = 2 * Math.PI; private const double HALF_PI = Math.PI / 2; private readonly Predicate< Point & gt; _halfPlane = Plane.VerticalHalfPlane(220, false); private void RotateHalfPlaneAnimation() { DispatcherTimer rotateTimer = new DispatcherTimer { 间隔=新时间间隔(0、0、0、1、0)}; rotateTimer.Tick += RotateTimer_Tick; rotateTimer.Start(); } private void RotateTimer_Tick(object? sender, EventArgs e) { _halfPlane.RotateSet(_theta).Draw(PlaneCanvas); _theta += HALF_PI; _theta %= TWO_PI; }
给出以下结果:
对于那些想更进一步的人
很简单,不是吗?对于那些想更进一步的人,您可以探索以下内容:
- 椭圆
- 三维欧几里得空间
- 椭球体
- 抛物线
- 双曲线
- 球谐函数
- 超椭球体
- 哈曼
- 同源
- 眼点
分形
分形是具有分形维数的集合,其分形维数通常超过其拓扑维数,并且可能介于整数之间。例如,Mandelbrot集是由一族复二次多项式定义的分形:
Pc(z)= z ^ 2 + c
其中 c
是一个复数。 Mandelbrot分形被定义为所有点 c
,使得上述序列不会逃逸到无穷远。在集合代数中,这被表示为:
如上图所示的Mandelbrot集。
分形(抽象数据类型)可以始终表示为C#中的以下形式:
松饼
复数和绘图
为了能够绘制分形,我需要操作复数。因此,我使用了 Meta.numerics
库。我还需要一个在 Bitmap
中绘制复数的实用程序,因此我使用了 ColorMap
和 ClorTriplet
类,这些类在这个CodeProject 文章中可用。
Mandelbrot分形
我创建了一个Mandelbrot(抽象数据类型表示) P(z)= z ^ 2 + c
,如下所示。
public static Func MandelbrotFractal()=>(c,z)=> z * z + c;
为了能够绘制复数,我需要更新绘制
函数。因此,我创建了一个使用 ColorMap
和 ClorTriplet
类的 Draw
函数的重载。以下是C#中的实现。
public static void Draw(this Func fractal,Image plane){ var bitmap = new Bitmap((int)plane.Width,(int)plane.Height); const double reMin = -3.0; const double reMax = +3.0; const double imMin = -3.0; const double imMax = +3.0; for(int x = 0; x <plane.Width; x ++){ double re = reMin + x *(reMax-reMin)/ plane.Width; for(int y = 0; y <plane.Height; y ++){ double im = imMax-y *(imMax-imMin)/ plane.Height; var z = new Complex(re,im); Complex fz = fractal(z); if(Double.IsInfinity(fz.Re)|| Double.IsNaN(fz.Re)|| Double.IsInfinity(fz.Im) || Double.IsNaN(fz.Im)){ continue; } ColorTriplet hsv = ColorMap.ComplexToHsv(fz); ColorTriplet rgb = ColorMap.HsvToRgb(hsv); var r =(int)Math.Truncate(255.0 * rgb.X); var g =(int)Math.Truncate(255.0 * rgb.Y); var b =(int)Math.Truncate(255.0 * rgb.Z); Color color = Color.FromArgb(r,g,b); bitmap.SetPixel(x,y,color); } } plane.Source = Imaging.CreateBitmapSourceFromHBitmap(bitmap.GetHbitmap(),IntPtr.Zero,Int32Rect.Empty,BitmapSizeOptions.FromWidthAndHeight( bitmap.Width,bitmap.Height)); }
因此,运行下面的代码:
Plane.MandelbrotFractal().Draw(PlaneCanvas, 20, 1.5);
将得到以下结果:
牛顿分形
我还创建了一个牛顿分形(抽象数据类型表示)P(z) = z^3 - 2*z + 2
,如下所示:
public static Func<Complex, Complex> NewtonFractal() => z => z * z * z - 2 * z + 2;
因此,运行下面的代码:
Plane.NewtonFractal().Draw(PlaneCanvas);
将得到以下结果:
进一步探索
对于那些希望深入了解的人,您可以探索以下内容:
- 曼德博集分形
- 朱利亚分形
- 其他牛顿分形
- 其他分形
懒惰的介绍
在本节中,我们将看到如何使一个类型变为懒惰。
懒惰评估是一种评估策略,它延迟表达式的评估直到其值被需要,同时避免重复评估。共享可以通过指数因子减少某些函数的运行时间,相对于其他非严格评估策略,例如按名称调用。以下是懒惰评估的好处。
- 通过避免不必要的计算和评估复合表达式中的错误条件来提高性能
- 能够构建可能无限大的数据结构:例如,我们可以通过函数轻松创建一个无限的整数集合(请参阅“集合”部分中的质数示例)
- 能够将控制流(结构)定义为抽象而不是原始的
让我们来看看下面的代码:
public class MyLazy<T>{ #region Fields private readonly Func<T> _f; private bool _hasValue; private T? _value; #endregion #region Constructors public MyLazy(Func<T> f) { _f = f; } #endregion #region Operators // // 通过隐式关键字,将类型为MyLazy<T>的对象用作类型为T的对象 // public static implicit operator T?(MyLazy<T?> lazy) { if (!lazy._hasValue) { lazy._value = lazy._f(); lazy._hasValue = true; } return lazy._value; } #endregion}
MyLazy<T>
是一个泛型类,包含以下字段:
_f
: 用于惰性评估的函数,返回类型为T
的值_value
: 类型为T
的值(冻结的值)_hasValue
: 一个布尔值,指示是否计算了该值
为了将类型为MyLazy<T>
的对象作为类型为T
的对象使用,使用了implicit
关键字。评估是在类型转换时进行的,这个操作被称为解冻。
因此,运行下面的代码:
var myLazyRandom = new MyLazy<double>(GetRandomNumber);double myRandomX = myLazyRandom;Console.WriteLine("\n 带有MyLazy<double>的随机数: {0}", myRandomX);
其中GetRandomNumber
返回一个随机的double
,如下所示:
static double GetRandomNumber() => new Random().NextDouble();
将得到以下输出:
.NET Framework 4 引入了一个用于惰性评估的类System.Lazy<T>
。该类通过属性Value
返回值。运行下面的代码:
var lazyRandom = new Lazy<double>(GetRandomNumber);double randomX = lazyRandom;
将此HTML (保持HTML代码在结果中) 翻译成中文:
由于类型 Lazy<T>
与类型 double
不同,因此会导致编译错误。
要使用类 System.Lazy<T>
的值,必须按如下方式使用属性 Value
:
var lazyRandom = new Lazy<double>(GetRandomNumber);double randomX = lazyRandom.Value;Console.WriteLine("\n 使用 System.Lazy<double>.Value 的随机数: {0}", randomX);
这将产生以下输出:
.NET Framework 4 还引入了 ThreadLocal
和 LazyInitializer
用于惰性评估。
运行源代码
要运行源代码,您需要安装 Visual Studio 2022 和 .NET 7.0 SDK。
以下是解决方案中可用的项目:
Functional.Core
是一个包含函数和辅助方法的类库。Functional.Core.WPF
是一个包含平面和分形函数和辅助方法的 WPF 类库。Functional.EuclideanPlane
是一个包含欧几里德平面和分形示例的 WPF 应用程序。Functional.Laziness
是一个包含惰性评估示例的控制台应用程序。Functional.Set
是一个包含集合示例的控制台应用程序。
就是这样!希望您喜欢阅读。
参考资料
历史记录
- 2023年10月19日:首次发布
- 2023年11月21日:内容更新
Leave a Reply