Gleam,一种新的函数式编程语言简介

Gleam 是一种类型安全的函数式编程语言,用于构建可扩展的并发系统。它是否像声称的那样友好?我们来了解一下。

译自 Introduction to Gleam, a New Functional Programming Language,作者 David Eastman。

当我的同事读完我的 Virgil 帖子 后,他立刻建议我看看 Gleam。它很酷且很新——版本 1 在今年 3 月 发布——并且在编程生活的函数式方面表现得很出色。

Gleam 是一种类型安全的函数式编程语言,用于构建可扩展的并发系统。它编译为 ErlangJavaScript,因此与其他“BEAM”语言(如 Erlang 和 Elixir)具有直接的互操作性。(BEAM 是在 Erlang 运行时系统中执行用户代码的虚拟机。我相信它的缩写是 Bogdan’s Erlang Abstract Machine。别问。)

Erlang 是一种早期的电信行业语言,非常注重并发性和容错性。它的做事方式仍然受到尊重,并且解释了 Elixir 的流行。在这篇文章中,我不会假设你熟悉这些;实际上,Gleam 特别友好,因此它也没有做出太多假设。

让我们从 hello world 开始:

import gleam/io
pub fn main() {
  io.println("hello world!")
}

这与 Zig 中的相同内容非常相似。

有一个非常愉快的 语言之旅,它利用 Gleam 编译到 JavaScript 来提供动态检查。你还可以将其用作游乐场。安装 Gleam 也意味着安装 Erlang。对于我的 Mac,我只使用了 Homebrew:

brew install gleam

Homebrew 会自动安装 Erlang。

Gleam 带有一个模板(或项目)生成器,很像 Rails。因此,要创建一个新的 hello 项目,我只需键入:

目前还没有节省时间,“hello world”风格的一行代码已经作为 hello.gleam 中的默认代码存在:

如果我运行整个项目:

请注意,这两个包仅在第一次运行时编译。

包管理

有两个 .toml 文件(显然是 Tom’s Own Markup Language。别问),它们充当配置。

由于它们应该很简单,我们可以快速浏览一下。在 gleam.toml: 中:

[dependencies]
gleam_stdlib = ">= 0.34.0 and < 2.0.0"

请注意,它们有一个 版本约束——提到最大版本以减少不兼容性。

下载和使用的实际版本在 manifest.toml 中提到。

如果我们 遵循 一个简单的示例,我们可以学习一些 Gleam 并使用包管理器。我们将添加几个包,并编写一些代码来打印环境变量。我将使用相同的 hello 项目模板,但插入了新代码。

首先,我们将添加新包以允许读取环境( envoy)和读取命令行参数( argv)——你可能希望它们是内置的,但可能反映系统差异。

因此,让我们用打印环境变量的代码替换 hello.gleam 中的代码:

import argv
import envoy
import gleam/io
import gleam/result
pub fn main() {
  case argv.load().arguments {
    ["get", name] -> get(name)
    _ -> io.println("Usage: get <name>")
  }
}
fn get(name: String) -> Nil {
  let value = envoy.get(name) |> result.unwrap("")
  io.println(format_pair(name, value))
}
fn format_pair(name: String, value: String) -> String {
  name <> "=" <> value
}

添加到公共 main 入口点,我们有两个函数。它们使用与我们在 Virgil 中看到完全相同的格式。事实证明,类型注释是可选的,但被认为是良好的实践。现在,我们变得有点函数化。argv 加载执行你期望的操作,并拉入一个列表,该列表希望恰好包含两个字符串——第一个字符串等于“get”。这在 case 语句中使用。

简单说一下,Gleam case 比大多数非函数式语言中的更灵活。在这里,我们看到列表的内容正在被比较:

let result = case x {
  [] -> "Empty list"
  [1] -> "List of just 1"
  [4, ..] -> "List starting with 4"
  [_, _] -> "List of 2 elements"
  _ -> "Some other list"
}

因此, 模式可以在 case 语句中进行比较。下划线 _ 表示默认值,并且可能的情况被穷举检查。

回到我们的环境变量读取代码,如果模式 不是 两个字符串的列表,那么将显示帮助文本。否则,它将调用 get 函数。 我们看到了管道函数,它只是帮助使从左到右的长函数调用更具可读性。

let value = envoy.get(name) |> result.unwrap("")

这与以下内容相同:

let value = result.unwrap(envoy.get(name),"")

因为 Gleam 不会抛出异常,它使用内置的 Result 类型,而 unwrap 获取正确的路径值。

最后的奇点是:

name <> "=" <> value

…这只是字符串连接。

我在这里运行它,第二次使用必需的参数:

Gleam 没有 null、没有隐式转换,也没有异常。因此,如果它编译,那么你很好。此外,没有数字运算符重载,因此用于添加整数的代码与用于添加浮点数的代码不同:

io.debug(1 + 1) //ints
io.debug(1.0 +. 1.5) //floats

相等适用于任何类型。最好通过使用函数式语言一段时间来体验不变性的总体概念,因此我不会对此进行详细说明。它确实有助于消除一整套错误。

代数数据类型

最后,我们看到了代数数据类型 (ADT) 用于 Virgil,因此我热衷于了解 Gleam 中的等效项如何工作。事实上,我们已经看到了 case 语句的使用。

我们得到自定义类型,我们对其进行模式匹配。因此,我们已经完成了一部分:

pub type Season { 
  Spring 
  Summer
  Autumn  
  Winter 
} 
 
fn weather(season: Season) -> String { 
  case season { 
    Spring -> "Mild" 
    Summer -> "Hot" 
    Autumn -> "Windy" 
    Winter -> "Cold" 
  } 
}

类型可以在记录中保存数据,这就是我们接近我的 Virgil 示例的方式:

import gleam/io
 
pub type Travel {
  Walk(hours: Int)
  Cycle(hours: Int)
  Drive(hours: Int, speed: Int)
}
 
pub fn main() {
  let walking = Walk(1)
  let cycling = Cycle(1)
  let bus_trip = Drive(2, 50)
 
  let trip = [walking, cycling, bus_trip]
  io.debug(trip)
}
 
// [Walk(hours: 1), Cycle(hours: 1), Drive(hours: 2, speed: 50)]

我认为我无法在类型内部关联一个方法,但我可以访问记录值以获得与我们在 Virgil 中获得的结果类似的结果。我将把它留给更流利的用户作为练习!

对于像我这样不太使用函数式代码的人来说,Gleam 非常容易理解,并且不会立即用“柯里化”和其他函数式冲击之类的术语来让我不知所措。但是,如果你还没有成为拥护者,它应该是一种让你欣赏编程的不可变优势的好方法。

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注