写自:2024-05-19
作者:柳下川
🔗 背景
gleam 是一门类型安全, 高度扩展, 语法友善, 表达力强的现代编程语言
gleam 在 v0.25 版本中引入了一种期待已久的新功能: use
当我们敲代码
一个 open
时, 使用完毕之后就得调用对应的 File
方法, 保证文件正确关闭close
倘若每次都得由程序员手动保证调用 close, 不仅繁琐而且容易出错
在主流语言中, 你会发现各种各样的, 管理与释放资源的不同姿势:
- go/zig 中的
语句, 将在函数/代码块结束之前被调用defer
- java 中的
, 让实现了try-with-resource
接口的实例在AutoClosable
后自动调用try-catch
方法close()
- c# 中的
, 作用于实现了using
接口的实例, 编译器将IDisposable
自动转换为等价的using
try-finally
- cpp/rust 中的
, 让资源的生存期与变量的作用域相互绑定RAII
- ……
- ……
但是在 gleam 语言中, 已经存在了一个用于解决上述问题, 并且更加泛用的语法: use
(类似的概念在其他语言中也存在, 这并非是 gleam 独创的, 借鉴了 ocaml, koka, roc, ml系列, 甚至 python)
🔗 缩进问题
让我们来点例子, 假设存在这么一段代码:
pub fn login(credentials) { case authenticate(credentials) { Error(e) -> Error(e) Ok(user) -> case fetch_profile(user) { Error(e) -> Error(e) Ok(profile) -> render_welcome(user, profile) } } }
与 authenticate
会返回 fetch_profile
类型, 用 Result
分别表示成功与失败时的情况, 并包裹返回的值:Ok/Error
import gleam/io pub type Result(a, e) { Ok(a) Error(e) } pub fn main() { let a = Error("error") let info = case a { Ok(_) -> "It's ok" Error(_) -> "It's error" } io.println(info) }
再回过头去看前面的那段代码, 意思就是当前一步成功(Ok)时, 再运行下一步(Ok), 每一步失败时就返回错误(Error)
但这有一个烦恼: 代码的缩进问题很严重, 倘若某个函数里调用了许多个会返回
类型的函数……Result
于是 gleam 在早期的 v-0.9 版本引入了
语法, 可以将前文的代码写成如下的等价形式:try
// 当前一步失败时直接将 Error 作为这一段落的表达式, 无视后面的代码 pub fn login(credentials) { try user = authenticate(credentials) try profile = fetch_profile(user) render_welcome(user, profile) }
这很好, 但仍不够好, 缩进问题没被完全解决
导致缩进复杂的情况还包括 闭包/匿名函数/回调函数:
pub fn handle_request(request: HttpRequest) { logger.span("handle_request", fn() { database.connection(fn(conn) { case request.method { Post -> case database.insert(conn, request.body) { Ok(record) -> created_response(record) Error(exc) -> bad_request_response(exc) } _ -> method_not_allowed_response() } }) }) }
显而易见, 这段代码虽然条理清晰, 但实话实说看见的第一眼并不容易阅读, 也因为缩进问题而不够美观
当这种 闭包/匿名函数/回调函数 一多起来, 就会出现著名的 回调函数地域(callback-hall)
gleam 团队给出的解决方案就是新的
表达式, 在保持简单概念的同时, 处理 错误处理/回调函数/资源管理 时的缩进问题:use
pub fn handle_request(request: HttpRequest) { use <- logger.span("handle_request") use <- require_method(request, Post) use conn <- database.connection() case database.insert(conn, request.body) { Ok(record) -> created_response(record) Error(exc) -> bad_request_response(exc) } }
下面将正式介绍
的一些使用例子use-expression
🔗 use表达式
仅仅是一些语法糖, 它将后续的所有表达式转换为 闭包/匿名函数, 将此函数作为参数传递给 use
右侧的调用use
例如, 假设有一个函数
, 打开一个文件, 进行读写, 然后关闭该文件:with_file
fn open(file) { todo } fn close(file) { todo } // Define the function pub fn with_file(path, handler) { let file = open(path) handler(file) close(file) } // Use it pub fn main() { with_file("pokemon.txt", fn(file) { write(file, "Oddish\n") write(file, "Farfetch'd\n") }) }
在 gleam 语言中, 小写的参数表示泛型, 类型会根据传入的参数自动推导, 因此上面的例子是可以编译的, 并非伪代码
使用 use 无需额外缩进即可调用:
pub fn main() { use file <- with_file("pokemon.txt") write(file, "Oddish\n") write(file, "Farfetch'd\n") }
它不仅限于单个参数, 还可用于任何参数的函数, 包括不接受参数的函数
这个
表达式是高度通用的, 不限于任何特定类型, 接口, 因此它可以应用于许多不同的事物use
(在 gleam 语言中, 小写的参数表示泛型, 类型会根据传入的参数自动推导, 因此看见下面例子中诸如
时, 还请不要惊讶)defer
下面是一些小例子:
// 编写 http 中间件 pub fn require_method(request, method, continue) { case request.method == method { True -> continue() False -> method_not_allowed() } } pub fn handle_request(request) { use <- require_method(request, Post) // ... }
// 复制 go/zig 等语言中的 defer 语法 pub fn defer(cleanup, body) { body() cleanup() } pub fn main() { use <- defer(fn() { io.println("Goodbye") }) io.println("Hello!") }
// 复制 elixir/haskell/scala 等语言中的 `for-comprehension` import gleam/list pub fn main() { use letter <- list.flat_map(["a", "b", "c"]) use number <- list.map([1, 2, 3]) #(letter, number) } // [ // #("a", 1), #("a", 2), #("a", 3), // #("b", 1), #("b", 2), #("b", 3), // #("c", 1), #("c", 2), #("c", 3), // ]
// 复制 gleam 自己的 `try` 表达式(目前已被 `use` 取代) pub fn attempt(result, transformation) { case result { Ok(x) -> transformation(x) Error(y) -> Error(y) } } pub fn main() { use user <- attempt(authenticate(credentials)) use profile <- attempt(fetch_profile(user)) render_welcome(user, profile) }
🔗 总结
gleam 语言中的
是支持了 句法变换(syntactic-transformation) 这一概念的语法use
它相比较于主流语言更加泛用, 仅仅是函数应用的语法糖, 而非诸如 defer/using 等针对特定问题的特殊支持
这种概念在函数式编程语言中较为常见, 如 ocaml 中的
, koka 中的 let*
, roc 中的 with
, 与 backpassing
都是十分相似的概念use
(上面有部分涉及到
, 比 gleam 这种单纯的玩意要高级很多)algebraic effect
use 使得嵌套(nested)的函数可以被轻松地扁平化(flatten)
为了更加直观地体现这点, 我们可以编写一个函数 twice, 参数是一个闭包, 让该闭包可以运行两次:
import gleam/io fn twice(f) { f() f() } fn main() { twice(fn() { io.print("Message ") }) } // Message Message
使用
的等价写法:use
fn main() { use <- twice() io.print("Message ") } // Message Message
当发生嵌套时(我们想调用两次, 三次, 更多次 twice 时), use 的优势会更加明显:
fn main() { twice(fn() { twice(fn() { twice(fn() { io.print("Message") }) }) }) } // Message Message Message Message Message Message Message Message
fn main() { use <- twice() use <- twice() use <- twice() io.println("Message") } // Message Message Message Message Message Message Message Message
于此同时, 因为 gleam 是门类型安全的语言, 我们在使用
时也会检查是否匹配类型的, 比如:use
fn print(self: List(String)) { use <- bool.guard(when: self |> list.is_empty, return: io.print("Empty list")) use i <- list.each(self) io.println(i) }
倘若当我们疏忽地写成了这样:
fn print(self: List(String)) { use <- bool.guard(when: self |> list.is_empty, return: "Empty list") use i <- list.each(self) io.println(i) }
Expected type: String Found type: Nil
这里再来一个其他语言作为 嵌套 被 扁平化 的例子吧, 来自 rock 语言的
, 以下两段代码等价:backpassing
readLicense : Filename -> Task License File.ReadErr readLicense = \filename -> Task.await (File.readUtf8 settingsFilename) \settingsYaml -> settingsYaml |> Yaml.decode settingsDecoder |> Task.fromResult |> Task.mapFail InvalidFormat |> Task.await \settings -> Task.await (File.readUtf8 settings.projectFilename) \projectCsv -> projectCsv |> Csv.decode projectDecoder |> Task.fromResult |> Task.mapFail InvalidFormat |> Task.await \project -> Task.await (File.readUf8 project.licenseFilename) \licenseStr -> License.fromStr licenseStr |> Task.fromResult |> Task.mapFail InvalidFormat
使用
:backpassing
readLicense : Filename -> Task License File.ReadErr readLicense = \filename -> settingsYaml <- Task.await (File.readUtf8 settingsFilename) settings <- settingsYaml |> Yaml.decode settingsDecoder |> Task.fromResult |> Task.mapFail InvalidFormat projectCsv <- Task.await (File.readUtf8 settings.projectFilename) project <- projectCsv |> Csv.decode projectDecoder |> Task.fromResult |> Task.mapFail InvalidFormat licenseStr <- Task.await (File.readUf8 project.licenseFilename) License.fromStr licenseStr |> Task.fromResult |> Task.mapFail InvalidFormat