在我过去十年开发的信息系统中,数据在前端应用程序、后端服务器和服务等程序之间流动。这些程序使用诸如 JSON 之类的交换格式进行 络通信。
多年来,我注意到程序的复杂性不仅取决于业务需求的复杂性,还取决于数据的表示方法。
在静态类型语言(如 Java、C#、Go、OCaml 或 Haskell)中,用自定义类型或类表示数据似乎很自然的,而在动态类型语言(如 JavaScript、Ruby、Python 或 Clojure)中,我们通常会使用泛型数据结构,如 Map 和数组。
每种方法都有其优点和成本。当我们用静态类型表示数据时,IDE 可以为我们提供很大的支持,并且类型系统也为我们带来很大的安全性,但也导致代码变得更加冗长,数据模型更加严格。
在动态类型语言中,我们用灵活的 Map 来表示数据。我们可以快速地创建中小规模的代码,而不需要任何形式的繁文缛节,但 IDE 无法为我们提供自动完成支持,如果我们输入了错误的字段名时,在运行时会遇到错误。
Ballerina 全新的类型系统
在接触 Ballerina 之前,我认为这种权衡是编程固有的组成部分,我们不得不接受这种权衡。但后来我发现我错了:把两种类型系统的优点结合起来是可能的。在不影响安全和清晰度的情况下实现快速编码是可能的。我们可以从灵活的类型系统中受益。
我不能慢路,因为太慢了。
我害怕小跑,因为风险太大了。
我想要轻松而自信地流动,像一个芭蕾舞演员。
数据是一等公民
当我们在开发一个操作数据的程序时,通常把数据当作一等公民来对待。一等公民的特权之一是,它们可以像数字和字符串一样,不需要额外的步骤就可以创建好。
不幸的是,在静态类型语言中,数据通常无法绕过很多必要的约束。你需要使用命名的构造函数来创建数据。如果数据没有嵌套,尽管缺少字面量也并不会太麻烦。例如,创建一个叫作 Kelly Kapowski 的 17 岁图书馆会员:
Member kelly = new Member( "Kelly", "Kapowski", 17);
复制代码
Member kelly = new Member( "Kelly", "Kapowski", 17, List.of( new Book( "The Volleyball Handbook", new Author("Bob", "Miller") ) ));
复制代码
在动态类型的语言中,如 JavaScript,使用字面量来创建嵌套数据会更加自然一些。
var kelly = { firstName: "Kelly", lastName: "Kapowski", age: 17, books: [ { title: "The Volleyball Handbook", author: { firstName: "Bob", lastName: "Miller" } } ]};
复制代码
在处理数据的方式上,动态类型语言的问题在于数据不受控制。你只知道你创建的数据是一个嵌套的 Map。因此,你需要依靠文档来了解确切的数据类型是什么。
Ballerina 的第一个优势是,我能够用它创建自定义类型,并保持使用数据字面量创建数据的便利性。
与静态类型语言一样,在 Ballerina 中,我们可以创建自定义记录类型来表示数据模型。下面是创建 Author、Book 和 Member 记录类型的方法:
type Author record { string firstName; string lastName;};type Book record { string title; Author author;};type Member record { string firstName; string lastName; int age; Book[] books;};
复制代码
同样,与动态类型语言一样,在 Ballerina 中,我们也可以用数据字面量来创建数据。
Member kelly = { firstName: "Kelly", lastName: "Kapowski", age: 17, books: [ { title: "The Volleyball Handbook", author: { firstName: "Bob", lastName: "Miller" } } ] };
复制代码
当然,与传统的静态类型语言一样,如果我们遗漏了记录类型的某个字段,类型系统会让我们知道。我们的代码无法通过编译,编译器会告诉我们确切的原因。
Author yehonathan = { firstName: "Yehonathan"};ERROR [...] missing non-defaultable required record field 'lastName'
复制代码
如果 VSCode 中安装了 Ballerina 插件,就会获得关于缺失字段的警告。
现在,你可能会问自己,Ballerina 的类型系统是静态的还是动态的。接下来,让我们来看一看。
灵活的 Ballerina 类型系统
在传统的静态类型语言中,我需要为新填充的数据创建一个新的类型,比如一个叫作 EnrichedAuthor 的新类型。但在 Ballerina 中,这不是必需的,它的类型系统允许你使用中括 表示法动态地添加字段,就跟动态类型语言一样。例如,在下面的例子中,我们将 fullName 字段添加到 Author 记录中:
Author yehonathan = { firstName: "Yehonathan", lastName: "Sharvit"};yehonathan["fullName"] = "Yehonathan Sharvit";
复制代码
这种语言特性让人感到惊艳。从某种意义上说,Ballerina 优雅地引入了两种不同符 之间的语义差异,让开发人员可以鱼和熊掌兼得:
- 当我们使用点 访问或修改记录字段时,Ballerina 为我们提供了与静态类型语言相同的安全性。
- 当我们使用中括 来访问或修改记录字段时,Ballerina 为我们提供了动态类型语言的灵活性。
在某些情况下,我们希望严格一些,不允许添加字段。这没问题,因为 Ballerina 支持封闭记录。封闭记录的语法与开放记录的语法类似,只是字段列表包含在两个|字符中间。
type ClosedAuthor record {| string firstName; string lastName;|};ClosedAuthor yehonathan = { firstName: "Yehonathan", lastName: "Sharvit"};
复制代码
类型系统不允许你向封闭记录添加字段。
yehonathan["fullName"] = "Yehonathan Sharvit";ERROR [...] undefined field 'fullName' in 'ClosedAuthor'
复制代码
type AuthorWithOptionalFirstName record { string firstName?; string lastName;};
复制代码
在访问记录的可选字段时,你需要处理好字段不存在的情况。在传统的动态类型语言中,由于缺少静态类型检查器,开发人员很容易就忘了处理这种情况。1965 年,Tony Hoare 在一门叫作 ALGOL 的编程语言中引入了空引用,后来,他认为这是一个价值数十亿美元的错误。
function upperCaseFirstName(AuthorWithOptionalFirstName author) { author.firstName = author.firstName.toUpperAscii();}
复制代码
这段代码无法通过编译:类型系统(和 VSCode 的 Ballerina 扩展插件)会提醒你无法保证可选字段的存在。
ERROR [...] undefined function 'toUpperAscii' in type 'string?'
复制代码
那么,我们该如何修改我们的代码,以便正确地处理可选字段缺失的情况呢?很简单,就是在访问可选字段后,检查它是否存在。在 Ballerina 中,字段的缺失使用()来表示。
function upperCaseFirstName(AuthorWithOptionalFirstName author) { string? firstName = author.firstName; if (firstName is ()) { return; } author.firstName = firstName.toUpperAscii();}
复制代码
需要注意的是,这里不需要进行类型转换。类型系统足够聪明,它知道在检查变量 firstName 不是()之后,就可以保证它是一个字符串。
我发现 Ballerina 的类型系统还有一个非常有用的地方,即记录类型只需要通过字段结构来定义。这个让我来解释一下。
当我们在开发一个操作数据的程序时,大部分代码都是由接收数据和返回数据的函数组成。每个函数都对它接收的数据格式有所要求。
在静态类型语言中,这些要求表示为类型或类。通过查看函数签名就可以确切地知道参数的数据格式。问题是,这样会在代码和数据之间造成紧密的耦合。
function fullName(Author author) returns string { return author.firstName + " " + author.lastName;}
复制代码
这个函数的局限性是它只能处理 Author 类型的记录。它不接受 Member 类型的记录,这让我感到有点失望。毕竟,Member 记录也会有 firstName 和 lastName 字段。
需要注意的是,一些静态类型语言允许你通过创建数据接口来绕开这个限制。
动态类型语言要灵活得多。例如,在 JavaScript 中,你可以像这样实现函数:
function fullName(author) { return author.firstName + " " + author.lastName;}
复制代码
函数的参数名是 author,但实际上,它可以接受任何具有 firstName 和 lastName 字符串字段的数据。问题是,当你传给它一个不包含这些字段(或其中一个)的数据时,它将会抛出运行时异常。此外,参数的预期数据格式并没有在代码中体现。因此,要知道函数需要什么样的数据,我们要么依赖于文档(它并不总是最新的),要么需要研究一下函数的代码。
Ballerina 的类型系统允许你在不牺牲灵活性的情况下指定函数参数的格式。你可以创建一个新的记录类型,并只提到正常调用函数所需的字段。
type Named record { string firstName; string lastName;};function fullName(Named a) returns string { return a.firstName + " " + a.lastName;}
复制代码
小贴士:你可以使用匿名记录类型来指定函数参数的格式。
function fullName(record { string firstName; string lastName; } a) returns string { return a.firstName + " " + a.lastName;}
复制代码
你可以使用任何包含必需字段的记录来调用函数,无论它是 Member 或 Author,还是任何其他具有函数所期望的字段的记录。
Member kelly = { firstName: "Kelly", lastName: "Kapowski", age: 17, books: [ { title: "The Volleyball Handbook", author: { firstName: "Bob", lastName: "Miller", fullName: "Bob Miller" } } ] };fullName(kelly);// "Kelly Kapowski"fullName(kelly.books[0].author);// "Bob Miller"
复制代码
我觉得 Ballerina 处理类型的方式可以用一个类比来说明:类型就像是我们在程序中用来观察现实的透镜。但需要注意的是,我们透过透镜看到的只是现实的一个方面,它不是现实本身。就像谚语说的:地图并不是领土。
例如,我们不能确切地说 fullName 函数接受的一定是一个有名字的记录,而应该是说,fullName 函数通过有名字的记录透镜来决定要接收的数据。
让我们来看另一个例子。在 Ballerina 中,具有相同字段值的两种不同类型的记录被认为是相等的。
Author yehonathan = { firstName: "Yehonathan", lastName: "Sharvit"};AuthorWithBooks sharvit = { firstName: "Yehonathan", lastName: "Sharvit"};yehonathan == sharvit;// true
复制代码
首先,这种行为让我感到惊讶。两种不同类型的记录为什么被认为是相等的?但当我想到透镜的类比时,我明白了:
这两种类型是两种不同的透镜,它们看到的是同一个现实。在我们的程序中,最重要的是现实,而不是透镜。有时候,传统的静态类型语言似乎更强调透镜,而不是现实。
到目前为止,我们已经看到了 Ballerina 的类型系统不仅不会妨碍到我们,还让我们的开发工作流更高效。但其实 Ballerina 在这个基础上更进一步,提供了一种强大且便利的方式,允许我们通过表达性查询语言来操作数据。
强大的表达性查询语言
作为一个函数式编程行家,在操作数据时,我常用的命令都是一些高阶函数,如 map、filter 和 reduce。Ballerina 支持函数式编程,但在 Ballerina 中处理数据操作的惯用方式是使用表达性查询语言,我们可以用它非常流畅地表达业务逻辑。
function enrichAuthor(Book book) returns Book { book.author["fullName"] = fullName(book.author); return book;}
复制代码
我们可以用 map、filter 和一些匿名函数来填充书籍。
function enrichBooks(Book[] books) returns Book[] { return books.filter(function(Book book) returns boolean { return book.title.includes("Volleyball"); }). map(function(Book book) returns Book { return enrichAuthor(book); });}
复制代码
但这样很繁琐,声明这两个匿名函数的类型也有点烦人。但如果使用 Ballerina 的查询语言,代码就会更紧凑,更容易阅读。
function enrichBooks(Book[] books) returns Book[] { return from var book in books where book.title.includes("Volleyball") select enrichAuthor(book);}
复制代码
Ballerina 的查询语言将在我们的 Ballerina 系列文章中详细介绍。
在继续介绍 JSON 相关的特性之前,我们先为函数编写一个单元测试。在 Ballerina 中,当记录具有相同的字段和值时,它们就被认为是相等的。因此,要比较函数返回的数据和我们期望的数据就很容易了。
Book bookWithVolleyball = { title: "The Volleyball Handbook", author: { firstName: "Bob", lastName: "Miller" }};Book bookWithoutVolleyball = { title: "Friendship Bread", author: { firstName: "Darien", lastName: "Gee" }};Book[] books = [bookWithVolleyball, bookWithoutVolleyball];Book[] expectedResult = [ { title: "The Volleyball Handbook", author: { firstName: "Bob", lastName: "Miller", fullName: "Bob Miller" } } ];
声明:本站部分文章及图片源自用户投稿,如本站任何资料有侵权请您尽早请联系jinwei@zod.com.cn进行处理,非常感谢!