当前位置: 首页 > article >正文

在 MoonBit 中引入 Elm 架构:用简单原则打造健壮的 Web 应用

在这里插入图片描述

Elm 是一种纯函数式编程语言,专为构建前端 Web 应用程序而设计。它编译为 JavaScript,强调简洁性、性能和健壮性。

纯函数式的含义是函数没有副作用,这使得代码更易于理解和调试。通过强大的静态类型检查,Elm 确保应用程序不会抛出运行时异常,从而提供更高的可靠性。Elm 架构强制执行单向数据流,使状态管理变得可预测且简单。

受 Elm 的启发,我们使用 MoonBit 开发了一个名为 Rabbit-TEA 的前端 Web 框架。

为什么选择 MoonBit?

函数式编程的兴起也推动了 React 从基于类的范式向 React Hooks 的演进。在 JavaScript 中已经有了一些函数式框架,那么为什么还需要 MoonBit?

尽管 JavaScript 生态中已经有一些支持函数式风格的框架和库,但 JavaScript 本身缺乏诸如模式匹配等特性,这使得编码体验不够理想。以下是 MoonBit 和 JavaScript 等效代码的对比:

let x = match y {a => value1b => value2c => value3
}
f(x)
var x = null;
if (y == a) { x = value1 }
else if (y == b) { x = value2 }
else { x = value3 }
// x 可能会在这里被意外修改!
return f(x);// 或者
const x = y == a ? value1 : (y == b ? value2 : value3);
return f(x);

以下是另一个 MoonBit 例子。函数 validate_utf8 检查输入的 bytes 是否是有效的 UTF-8 序列:

fn validate_utf8(bytes : Bytes) -> Bool {loop bytes {[0x00..=0x7F, ..xs]| [0xC0..=0xDF, 0x80..=0xBF, ..xs]| [0xE0..=0xEF, 0x80..=0xBF, 0x80..=0xBF, ..xs]| [0xF0..=0xF7, 0x80..=0xBF, 0x80..=0xBF, 0x80..=0xBF, ..xs] => continue xs[_, ..] => false[] => true}
}

MoonBit 是一种面向表达式的语言,变量默认是不可变的。它完全支持模式匹配这一在函数式编程语言中流行已久的特性,确保生成高效的 JavaScript 代码。此外,MoonBit 编译器支持多种后端,包括 JavaScript、WebAssembly 和 native 后端。

Rabbit-TEA 如何工作

TEA 架构非常的简单,它由 Model、View、Update 组成:

  • Model:App 的状态,由不可变的数据结构组成。
  • View:App 的视图,声明了如何将 Model 显示为 HTML、用户的操作触发什么消息。
  • Update:App 的逻辑,处理用户触发的消息,将旧的 Model 转换为新的 Model。

下面是一个完整的计数器程序:

