Tridash 0.8:使用函数式编程实现状态化应用程序





0/5 (0投票)
本文首先描述了状态管理问题及其当今编程语言提供的各种解决方案。最后一部分详细介绍了 Tridash 编程语言 0.8 版本提供的解决方案。
本文首先描述了状态管理问题及其当今编程语言提供的各种解决方案。最后一部分详细介绍了 Tridash 编程语言 0.8 版本提供的解决方案。该部分包含一个简单应用程序的完整代码示例,该应用程序包含一个每当按下按钮时都会递增的计数器。
状态管理问题
在开发具有交互式用户界面的应用程序时,您必须处理的核心问题之一是管理和同步应用程序所有组件的状态。
让我们来看一个简单的例子:一个应用程序,它接受用户输入的两个数字并显示它们的总和。该应用程序包含两个文本输入字段。每当字段中的值发生变化时,都应该重新计算并重新显示总和。此应用程序包含四个组件:
- 两个输入字段
- 显示结果总和的 UI 元素,即显示组件
- 负责计算总和的内部组件
内部组件需要知道输入字段中的值才能计算它们的总和。换句话说,内部组件依赖于两个输入字段的状态。显示总和的 UI 元素需要知道总和是多少,因此依赖于负责计算总和的内部组件的状态。
当输入字段中的值发生更改时,需要通知内部组件此更改以便重新计算总和。同样,需要通知显示组件新的总和,以便显示它。这种响应用户事件的状态更新包含了状态管理问题。
解决方案
当今的编程语言提供了许多常见的状态管理问题解决方案。
命令式编程
最基本的解决方案是命令式编程语言提供的。命令式语言(如 Java 或 C/C++)可以直接访问底层内存。这意味着程序员可以很少受限制地读写内存。在命令式语言中,对象引用内存中的一个位置,在该位置存储可以修改的值或一系列值。对该对象的修改将对同一对象的所有引用可见。实际上,内存构成了应用程序的状态。
使用命令式编程,总和应用程序的典型实现涉及为两个字段附加事件侦听器,每当它们的值发生变化时都会调用这些侦听器。在事件侦听器中,从内存中读取文本字段中输入的值,并将其存储在一个变量中,该变量是对另一个内存位置的引用,该内存位置可被内部总和组件访问。调用一个过程来重新计算总和。此过程读取存储文本字段中输入值的内部变量的值,并计算新的总和。最后,必须将此新总和写入内存,在其中存储显示给用户的该值。
这种方法的问题在于,应用程序状态在所有组件之间的同步完全取决于程序员。这很快就变得重复,应用程序逻辑被埋在状态更新和同步代码的层下面。
这种方法对于应用程序规范的更改也很不灵活。例如,考虑我们要增强应用程序,以便如果总和超过用户提供的限制,将显示“超出限制”消息。本质上,这涉及到添加一个新的应用程序组件。为此,我们必须修改内部组件的代码,该组件负责计算总和,以便将总和的变化通知新组件。
最后,这种方法容易出现错误,即如果一个组件没有被正确通知另一个组件的状态变化,应用程序状态就不会被正确更新。当添加多线程以防止用户界面因长时间运行的计算或 IO 任务而变得无响应时,会进一步产生错误的空间。现在程序员必须担心在多个线程之间同步应用程序状态。
函数式编程
函数式编程与命令式编程不同,因为它不提供内存的概念。在函数式编程语言中,应用程序由纯函数组成,纯函数接受一系列输入并产生一个输出。然后,此输出被馈送到进一步的函数,这些函数产生进一步的输出。在函数式编程语言中,对象不引用存储值的内存位置,而是对象本身就是值。对象不能被修改,只能创建新对象。通过避免内存,函数式编程完全避免了应用程序状态。没有状态,无法实现与用户在运行时交互的应用程序。
为了解决此限制,整个应用程序被构建为其输入的纯函数,输入是用户事件的无限流。该函数将此流映射到结果应用程序状态的流。正是这个输出流被用户观察。
使用这种方法,总和应用程序将被实现为一个函数,该函数以两个文本字段的值作为输入,并输出一个显示两个值总和的应用程序对象。每当用户与应用程序交互时(即,当文本字段中的值发生更改时)都会调用此函数,并显示结果应用程序对象,该对象与前一个应用程序对象相同,唯一的区别是总和已更新。
这种方法改进了命令式方法,因为它使应用程序代码仅由应用程序逻辑本身组成。没有状态更新或状态同步代码,因为根本没有状态。这种方法的问题在于,本质上是一个状态化问题(应用程序本身)必须人为地扭曲成一个事件流上的纯函数。程序员无法再从当前时间点的应用程序的角度思考,而是必须考虑计算任何时刻应用程序的实际状态。
函数式响应式编程
第三种解决方案,也是 Tridash 所采用的方法,与第二种密切相关,但引入了内存的概念。与其完全避免内存和状态,不如让每个应用程序组件(在 Tridash 中称为节点)拥有一个状态。但是,与命令式方法不同,此状态不能被任意修改。相反,每个组件的状态被指定为其依赖组件状态的纯函数。每当组件状态发生变化时,依赖于该组件的所有组件的状态都会被重新计算。在 Tridash 中,组件(节点)与其依赖项之间的这种关系被称为绑定。
因此,总和应用程序的实现就是声明每个组件,声明内部总和组件的值是字段中输入值的总和,并声明显示为输出的值是总和组件的值。
在 Tridash 中,应用程序的全部内容(不包括用户界面)都可以通过以下方式实现:
a <- input-a.value b <- input-b.value sum <- a + b output <- sum
其中 `<-` 运算符表示左侧节点的值绑定到右侧节点/表达式的值。
函数式响应式编程具有常规函数式编程的优点,但提供了一个更自然、更易于状态化的应用程序视图。程序员无需考虑输入如何映射到应用程序对象的内部表示,以及该对象如何映射到输出(用户界面的状态),而是可以从“此组件的状态如何与其他组件的状态相关?”的角度进行思考。
然而,函数式响应式编程 (FRP) 的缺点是,无法干净地指定一个组件依赖于其 past states。例如,实现一个计数器,每次按下按钮时该计数器增加一,这是不可能的。当需要将组件的 past state 映射到新 state 时,大多数 FRP 系统只会退化为常规函数式编程的事件流方法。
Tridash 0.8 中的节点状态和状态化绑定
Tridash 0.8 引入了两项新功能——节点状态和状态化绑定——它们允许将组件(节点)的 past state 映射到其当前状态,而无需离开函数式响应式编程范例。
在 Tridash 中,绑定使用 `->` 运算符或 `<-` 运算符(如上所示)建立,它们是同一个运算符,但参数顺序相反。
a -> b
在此示例中,在节点 `a` 和节点 `b` 之间建立了一个绑定。每当 `a` 的值发生变化时,`b` 的值都会自动更新为 `a` 的值。
然而,这不允许将 previous state 映射到 current state,即 `a` 不能是 `b` 的函数。使用以下方法实现计数器将不起作用:
counter + 1 -> counter
原因在于,这指定了每当 `counter + 1` 的值发生变化时,`counter` 的值就会更新为它。但是,当 `counter` 的值更新时,`counter + 1` 的值也会被重新计算和更新。这导致 `counter` 的值再次更新,从而导致 `counter + 1` 再次重新计算。显而易见,当其依赖项(在此情况下为 `counter + 1`)的值发生变化时,更新 `counter` 值的简单规则是不够的。我们需要一种方法来告诉语言我们何时实际希望更新 `counter` 的值。
节点状态允许为节点在特定时间点分配一个显式的命名状态。状态由符号标识符命名。状态化绑定是一种绑定,仅当节点切换到特定命名状态时才会生效。当绑定中的观察节点(`->` 运算符的右侧节点)使用 `::` 运算符显式指定状态时,就会建立状态化绑定。
a -> b :: state
在此示例中,建立了一个状态化绑定,当 `b` 切换到标识符为 `state` 的状态时生效。 `a` 可以是 `b` 的函数,因为绑定仅在 `b` 的状态变为 `state` 时生效,而不是在 `a` 的值更改时生效。
可以使用以下状态化绑定实现计数器:
counter + 1 -> counter :: increment
因此,当节点 `counter` 的状态变为 `increment` 时,它就会被递增。
剩下要做的就是实际设置 `counter` 的状态。是什么决定了节点的 it state?很简单,特殊节点 `/state(node)` 的值,其中 `node` 是节点标识符。要设置 `counter` 的状态,我们只需与 `/state(counter)` 建立绑定作为观察者。
我们希望每当按下按钮时,`counter` 都会递增。假设我们有另一个节点 `clicked?`,当按钮按下时其值为 true,当按钮释放时其值为 false。因此,我们可以将 `/state(counter)` 绑定到一个表达式,当 `clicked?` 为 true 时,该表达式计算为字面符号 `increment`,当 `clicked?` 为 false 时,计算为其他值,例如符号 `default`。以下是我们所需的表达式(`/state(counter)` 绑定到该表达式):
case ( clicked? : '(increment), '(default) ) -> /state(counter)
`'` 运算符简单地返回其参数作为字面符号,类似于 Lisp 中的 `'` 或 `quote` 运算符。
用户界面
应用程序逻辑已完成,唯一缺少的是用户界面。可以使用以下 HTML 定义界面:
<div>Value of counter is <?@ counter ?></div> <button id="increment">Increment</button>
在当前版本的 Tridash 中,`clicked?` 节点的值必须通过 JavaScript 手动设置,在按钮被点击时。在下一个版本中,您将能够绑定到一个节点,当按下按钮时该节点会自动设置为 true,当释放按钮时设置为 false。
为了能够从 JavaScript 引用 `clicked?` 节点,需要以下属性声明:
/attribute(clicked?, input, 1) /attribute(clicked?, public-name, "is_clicked")
这指定 `clicked?` 是一个输入节点,并为其分配了标识符 `is_clicked`,JavaScript 使用该标识符引用它。
需要以下 JavaScript 脚本标签来为按钮附加事件侦听器并设置节点的值:
<script> var button = document.getElementById('increment'); button.addEventListener('click', function() { Tridash.nodes.is_clicked.set_value(true); Tridash.nodes.is_clicked.set_value(false); }); </script>
第一行获取 HTML `button` 元素的引用。其余代码附加 `click` 事件的事件侦听器,并将 `clicked?` 节点的值设置为 true,然后立即设置为 false。我们之所以在设置为 true 之后立即设置为 false,是为了模拟 `button.clicked?` 节点的行为,该节点将在下一个版本中添加。
为了将所有内容放在一起,我们将 Tridash 代码放在 HTML 文件顶部的 Tridash 代码标签中:
<? /import(core) counter + 1 -> counter :: increment case ( clicked? : '(increment), '(default) ) -> /state(counter) # Initial Value of Counter 0 -> counter # Attribute for reference `clicked?` node from JavaScript /attribute(clicked?, input, 1) /attribute(clicked?, public-name, "is_clicked") ?> <!doctype html> <html> <head> <title>Counter</title> </head> <body> <h1>Counter</h1> <div>Value of counter is <?@ counter ?></div> <button id="increment">Increment</button> <script> var button = document.getElementById('increment'); button.addEventListener('click', function() { Tridash.nodes.is_clicked.set_value(true); Tridash.nodes.is_clicked.set_value(false); }); </script> </body> </html>
这是使用函数式响应式编程范例实现的完整应用程序(不包括 JS 脚本标签,该标签仅在当前版本中是必需的)。
以下命令构建应用程序:
tridashc counter.html : node-name=ui -o app.html -p type=html -p main-ui=ui
这是初始状态的快照(`counter` 被赋予初始值 `0`):
按下Increment一次后
再次按下Increment后
结论
Tridash 0.8 添加了两个新颖的功能,它们扩展了简单的函数式响应式编程范例,允许实现状态化应用程序,其中应用程序组件的状态依赖于其 past state 而非另一个组件的状态。这保留了函数式编程的所有优点,但提供了应用程序状态的自然模型。
有关 Tridash 的更多信息(安装说明、文档教程),请访问 https://alex-gutev.github.io/tridash/。
有关更详细、功能更丰富的示例,以及状态化绑定与显式From和To状态的示例,请访问:https://alex-gutev.github.io/tridash/tutorials/ar01s10.html。