你懂单曲吗?我不知道,所以我想我应该给你解释一下。
那么,一旦你拿到了,我会重新解释Lucid的。没有哈斯克尔,可选的分类理论,无麸质。
当时的想法是,程序是一组方程,描述了一组随时间变化的变量。最初的想法是通过减少编程对数学的依赖来使正式的程序验证变得更容易-程序只是一组方程式。然而,项目迅速扩展,直到我们意识到我们已经发明了一种函数式数据流语言。
除了这种被认为是函数式的语言外,它还有很多奇怪的特点。首先,这纯粹是第一个订单(尽管这一问题后来得到了部分解决)。更奇怪的是,变量表示数据对象的无限序列,以及像逐点加法运算这样的数据操作。
更奇怪的是,我们发现函数有两个不同的概念。最一般的,我们简称为“函数”的是任意的(但可计算的)流转换。我们称之为“映射”的更专业,使用随时间索引变化的规则逐点转换流。同样,也有两种子计算的概念,一种是通用的,另一种是逐点的。我们分别将这些构造称为“产生”和“计算”。
(根据lambda演算符号,这对应于两个lambdas和两个“let”构造。在通用版本中,变量被绑定到任意流,在专用版本中,变量被绑定到单个数据对象)。
Wadler将两个lambdas和两个let之间的区别描述为按值调用和按名称调用。我认为这是准确的。我们将值函数(映射)调用称为“同步”,因为时间t时f(X)的值仅取决于t和时间t时X的值。
然而,在当时,这种区别被认为是“混乱和奇怪的”,我们放弃了call by value WHERE子句(相当于let)。
我们几乎不知道这些奇怪的现象是使用单体的结果。既然他们还没有被介绍给计算机科学,我们怎么能这样做呢?
关于Monad的教程很多,Phil Wadler的函数式编程的精髓是最好的教程之一,但我没有发现其中任何一个特别容易理解。原因之一是它们需要熟悉Haskell类型和范畴理论,这是我宁愿避免的。另一个原因是他们认为动机是把顺序性和副作用带回函数式编程。不用了谢谢。埃德和我发明了Lucid,允许编程不按顺序进行。如果我想做顺序编程,我会用C编写。
所以我想出了一种避免哈斯克尔和(大多数)范畴论的单音方法。我还放弃了这样的假设,即我们想要编写带有副作用的命令式I/O。
单体的基本思想是,对于每个数据域D,我们都有一个派生域D*,它包括D,并且在某种意义上概括了元素。一个要记住的例子是,让D*成为D的所有元素集的集合。
这将我们带到单数的第一个组件,即从D到D*的嵌入(我也称之为*)。这确保可以在D*中“找到”D中的每个单独的数据对象。例如,如果d是3,而我们处理的是集合单元数,则d*是{3}。这个映射的存在保证了D在某种意义上是D*的子域。
第二个属性是*on domain基本上是一个闭包运算符:D**与D*相同。不是字面上的:对于集合单体,D**是D的元素集合集合的域,与D*不同。第二个属性保证的是将D**映射到D*的折叠运算符V。在集合的情况下,是并集运算符将集合转换为其元素的并集。因此,{{2,3},{4,5,6},{2,6,7}}V是{2,3,4,5,6,7}。
请注意,例如,3**({{3}})折叠为3*({3})。这种情况必须一直存在。
其次,塌陷必须是联想的。D*有两种方式:AS(D**)*和(D*)**。结果是两种方式将其折叠为D*,即(D**)*to(D**)*to D**to D**to(D*)*to(D*)*to D*.。这些必须产生相同的结果。
例如,对于集合,D*的元素是集合的集合。它折叠(双向)为所有元素“出现”在任何位置的集合。
第三个性质是,对于任何域D和E以及从D到E的任何函数f,都存在一个从D*到E*的函数f*。我们可以把f*(S)看作映射(f,s)。这些函数必须组成;如果f:D->;E和g:E->;F,则(f*|g*)=(f|g)*,(“|”是Unix管道的反向组合)。
对于集合单体,f*以元素方式应用于其参数的元素。如果s是{2,3,5},且f是平方函数,则f*(S)={4,9,25)。
此外,*必须遵守D到D*和E到E*的嵌入:F*(d*)=f(D)*。
最后,*on函数必须很好地处理折叠。如果s‘在D**中,则有另一种计算f**(s’)V的方法,即先将s‘折叠成s,然后取f*(S)。这必须产生相同的结果。在符号中,f**(s‘)V=f*(s’V)。
就集合而言,f**取一组集合,并以元素方式将f应用于“较低”的集合。收拢结果与收拢集合的集合并按元素应用f相同。此规则(警告:范畴论!)。认为塌缩是从函数者**到函数者*的自然转换。
这就把我们带到了我们最喜欢的单子,溪流单子。流是一个无限的序列。
数据对象的。在流中,单数D*是D上所有流的集合。给定D中的d,d*是常量流
给定f从D到E,f*从D*到E*按点工作:如果D*中的s为。
最后,从D**到D*的折叠运算符取对角线;如果D**中的s‘为。
<;a 0,a 1,a 2,a 3,…。>;,<;b 0,b 1,b 2,b 3,…。>;,<;c 0,c 1,c 2,c 3,…。>;,…。
单子定律很容易检查,流单子并不比集合单子复杂。特别地,D*到D**的折叠都给出了三维立方体的对角线。
对流单元上的λ演算的非标准解释完全是清晰的,使用了有争议的嵌套循环结构(我将在另一次进行讨论)。
集合单子,就像流单子一样,决定了对λ演算的非标准解释。在这种解释中,D*的元素不是流,而是D的非确定性元素。D*的每个元素都是一组可能性。因此,如果D*中的s是{1,3,5},则它是一个1、3或5的值。
给定D的元素d(例如,3),d*是{3},这意味着一个值肯定是3。如果s如上所述,并且f是平方函数,则f*(S)是s-{1,9,25}的可能值的平方。换句话说,要获得f*(S)的值,您需要获得s的值,然后对其应用f。
如果s‘在D**中,则它是D的非确定性非确定性元素。折叠成D*意味着取这些非确定性元素中的一个,然后取它的一个值。这意味着“%s”向其成员的联盟崩溃。
例如,检查一元定律并验证两个不确定的D值之和是否具有所有可能的召唤值对并不难。
那么,按点工作的同步函数的类比是什么呢?这些函数一次只检查一种可能性。因此,接受其参数的值并返回其平方根之一的函数符合条件。另一方面,接受两个值并返回其间一个值的函数则不会。
为了使语言变得有趣,我们需要额外的基元,比如fby,它们不是f*的形式。对于集合,我们可以将fby(s,t)定义为s和t的并,这是McCarthy的amb函数。
集合单体的一种变体是用0到1之间的数字标记集合中的每个元素。那么D*是标记加起来为1的所有标记元素集合。我们可以将D*的元素看作D上的随机变量(如果我们只取有限集合,它会简化事情)。
给定d,d*是唯一元素是d的集合,标记为1。折叠是并集,除非内部标记和外部标记相乘。函数f*(S)取s的元素d,并以相同的概率返回f(D)。两个操作数的和是成对求和的集合,每个和用和的标签的乘积进行标记。此外,我们可能需要合并:如果不同的和产生相同的结果,则添加单独的标记。
与SYNCHRONCE的类比类似于集合单体的类比,只有一个样本规则。平方根函数就是一个例子,每个根的概率为50/50。
对于fby的类比,我们可以将fby(s,t)定义为50%s和50%t。
另一个可能的单数是D的元素的D*Be(有限)序列。我们可以将这些元素看作迭代器,它们根据重复的要求按顺序产生序列值。
D**的元素是序列序列,通过按顺序连接序列来折叠。我们可以将它们视为产生迭代器的迭代器。折叠意味着依次生成迭代器,然后单独运行它们。两个迭代器的和按字典顺序产生所有成对和。
F*(S)是将f应用于序列s的每个元素的结果。在操作上,它意味着运行s并将f应用于产生的所有内容。
同步表示g(S)的第n个输出仅取决于n和s的第n个输出。计数器是同步的。
我们可以将fby(s,t)定义为s的第一个元素,简写为t。作为迭代器,它运行s一次,发送值,然后变为t。
在这个单子中,D*的一个元素是用一个自然数来标记的D的一个元素,我们可以认为它是一个到达时间(时钟滴答)。
值d*是用0标记的d。折叠D**意味着将D的双标记元素转换为D的单标记元素。我们取两个标记中的最大值。
F*(S)的值是将f应用于s的数据部分并保持标签不变的结果。我们等待s到达,然后立即应用f。
两个标签值的总和是它们用两个标记中的最大值标记的值的总和。我们等待操作数到达,一旦最后一个操作数出现,立即将它们相加。
按值调用函数对其参数的数据值进行操作,可能会增加延迟。
非f*原语是race(x,y),它返回具有最小标记的参数-第一个到达的参数。
这是一个广为人知的单曲,在大多数教程中都有它的特点。D*是添加了单个错误对象的D的副本。例如,N*可能是错误的自然数。也许是…。一个自然数。
给定D中的任意d,d*是D*中d的像。如果f将D映射到E,则f*将D*中D的元素映射到E*中f(D)的相应元素,并将ERROR映射为ERROR。D**上的折叠将D的图像映射到自身,两个错误都映射到D*中的单个错误。如果D*的两个元素都在D的像中,则D*的两个元素的和就是它们的正态和,否则就是错误的。
那么,IO Monad是什么,它们中最著名的是什么呢?对很多人来说,单曲的全部意义是什么?
这一点还远不清楚,我已经努力了一段时间来确定这一点。Wadler在“Essence”中定义了一个输出Monad,其中D*的元素是D的元素和字符串。显然,我们的想法是字符串是在生成D的元素时生成的输出。
元素d*与空字符串一起是d。函数f*对数据组件进行操作,并传递字符串。从D**到D*的折叠将连接与D**中的值相关联的两个字符串。D*的两个元素的总和是它们的值与两个输出字符串的连接的总和。按值调用函数转换数据并添加一些输出。
这是我最好的机会了。我不明白的是副作用从何而来。我已经讨论过的其他单药不会产生副作用。*。我问过应该知道的人,得到了不同的答案。最常见的解释是,Haskell人员设置了实现的诱杀部分,并将其直接连接到输出。谁知道呢?
在未来的帖子中,我将详细介绍Lucid的I/O方法,它既不使用副作用,也不使用IO monad。