typealias Model = Int
let model = 0enum Msg {IncrementDecrement
}fn update(msg : Msg, model : Model) -> (Command[Msg], Model) {match msg {Increment => (none(), model + 1)Decrement => (none(), model - 1)}
}fn view(model : Model) -> Html[Msg] {div([h1([text(model.to_string())]),button(click=Msg::Increment, [text("+")]), // button1button(click=Msg::Decrement, [text("-")]), // button2])
}fn main {@tea.startup(model~, update~, view~)
}

这个程序除了定义 Model、Update 和 View,还定义了 Msg 类型。Msg 类型类似于事件。当用户点击增加按钮时,会触发运行时向 update 函数发送 Increment 消息以及当前的 model。在 update 函数中,并不会直接修改 model,而是基于旧值创建一个新的 model 并返回。整个函数是无副作用的。最后,新的 model 将通过 view 函数渲染为 HTML。

update 函数还返回一个 Cmd 类型,它表示一个未执行的操作。我们稍后会对此进行介绍。

声明式视图: HTML EDSL

EDSL 即内嵌领域特定语言,是通过利用编程语言已有的语法结构设计的 DSL,不需要额外的预处理和编译步骤。

Rabbit-TEA 使用 MoonBit 的 labeled argument 语法糖定义了一组辅助函数,这些 HTML 函数尽可能地通过类型来提示用户参数的作用, 避免滥用模糊和令人困惑的 String 类型。它们使用起来就像:

fn view() -> Html[Msg] {div([h1([text("hello MoonBit")]),p([text("build robust app")]),a(href="moonbitlang.com", target=Blank, [text("try it")]),])
}

在这里插入图片描述

以标签 <a> 为例子,它的辅助函数的类型定义如下。除了唯一的 childrens 参数,其他所有参数都是 labeled argument,labeled argument 根据需要可以声明为可选的,或者设置缺省值。

pub(all) enum Target {SelfBlank
}pub fn a[M](style~ : Array[String] = [],id? : String,class? : String,href~ : String,target~ : Target = Self,childrens : Array[Html[M]]
) -> Html[M]

在未来,我们将考虑在 MoonBit 中引入类似 JSX 的拓展语法,使视图的代码拥有更好的可读性。

消息与模式匹配

TEA 架构充分利用了模式匹配和带标签联合类型,用户定义的 Msg 可以视情况带上额外的数据,这使得 update 处理消息时更加安全和灵活。

例如,对于一个 input 元素,假如希望在用户输入的同时,处理它的值并将结果显示在另一个元素 p 中。我们可以在 Msg 类型中增加一个带 String 数据的 enum constructor,GotInputChangeGotInputChange 的类型和 inputchange 参数的类型都是 (String) -> Msg。用户在修改 input 的值时,input 的值会被打包进 GotInputChange 并发送给 update 函数处理。

enum Msg {GotInputChange(String)
}fn update(msg : Msg, model : String) -> (Cmd[Msg], String) {match msg {GotInputChange(value) => {...(none(), value)}}
}fn view(model : String) -> Html[Msg] {div([p([text(model)])input(input_type=Text, value=model, change=Msg::GotInputChange),])
}

依靠 MoonBit 的穷尽性检查,下次 Msg 增加新的 enum constructor 时,IDE 将会提示相关的 pattern matching 需要处理新的 case,减少运行时错误。

管理副作用

Rabbit-TEA 与 Elm 一样使用 Cmd 类型管理函数的副作用,也就是修改外部状态、与外部系统交互的操作。
例如,请求浏览器记录 URL History、滚动浏览器视图、向服务器发送 JSON 请求并处理返回的数据。
所有的这些操作被封装为 Cmd。Cmd 代表一个未执行的动作,只有当它被 Update 函数的返回时,才会被 Rabbit-TEA 的 runtime 触发执行。

下面的例子演示了在用户点击相关的卡片时,先将界面转换为加载状态,然后通过 http 请求加载卡片的内容。
@http.get 返回了一个 Cmd,而它的第一个参数是请求的 URL,第二个参数声明期望的数据格式和处理响应的方法。

enum Msg {GotCardText(Result[String, String])ClickCard(Int)
}pub fn update(msg : Msg, model : Model) -> (Cmd[Msg], Model) {match msg {ClickCard(id) => (@http.get("/api/card/\{id}", expect=Text(GotCardText)), Loading)GotCardText(Ok(text)) => (none(), Editing(text))GotCardText(Err(_)) => (none(), NotFound)}
}

这样一来,服务器响应请求时将触发 GotCardText 这个消息。它带上了一个 Result[String,String] 类型,
这提示我们对响应进行分类讨论:当成功返回 card text 时,我们更新 model 以便 view 向用户展示内容;当请求失败时,我们跳转到 NotFound 页面。

Cmd 模式鼓励用户避免在一个函数中交替地触发副作用和更新 Model,从而防止 update 和 view 同时存在两个 Model 的数据来源,保证程序状态的一致性。

与 JS 世界交互

上面提到的这些 Cmd 并非经过框架特殊处理,而是基于 Cmd 的接口实现的。你也可以定义自己的 Cmd 来让 Rabbit-TEA 和外部的 JS 世界交互。MoonBit 允许以内联的形式书写 JS 代码,并将它和函数定义绑定。这很简单:

extern "js" fn set_timeout(f : () -> Unit, ms : Int) = "(f,ms) => setTimeout(f, ms)"

set_timeout 绑定了 JS 的函数 setTimeout。参数 f 表示将要触发的动作,ms 表示触发前等待的时间。
我们定义一个函数 delay。它接受的参数 msg 表示将要触发的消息,ms 作用同上,最终返回 Cmd

fn delay[M](msg : M, ms : Int) -> Cmd[M] {Cmd(fn(events){set_timeout(fn(){ events.trigger_update(msg) }, ms)})
} 

Cmd 类型的值可以通过同名的 Cmd 构造器创建,它是一个未执行的动作的包装。在这个包装中,我们让 set_timeout 在合适的时机调用 events.trigger_update(msg),让运行时使用 msg 和当前的 model 调用 update 函数。

我们可以使用这个 delay 函数,在用户点击按钮的5秒后显示一段文本:

enum Msg {Timeout(String)SetTime
}fn update(msg : Msg, model : String) -> (Cmd[Msg], String) {match msg {Timeout(tips) => (none(), tips)SetTime => (delay(Timeout("You clicked the button 5s ago."), 5000), "")}
}fn view(model : String) -> Html[Msg] {div([text(model),button(click=Msg::SetTime, [text("show tips after 5s")]),])
}

不过,在编写内联 JS 时需要小心运行时错误。我们计划在 Rabbit-TEA 中提供更丰富的 Cmd,用户一般不需要自己编写这些绑定。

轻量的运行时

我们开发 Rabbit-TEA 时并没有注意过编译生成的 js 文件的大小。得益于 MoonBit 的全局 DCE 优化,我们发现 Rabbit-TEA 在带上 virtual dom 和 MoonBit 的标准库的同时,一个计数器应用生成的代码仅 33KB(经过 minify,未 gzip),小于 vue 的 50~60kb。

结论

在实现 Rabbit-TEA 的同时,我们还用它重写 MoonBit 的包管理网站 mooncakes.io。目前 mooncakes.io 和 Rabbit-TEA 还不够完善。我们希望随着 mooncakes.io 的实际应用,Rabbit-TEA 也能够成为成熟可靠的 UI 框架。我们未来也计划探索 TEA 架构的更多可能性,例如 native 后端支持、服务端渲染、时间旅行调试等等令人兴奋的特性。

如果你对这个框架和 MoonBit 感兴趣,也欢迎参与到 MoonBit 的社区当中。

实习机会

我们也开放了一个前端实习岗位,负责 MoonBit 前端框架 Rabbit-TEA 和周边工具的研发。如果你了解前端框架中的 Virtual DOM、Server Side Rendering、状态管理等机制,了解函数式编程范式、The Elm Architecture 或者 React + Redux,欢迎复制打开链接或扫描下方二维码投递简历。
https://idea.zhiye.com/intern/detail?jobAdId=c2b87413-5ab5-40d3-93c7-a65ca48300e04

下一步

  • 下载 MoonBit
  • MoonBit 初学者之旅
  • MoonBit 语言导览
  • 查看 MoonBit 文档

相关文章:

在 MoonBit 中引入 Elm 架构:用简单原则打造健壮的 Web 应用

Elm 是一种纯函数式编程语言&#xff0c;专为构建前端 Web 应用程序而设计。它编译为 JavaScript&#xff0c;强调简洁性、性能和健壮性。 纯函数式的含义是函数没有副作用&#xff0c;这使得代码更易于理解和调试。通过强大的静态类型检查&#xff0c;Elm 确保应用程序不会抛…...

CMD命令行笔记

CMD命令行笔记&#xff0c;涵盖常用命令及实用技巧&#xff0c;适合快速查阅&#xff1a; 一、基础操作 打开CMD Win R → 输入 cmd → 回车管理员模式&#xff1a;右键开始菜单 → 选择“命令提示符&#xff08;管理员&#xff09;” 常用命令 help&#xff1a;查看所有命令…...

Python自动化办公

第五篇&#xff1a;Python自动化办公&#xff1a;10行代码搞定重复性工作 适合读者&#xff1a;职场人士、数据分析师 | 阅读时长&#xff1a;12分钟 引言 每天重复处理Excel、PDF或邮件&#xff1f;Python可以帮你自动化这些枯燥任务&#xff0c;节省90%的时间。本文通过实际…...

PDF 转换为 Word、HTML、LaTeX 和 Markdown 格式

PDF 转换为 Word、HTML、LaTeX 和 Markdown 格式 1. Doc2XReferences https://doc2x.com/ 1. Doc2X References [1] Yongqiang Cheng, https://yongqiang.blog.csdn.net/ [2] GPT 学术优化 (GPT Academic), https://github.com/binary-husky/gpt_academic [3] 学术版 GPT 网页…...

C#中async await异步关键字用法和异步的底层原理

目录 C#异步编程一、异步编程基础二、异步方法的工作原理三、代码示例四、编译后的底层实现五、总结 C#异步编程 一、异步编程基础 异步编程是啥玩意儿 就是让程序在干等着某些耗时操作&#xff08;比如等网络响应、读写文件啥的&#xff09;的时候&#xff0c;能把线程腾出来…...

shardingsphere-jdbc集成Seata分布式事务

1、导入相关依赖 <!-- shardingsphere-jdbc --><dependency><groupId>org.apache.shardingsphere</groupId><artifactId>shardingsphere-jdbc</artifactId><version>5.5.1</version></dependency><!-- shardingspher…...

华为OD机试真题——统计匹配的二元组个数(2025A卷:100分)Java/python/JavaScript/C++/C语言/GO六种最佳实现

2025 A卷 100分 题型 本文涵盖详细的问题分析、解题思路、代码实现、代码详解、测试用例以及综合分析&#xff1b; 并提供Java、python、JavaScript、C、C语言、GO六种语言的最佳实现方式&#xff01; 2025华为OD真题目录全流程解析/备考攻略/经验分享 华为OD机试真题《统计匹配…...

CSS 表格样式学习笔记

CSS 提供了强大的工具来美化和定制 HTML 表格的外观。通过合理使用 CSS 属性&#xff0c;可以使表格更加美观、易读且功能强大。以下是对 CSS 表格样式的详细学习笔记。 一、表格边框 1. 单独边框 默认情况下&#xff0c;表格的 <table>、<th> 和 <td> 元…...

MySQL表的增删改查进阶版

Mysql 1、数据库的约束1.1约束类型1.2 NULL约束1.3 UNIQUE&#xff1a;唯一约束1.4 DEFAULT&#xff1a;默认值约束1.5 PRIMARY KEY&#xff1a;主键约束&#xff08;重点&#xff09;1.6 FOREIGN KEY&#xff1a;外键约束&#xff08;重点&#xff09; 2.表的设计2.1一对一2.2…...

记录 | Pycharm中如何调用Anaconda的虚拟环境

目录 前言一、步骤Step1 查看anaconda 环境名Step2 Python项目编译器更改 更新时间 前言 参考文章&#xff1a; 参考视频&#xff1a;如何在pycharm中使用Anaconda创建的python环境 自己的感想 这里使用的Pycharm 2024专业版的。我所使用的Pycharm专业版位置&#xff1a;【仅用…...

2025年K8s最新高频面试题

目录 Kubernetes的核心组件有哪些,各自作用是什么? Pod和Deployment的区别? Service有哪些类型,分别适用于什么场景? ConfigMap和Secret有什么区别? StatefulSet 和 Deployment 的主要区别是什么? 什么是 Ingress,有哪些常用实现方式? 如何限制 Kubernetes 中 Pod …...

【Android】LiveData深度解析

一,概述 1,LiveData是状态订阅组件,是粘性的,而非事件订阅组件(可以没有事件,但不能没有状态)。所谓的状态,即UI状态,同一时刻只存在一种,且是最新状态,过期的状态应该被遗弃。事件,则是生产者创建的事件,需一一消费,不能被遗弃。 2,Android页面承载组件Activ…...

数据结构专题 - 线性表

线性表是数据结构中最基础、最常用的数据结构之一&#xff0c;它在实际应用中非常广泛。无论是操作系统中的内存管理&#xff0c;还是数据库中的索引结构&#xff0c;线性表都扮演着重要角色。 一、线性表的概念与抽象数据类型 1.1 线性表的逻辑结构 线性表是由n&#xff08…...

上门送水小程序区域代理模块框架设计

一、逻辑分析 代理申请流程&#xff1a; 潜在代理商通过小程序提交代理申请&#xff0c;需要填写个人或企业基本信息、联系方式、期望代理区域等。系统收到申请后&#xff0c;进行初步审核&#xff0c;检查信息的完整性和合规性。运营人员进行人工审核&#xff0c;根据公司政策…...

asp-for等常用的HTML辅助标记?

在ASP.NET Core Razor Pages 和 MVC 中&#xff0c;除了asp-for之外&#xff0c;还有许多常用的 HTML 辅助标记&#xff0c;下面为你详细介绍&#xff1a; 表单与路由相关 asp-action 和 asp-controller 用途&#xff1a;这两个标记用于生成表单或链接的 URL&#xff0c;指定…...

qt pyqt5的开发, 修改psd图像

这是引子, 需要将这个 photoshop-python-api 进行使用 https://juejin.cn/post/7445112318693621797#heading-4 这个是ps-python-api的官网, 在里面找api文档 https://pypi.org/project/photoshop-python-api/ 源码.gitee.url https://gitee.com/lbnb/psd_work.git 一. 安装必要…...

Spring 中的循环依赖问题:解决方案与三级缓存机制

目录 Spring 中的循环依赖问题&#xff1a;解决方案与三级缓存机制什么是循环依赖&#xff1f;循环依赖的定义循环依赖的举例 Spring 中的循环依赖类型1. 构造器注入引发的循环依赖2. Setter 注入引发的循环依赖3. 字段注入&#xff08;Autowired&#xff09;引发的循环依赖 Sp…...

ios接入穿山甲【Swift】

1.可接入的广告&#xff0c;点击右下角查看接入文档 https://www.csjplatform.com/union/media/union/download/groMore 2.进入接入文档&#xff0c;选择最新版本进行接入 pod Ads-CN-Beta,6.8.0.2pod GMGdtAdapter-Beta, 4.15.22.0pod GDTMobSDK,4.15.30pod KSAdSDK,3.3.74.0p…...

蓝桥杯大模板

init.c void System_Init() {P0 0x00; //关闭蜂鸣器和继电器P2 P2 & 0x1f | 0xa0;P2 & 0x1f;P0 0x00; //关闭LEDP2 P2 & 0x1f | 0x80;P2 & 0x1f; } led.c #include <LED.H>idata unsigned char temp_1 0x00; idata unsigned char temp_old…...

电脑一直不关机会怎么样?电脑长时间不关机的影响

现代生活中&#xff0c;许多人会让自己的电脑24小时不间断运行&#xff0c;无论是为了持续的工作、娱乐&#xff0c;还是出于忘记关机的习惯。然而&#xff0c;电脑长时间不关机&#xff0c;除了提供便利之外&#xff0c;也可能对设备的健康产生一系列影响。本文将为大家介绍电…...

vue3 当页面显示了 p/span/div 标签 想要转换成正常文字

返回值有标签出现时&#xff0c;使用v-html 解决 <p>{{ item.content }}</p> //页面直接显示接口返回的带标签的数据 <p v-html"item.content "></p> //转换成html文件 显示正常文字各种样式 问题&#xff1a; 解决&#xff1a;v-html 显…...

Elasticsearch 8.18 中提供了原生连接 (Native Joins)

作者&#xff1a;来自 Elastic Costin Leau 探索 LOOKUP JOIN&#xff0c;这是一条在 Elasticsearch 8.18 的技术预览中提供的新 ES|QL 命令。 很高兴宣布 LOOKUP JOIN —— 这是一条在 Elasticsearch 8.18 的技术预览中提供的新 ES|QL 命令&#xff0c;旨在执行左 joins 以进行…...

java CountDownLatch用法简介

CountDownLatch倒计数锁存器 CountDownLatch&#xff1a;用于协同控制一个或多个线程等待在其他线程中执行的一组操作完成&#xff0c;然后再继续执行 CountDownLatch用法 构造方法&#xff1a;CountDownLatch(int count)&#xff0c;count指定等待的条件数&#xff08;任务…...

k8s蓝绿发布

k8s蓝绿发布 什么是蓝绿部署K8S中如何实现蓝绿部署k8s蓝绿部署流程图 什么是蓝绿部署 参考: https://youtu.be/CLq_hA0lAd0 https://help.coding.net/docs/cd/best-practice/blue-green.html 蓝绿部署最早是由马丁福勒 2010年在他的博客中提出. 蓝绿部署是一种软件部署策略,用…...

链接世界:计算机网络的核心与前沿

计算机网络引言 在数字化时代&#xff0c;计算机网络已经成为我们日常生活和工作中不可或缺的基础设施。从简单的局域网&#xff08;LAN&#xff09;到全球互联网&#xff0c;计算机网络将数以亿计的设备连接在一起&#xff0c;推动了信息交换、资源共享以及全球化的进程。 什…...

记录Docker部署CosyVoice V2.0声音克隆

#记录工作 CosyVoice 是由 FunAudioLLM 团队开发的一个开源多语言大规模语音生成模型&#xff0c;提供了从推理、训练到部署的全栈解决方案。 项目地址&#xff1a; https://github.com/FunAudioLLM/CosyVoice.git 该项目目前从v1.0版本迭代到v2.0版本&#xff0c;但是在Wind…...

MCU刷写——HEX与S19文件互转详解及Python实现

工作之余来写写关于MCU的Bootloader刷写的相关知识,以免忘记。今天就来聊聊Hex与S19这这两种文件互相转化,我是分享人M哥,目前从事车载控制器的软件开发及测试工作。 学习过程中如有任何疑问,可底下评论! 如果觉得文章内容在工作学习中有帮助到你,麻烦点赞收藏评论+关注走…...

全链路开源数据平台技术选型指南:六大实战工具链解析

在数字化转型加速的背景下&#xff0c;开源技术正重塑数据平台的技术格局。本文深度解析数据平台的全链路架构&#xff0c;精选六款兼具创新性与实用性的开源工具&#xff0c;涵盖数据编排、治理、实时计算、联邦查询等核心场景&#xff0c;为企业构建云原生数据架构提供可落地…...

C++学习:六个月从基础到就业——面向对象编程:封装、继承与多态

C学习&#xff1a;六个月从基础到就业——面向对象编程&#xff1a;封装、继承与多态 本文是我C学习之旅系列的第九篇技术文章&#xff0c;主要讨论C中面向对象编程的三大核心特性&#xff1a;封装、继承与多态。这些概念是理解和应用面向对象设计的关键。查看完整系列目录了解…...

Golang Event Bus 最佳实践:使用 NSQite 实现松耦合架构

Go Event Bus 最佳实践&#xff1a;使用 NSQite 实现松耦合架构 什么是 Event Bus&#xff1f; Event Bus&#xff08;事件总线&#xff09;是一种消息传递模式&#xff0c;它允许应用程序的不同组件通过发布/订阅机制进行通信&#xff0c;而不需要直接相互依赖。这种模式特别…...