从 Gleam 调用 Elixir 库的简单示例
原文由 Michael Lynch 于 发布,订阅该博客
最近,作为寻找新编程语言尝试的一部分,我一直在体验 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,就能看到 erlang 和 Gleam 已经在 shell 中可用:
$ 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,但为了练习调用非 Gleam 库,我特意选用了 Elixir 的库。
使用 Elixir 测试 CSV 包
首先,我需要弄清楚如何调用 CSV 包的 API,然后才能为我需要的 API 编写 Elixir 封装。
我想调用CSV.encode 函数,它的签名如下:
@spec encode(Enumerable.t(), [encode_options()]) :: Enumerable.t()因此,encode 有两个参数:
- 一个实现了
Enumerable协议的对象。 - (可选)
encode_options()类型。
它返回一个实现了 Enumerable 协议的对象。
我不懂 Elixir,所以我启动了交互式 Elixir shell iex 来了解其语义:
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 对本文提供的宝贵反馈。
随机一篇博客
评论
登录后参与讨论