从 Gleam 调用 Elixir 库的简单示例
最近,作为我寻找新编程语言的一部分,我一直在尝试 Gleam 和 Elixir。
Gleam 的招牌特性之一是它可以调用 Elixir 代码和库,但我找不到任何关于如何做到这一点的示例。基于我对 Gleam/Elixir/Erlang 生态系统的初学者理解,我写了一个从 Gleam 项目调用 Elixir 库的简单示例。
安装依赖
在这个示例中,我使用的是:
- Gleam 1.10.0
- Erlang 27.3.4
- Elixir 1.18.3
你可以用任何方式安装这些依赖。我用 Nix 来管理依赖,所以我通过创建如下 flake.nix 来安装所有东西。
{
description = "Dev environment for gleam-call-elixir-simple";
inputs = {
flake-utils.url = "github:numtide/flake-utils";
# 27.3.4
erlang-nixpkgs.url = "github:NixOS/nixpkgs/8406224e30c258025cb8b31704bdb977a8f1f0";
# 1.10.0
gleam-nixpkgs.url = "github:NixOS/nixpkgs/3866ad91cfc172f08a6839def503d8fc2923c603";
};
outputs = {
self,
flake-utils,
erlang-nixpkgs,
gleam-nixpkgs,
} @ inputs:
flake-utils.lib.eachDefaultSystem (system: let
erlang = erlang-nixpkgs.legacyPackages.${system}.beam27Packages.erlang;
elixir = erlang-nixpkgs.legacyPackages.${system}.beam27Packages.elixir;
gleam = gleam-nixpkgs.legacyPackages.${system}.gleam;
inotify-tools = erlang-nixpkgs.legacyPackages.${system}.inotify-tools;
in {
devShells.default =
erlang-nixpkgs.legacyPackages.${system}.mkShell
{
packages = [
erlang
elixir
gleam
inotify-tools
];
shellHook = ''
echo "erlang" $(erl -eval 'erlang:display(erlang:system_info(otp_release)), halt().' -noshell)
gleam --version
'';
};
formatter = erlang-nixpkgs.legacyPackages.${system}.alejandra;
});
}如果我运行 nix develop,可以看到我的 shell 中有 erlang 和 Gleam 可用:
$ nix develop
erlang "27"
gleam 1.10.0创建项目
我使用 gleam new 创建一个项目。我把项目托管在 Github 之外,所以我加上 --skip-github 来跳过 Gleam 默认添加的 Github 相关文件:
PROJECT_NAME='call_elixir'
gleam new --name "${PROJECT_NAME}" --skip-github .插曲:绕过一个 Gleam 包的 bug
接下来,我尝试运行 gleam new 为我生成的样板代码,但它失败了:
$ gleam run
Resolving versions
Downloading packages
Downloaded 2 packages in 0.01s
Compiling gleam_stdlib
error: Incompatible Gleam version
The package `gleeunit` requires a Gleam version satisfying 1.11.0 <= v but you are using v1.10.0.问题在于 gleam new 添加了 gleeunit 1.4.0 作为依赖,但该包依赖于 Gleam 1.11.0,比我本地的 Gleam 版本(1.10.0)更新。
另一个人也遇到了同样的问题,并且在我碰到这个问题的一小时前就在 Github 上提交了 bug 报告。
我可以强制 Gleam 降级到 gleeunit 1.3.1 来绕过这个问题:
$ gleam add --dev [email protected]
Resolving versions
Downloading packages
Downloaded 1 package in 0.00s
Added gleeunit v1.3.1然后 gleam run 和 gleam test 就能按预期工作了:
$ gleam run
Compiling gleeunit
Compiling call_elixir
Compiled in 0.28s
Running call_elixir.main
Hello from call_elixir!$ gleam test
Compiled in 0.01s
Running call_elixir_test.main
.
Finished in 0.006 seconds
1 tests, 0 failures安装一个 Elixir 包
我想选一个语义简单的包,CSV 库怎么样?
hex 包管理器显示,最受欢迎的 CSV 包叫做 CSV,于是我安装了它:
$ gleam add csv
Resolving versions
Downloading packages
Downloaded 1 package in 0.00s
Added csv v3.2.2题外话:有一个 Gleam 原生的 CSV 库叫 gsv,但我改用 Elixir 库,这样我可以练习调用非 Gleam 库。
用 Elixir 测试 CSV 包
首先,我需要弄清楚到底该如何调用 CSV 包的 API,然后才能为我需要的 API 编写 Elixir 封装。
我想调用 CSV.encode 函数,它的签名是这样的:
@spec encode(Enumerable.t(), [encode_options()]) :: Enumerable.t()所以,encode 有两个参数:
- 一个实现了
Enumerable协议的对象。 - (可选)一个
encode_options()类型。
它返回一个实现了 Enumerable 协议的对象。
我不懂 Elixir,所以我启动 iex——Elixir 的交互式 shell——来理解它的语义:
iex然后,我在 iex 中安装 CSV 包:
iex> Mix.install([:csv])
Resolving Hex dependencies...
Resolution completed in 0.008s
New:
csv 3.2.2
* Getting csv (Hex package)现在,我尝试 CSV 包文档中的某个示例:
iex> [~w(a b), ~w(c d)]
[["a", "b"], ["c", "d"]]
iex> |> CSV.encode
#Function<61.117496853/2 in Stream.transform/3>
iex> |> Enum.take(2)
["a,b\r\n", "c,d\r\n"]我还不太理解 Elixir 的 sigil 语法,而且我不喜欢用“a”和“b”的示例,所以这里有一个对我来说更直观的重写版本:
iex> CSV.encode([["movie", "rating"], ["The Godfather", 10], ["Gigli", 2]])
#Function<61.117496853/2 in Stream.transform/3>
iex> |> Enum.to_list()
["movie,rating\r\n", "The Godfather,10\r\n", "Gigli,2\r\n"]
iex> |> IO.puts()
movie,rating
The Godfather,10
Gigli,2
:ok好,看起来 CSV.encode 接收一个字符串列表的列表,并返回一个字符串的 Enumerable。
为 Elixir 包创建 Gleam 封装
现在我理解了 CSV.encode 的语义,接下来需要编写一个封装函数以便从 Gleam 调用它。
为 Elixir 函数编写 Gleam 封装的主要挑战在于,这两种语言的类型并不匹配。Gleam 使用更严格的静态类型,而 Elixir 使用更灵活的动态类型。
封装 CSV.encode
我想调用 CSV.encode 函数,它的签名再重复一遍是这样的:
@spec encode(Enumerable.t(), [encode_options()]) :: Enumerable.t()Gleam 标准库似乎没有与 Elixir 的 Enumerable 等价的东西,所以我需要用另一个 Elixir API 把 Enumerable 转换成 Gleam 能理解的东西。
Enum.to_list 看起来是最佳选择,因为它返回 Elixir 内置的 list 类型,而 Gleam 有等价的 List 类型。
所以,首先,我为 Elixir 的 CSV.encode API 定义一个 Gleam 封装函数:
// Create a custom Gleam type to represent the type we receive from Elixir.
type ElixirEnumerable
@external(erlang, "Elixir.CSV", "encode")
fn csv_encode(data: List(List(String))) -> ElixirEnumerable@external 属性让我可以从 Gleam 调用 Elixir 代码。Gleam、Elixir 和 Erlang 都可以编译成在 BEAM 虚拟机上运行的字节码。在 BEAM 中,CSV.encode 函数出现在 Elixir.CSV 命名空间下,这就是为什么我需要在 @external 属性中指定 Elixir。
我把输入参数定义为 Gleam 的字符串列表的列表(List(List(String)))),它与 Erlang 的 Enumerable 类型兼容。
CSV.encode 返回一个 Elixir 的 Enumerable,但我不知道它在 Gleam 中的等价物是什么,所以我定义了一个自定义类型 ElixirEnumerable。
Gleam 代码无法对 ElixirEnumerable 做任何事情,因为它不包含任何 Gleam 能原生访问的数据,所以我需要一种方法把 ElixirEnumerable 转换为 Gleam 原生类型。
把 Elixir 的 Enumerable 转换为 Gleam 的 List
为了把 Elixir 的 Enumerable 类型转换为 Gleam 的 List,我再声明一个外部函数来转换结果:
@external(erlang, "Elixir.Enum", "to_list")
fn enum_to_list(elixir_enum: ElixirEnumerable) -> List(String)在这里,我使用 Elixir 的 Enum.to_list 函数把 ElixirEnumerable 转换为 Elixir 的 List 类型,它看起来与 Gleam 的 List 类型相匹配。
创建对 Gleam 友好的封装
现在我已经写好了能封装所需 Elixir API 的函数,是时候用一个我将从这个模块暴露给 Gleam 应用的函数把它们串起来了:
pub fn encode(data: List(List(String))) -> List(String) {
data
|> csv_encode
|> enum_to_list
}这个函数很简单。它接收字符串列表的列表形式的数据,然后使用 Gleam 的管道操作符把它传给 csv_encode,再用一次管道操作符把结果转换为 Gleam 兼容的字符串列表。
这是我的 csv 模块中唯一声明为公开的函数,因为其他封装太底层,对这个模块的 Gleam 客户端没什么用处。
完整的模块如下:
// Create a custom Gleam type to represent the type we receive from Elixir.
type ElixirEnumerable
@external(erlang, "Elixir.CSV", "encode")
fn csv_encode(data: List(List(String))) -> ElixirEnumerable
@external(erlang, "Elixir.Enum", "to_list")
fn enum_to_list(elixir_enum: ElixirEnumerable) -> List(String)
pub fn encode(data: List(List(String))) -> List(String) {
data
|> csv_encode
|> enum_to_list
}调用我的封装函数
既然我已经有了 CSV.encode 函数的 Gleam 原生封装,我就可以写一个简单的 Gleam 应用来调用我的封装:
import gleam/io
import gleam/string
import csv
pub fn main() {
[
["Title", "Author", "Release Year"],
["Infinite Jest", "David Foster Wallace", "1996"],
["Emma", "Jane Austen", "1815"],
["Catch-22", "Joseph Heller", "1961"]
] |> csv.encode()
|> string.concat()
|> io.print()
}我创建了一个简单的字符串列表的列表,把它传给我的 csv.encode Gleam 封装。它返回一个字符串列表,所以我调用 Gleam 标准库的 string.concat 函数把这些字符串连接成一个字符串。最后,我调用 io.print 把结果打印到控制台。
$ gleam run
Compiling call_elixir
Compiled in 0.20s
Running call_elixir.main
Title,Author,Release Year
Infinite Jest,David Foster Wallace,1996
Emma,Jane Austen,1815
Catch-22,Joseph Heller,1961成功了!我在这个简单的 Gleam 应用中成功调用了 Elixir 的 CSV 库。
源代码
这个示例的完整源代码可在以下位置获取:
–
感谢 Louis Pilford(路易斯·皮尔福德)和 DisguisedPigeon 对本文提出的有益反馈。
随机一篇博客