仿生电子锈会梦到自己变成纯函数式吗:gleam 语言初见

Gleam is a friendly language for building type-safe systems that scale! —— https://gleam.run/

虽然我倒不觉得纯函数式语言能有多 friendly……

事情的起因是刷到知乎提问如何评价 gleam 语言,然后有人说在 rust 和 go 之间应该有一门中间态的语言 balabala

于是就跑过去看了,最开始看语法如此像 rust,再加上安利的描述,还以为是 GC 的过程式语言加点函数式糖,直到看到 Gleam is an immutable language 才发现啊被骗了怎么是纯函数式()

先来看看 hello world:

import gleam/io

pub fn main() {
  io.println("Hello, world!")
}

非常好懂(点头

Immutable

虽然 immutable 比过程式语言通常写起来更容易遇到一些不得不搞一些奇怪的递归函数的情况,但是也是一个不错的选择。

Gleam 会编译到 Erlang(可以编译到 js,但是因为语言特性很容易爆递归),听说过 Erlang 的人大概都知道这个传闻:写 Erlang/Elixir 的人习以为常地开几万甚至几十万个进程[1],这也是得益于 immutable 的特性让并发操作变得非常简单好写

对于 mutable 的语言,需要担忧一块内存空间被并发地同时读和写,进而引出了一堆的互斥锁/原子操作之类的概念,而 immutable 根本不需要担心这个问题,因为你根本没有编写“覆写”一块内存区域的代码,(虽然 Erlang 虚拟机可能会优化这个操作,从而实际上覆写),所有的变量都是只读的。

这也是大多数 immutable 函数式语言共享的优势,许多纯函数连状态都没有,自然也就不存在状态不一致的问题。对于函数式而言并发、scale 都是天然的事情。

Pipeline

Gleam 支持 pipeline 操作

我是链式调用狂热粉丝(?)所以非常需要这个!对我来说一个语言能吸引我的一个重要的 sugar 就是类似 pipeline 的机制 i.e.

(|>) :: a -> (a -> b) -> b
x |> f = f x

这意味着任何一个 f:abf: a \to b 都能 pipeline 一个 aa 进去得到 bb,非常好用,因为很多现实世界的操作本来就是串联起来的

比如你可以在 gleam 中写这样的:

import gleam/io
import gleam/string

pub fn main() {
  // Without the pipe operator
  io.debug(string.drop_start(string.drop_end("Hello, Joe!", 1), 7)) // "Joe"

  // With the pipe operator
  "Hello, Mike!"
  |> string.drop_end(1)
  |> string.drop_start(7)
  |> io.debug // "Mike"

  // Changing order with function capturing
  "1"
  |> string.append("2")
  |> string.append("3", _)
  |> io.debug  // "312"
}

中缀表示法 a f b 就是比前缀表示法 f a b 在链式调用下好看多了!这或许是一种比 Ruby 的 Open Class 更好的方案来给对象添加“成员函数”,因为实际上成员函数就是一个 fn(self, ...)

Pattern matching

说到函数式语言最重要的特色当然是 Pattern matching!

Gleam 的 Pattern matching 基本上就是 rust 的形状,不过更强大,和大多数其它函数式语言一样可以匹配一些内置运算:

import gleam/io

pub fn main() {
  io.debug(get_name("Hello, Joe"))            // "Joe"
  io.debug(get_name("Hello, Mike"))           // "Mike"
  io.debug(get_name("System still working?")) // "Unknown"
}

fn get_name(x: String) -> String {
  case x {
    "Hello, " <> name -> name
    _ -> "Unknown"
  }
}

Gleam 也能写类似 Rust 的 let,但是 Gleam 不存在 if 所以没法写 if let 也没有 let mut 这些东西,let 就是一个纯的 irrefutable 的 pattern matching 而已。当然更多的时候可以用来给变量“赋值”()

import gleam/io

pub type Teacher {
  Teacher(name: String, subject: String)
}

pub fn main() {
  let teacher = Teacher("Mr Schofield", "Physics")
  let Teacher(tname, _) = teacher

  io.debug(tname) // "Mr Schofield"
}

只有递归

和很多纯函数式语言一样,Gleam 没有循环结构只有递归,老实说我不觉得这很好,因为这让一些简单的事情变得更复杂……

经典的 list 求 sum 可以写这样的函数

fn sum_list(list: List(Int)) -> Int {
  case list {
    [first, ..rest] -> first + sum_list(rest)
    [] -> 0
  }
}

有过函数式经验的人应该非常好理解,也就是每次取第一位递归地加末尾。写成 haskell 就是:

sumlist :: [Int] -> Int
sumlist [] = 0
sumlist (x:xs) = x + sumlist xs

看上去 gleam 还更长一点,没有 haskell 的韵味 xd

Use sugar

比较喜欢的是 Gleam 的 use sugar,类似 Javascript 的 async/await 一样,负责简化嵌套层级。

Gleam’s use expression helps out here by enabling us to write code that uses callbacks in an unindented style, as shown in the code window.
Gleam 的 use 表达式使我们能够编写以未缩进样式使用回调的代码,从而在此处提供帮助,如代码窗口中所示。

The higher order function being called goes on the right hand side of the <- operator. It must take a callback function as its final argument.
被调用的高阶函数位于 <- 运算符的右侧。它必须将回调函数作为其最终参数。

The argument names for the callback function go on the left hand side of the <- operator. The function can take any number of arguments, including zero.
回调函数的参数名称位于 <- 运算符的左侧。该函数可以接受任意数量的参数,包括零。

All the remaining code in the enclosing {} block becomes the body of the callback function.
封闭的 {} 块中的所有剩余代码都成为回调函数的主体。

直接放官方示例:

import gleam/io
import gleam/result

pub fn main() {
  let _ = io.debug(without_use())
  let _ = io.debug(with_use())
}

pub fn without_use() -> Result(String, Nil) {
  result.try(get_username(), fn(username) {
    result.try(get_password(), fn(password) {
      result.map(log_in(username, password), fn(greeting) {
        greeting <> ", " <> username
      })
    })
  })
}

pub fn with_use() -> Result(String, Nil) {
  use username <- result.try(get_username())
  use password <- result.try(get_password())
  use greeting <- result.map(log_in(username, password))
  greeting <> ", " <> username
}

// Here are some pretend functions for this example:

fn get_username() -> Result(String, Nil) {
  Ok("alice")
}

fn get_password() -> Result(String, Nil) {
  Ok("hunter2")
}

fn log_in(_username: String, _password: String) -> Result(String, Nil) {
  Ok("Welcome")
}

这里其实 use xxx <- result.try(foo()) 翻译成 rust 就是 foo 会返回一个 Result<T, E>,然后

let xxx = foo()?

如果匹配到 Err 就直接返回了,否则再对 Ok(T) 做某种映射

实战:写个 JSON Parser

其实本来是想在 rust 里写 json parser 的但是看到有这么像 rust 的 gleam 以后就干脆拿 gleam 了(

由于 parse number 比较复杂这又是个玩具就不写 number parser 了

首先定义节点的类型,长得很像 rust 的语法(整个语言都很像啊喂!)

pub type Node {
  Nul
  Bol(val: Bool)
  Num(val: Int)
  Str(val: String)
  Arr(val: List(Node))
  Obj(val: dict.Dict(String, Node))
}

我们大致定义一个 parse 函数返回的成功类型可能长这样:

type ParseObjResult {
  ParseObjResult(result: dict.Dict(String, Node), rest: String)
}

result 是 parse 出来的内核,rest 是剩下的字符串,用来继续 parse:

然后就能写出大概这样的 parser

可以看到,use sugar 在简化层级上有多么必要,use 可以极大程度简化代码防止过多的层级嵌套

fn parse_obj(str: String) -> Result(ParseObjResult, String) {
  case str |> string.trim_start {
    "}" <> rest -> Ok(ParseObjResult(dict.from_list([]), rest))
    x -> {
      use ParseStrResult(key, rest) <- result.then(parse_str(x))
      use rest <- result.then(parse_col(rest))
      use ParseNodeResult(node, rest) <- result.then(parse_node(rest))

      case rest |> string.trim_start |> string.pop_grapheme {
        Ok(#(",", next)) ->
          next
          |> string.trim_start
          |> parse_obj
          |> result.map(fn(res) {
            ParseObjResult(res.result |> dict.insert(key, node), res.rest)
          })
        Ok(#("}", rest)) ->
          Ok(ParseObjResult(dict.from_list([#(key, node)]), rest))

        Ok(#(x, _)) -> Error("unexpected token " <> x)
        Error(_) -> Error("unexpected eof")
      }
    }
  }
}


fn parse_node(str: String) -> Result(ParseNodeResult, String) {
  let str = str |> string.trim_start
  case str {
    "null" <> rest -> Ok(ParseNodeResult(Nul, rest))
    "true" <> rest -> Ok(ParseNodeResult(Bol(val: True), rest))
    "false" <> rest -> Ok(ParseNodeResult(Bol(val: False), rest))
    "\"" <> _ ->
      parse_str(str)
      |> result.map(fn(res) { ParseNodeResult(Str(val: res.result), res.rest) })
    "{" <> rest ->
      parse_obj(rest)
      |> result.map(fn(res) { ParseNodeResult(Obj(val: res.result), res.rest) })
    "[" <> rest ->
      parse_arr(rest)
      |> result.map(fn(res) { ParseNodeResult(Arr(val: res.result), res.rest) })
    x -> Error("unexpected token " <> x)
  }
}
完整代码
import gleam/dict
import gleam/io
import gleam/result
import gleam/string

pub type Node {
  Nul
  Bol(val: Bool)
  Num(val: Int)
  Str(val: String)
  Arr(val: List(Node))
  Obj(val: dict.Dict(String, Node))
}

type ParseNodeResult {
  ParseNodeResult(result: Node, rest: String)
}

type ParseStrResult {
  ParseStrResult(result: String, rest: String)
}

type ParseListResult {
  ParseListResult(result: List(Node), rest: String)
}

type ParseObjResult {
  ParseObjResult(result: dict.Dict(String, Node), rest: String)
}

fn read_str(str: String) -> Result(ParseStrResult, String) {
  case str |> string.pop_grapheme {
    Error(Nil) -> Error("Unexpected EOF")
    Ok(#(first, rest)) ->
      case first {
        "\"" -> Ok(ParseStrResult("", rest))
        _ ->
          read_str(rest)
          |> result.map(fn(res) {
            ParseStrResult(first <> res.result, res.rest)
          })
      }
  }
}

fn parse_str(str: String) -> Result(ParseStrResult, String) {
  let str = str |> string.trim_start
  case str {
    "\"" <> content -> read_str(content)
    x -> Error("unexpected token " <> x)
  }
}

fn parse_col(str: String) -> Result(String, String) {
  let str = str |> string.trim_start
  case str {
    ":" <> rest -> Ok(rest |> string.trim_start)
    x -> Error("unexpected token " <> x)
  }
}

fn parse_obj(str: String) -> Result(ParseObjResult, String) {
  case str |> string.trim_start {
    "}" <> rest -> Ok(ParseObjResult(dict.from_list([]), rest))
    x -> {
      use ParseStrResult(key, rest) <- result.then(parse_str(x))
      use rest <- result.then(parse_col(rest))
      use ParseNodeResult(node, rest) <- result.then(parse_node(rest))

      case rest |> string.trim_start |> string.pop_grapheme {
        Ok(#(",", next)) ->
          next
          |> string.trim_start
          |> parse_obj
          |> result.map(fn(res) {
            ParseObjResult(res.result |> dict.insert(key, node), res.rest)
          })
        Ok(#("}", rest)) ->
          Ok(ParseObjResult(dict.from_list([#(key, node)]), rest))

        Ok(#(x, _)) -> Error("unexpected token " <> x)
        Error(_) -> Error("unexpected eof")
      }
    }
  }
}

fn parse_arr(str: String) -> Result(ParseListResult, String) {
  case str |> string.trim_start {
    "]" <> rest -> Ok(ParseListResult([], rest))
    x -> {
      use ParseNodeResult(node, rest) <- result.then(parse_node(x))
      case rest |> string.trim_start |> string.pop_grapheme {
        Ok(#(",", next)) ->
          next
          |> string.trim_start
          |> parse_arr
          |> result.map(fn(res) {
            ParseListResult([node, ..res.result], res.rest)
          })
        Ok(#("]", rest)) -> Ok(ParseListResult([node], rest))

        Ok(#(x, _)) -> Error("unexpected token " <> x)
        Error(_) -> Error("unexpected eof")
      }
    }
  }
}

fn parse_node(str: String) -> Result(ParseNodeResult, String) {
  let str = str |> string.trim_start
  case str {
    "null" <> rest -> Ok(ParseNodeResult(Nul, rest))
    "true" <> rest -> Ok(ParseNodeResult(Bol(val: True), rest))
    "false" <> rest -> Ok(ParseNodeResult(Bol(val: False), rest))
    "\"" <> _ ->
      parse_str(str)
      |> result.map(fn(res) { ParseNodeResult(Str(val: res.result), res.rest) })
    "{" <> rest ->
      parse_obj(rest)
      |> result.map(fn(res) { ParseNodeResult(Obj(val: res.result), res.rest) })
    "[" <> rest ->
      parse_arr(rest)
      |> result.map(fn(res) { ParseNodeResult(Arr(val: res.result), res.rest) })
    x -> Error("unexpected token " <> x)
  }
}

pub fn parse_json(str: String) -> Result(Node, String) {
  case parse_node(str |> string.trim) {
    Error(e) -> Error(e)
    Ok(ParseNodeResult(node, rest)) ->
      case rest {
        "" -> Ok(node)
        _ -> Error("unexpected non EOF")
      }
  }
}

pub fn main() {
  let _ = io.debug(parse_json("null"))
  let _ = io.debug(parse_json("true"))
  let _ = io.debug(parse_json("false"))
  let _ = io.debug(parse_json("\"wwww\""))
  let _ = io.debug(parse_json("{}"))
  let _ = io.debug(parse_json("{\"a\": true, \"b\": [true, false, null]}"))
  let _ = io.debug(parse_json("[ true, false, null, \"abcd\" ]"))

  io.debug("done")
}

还有一些缺陷

可能是我不会用的原因,gleam 莫名不能 production build……build 的产物全是在 dev 文件夹下的,令人担忧它实际的能力

gleam 也不能随便找个文件夹就开始编写单文件代码然后直接 build 一个可执行文件,必须新建一个 project

总结

Gleam 是一个长得很像 rust 的纯函数式语言。由于我不怎么写函数式,写起来还是略微费劲了一点()

像下面 ⬇️ 这样的代码片段显得 gleam 很有魅力,但是因为没有 if,遇到只有两个的情况还不得不 case match 就有点令人抓狂了

use ParseStrResult(key, rest) <- result.then(parse_str(x))
use rest <- result.then(parse_col(rest))
use ParseNodeResult(node, rest) <- result.then(parse_node(rest))

case rest |> string.trim_start |> string.pop_grapheme {
  Ok(#(",", next)) ->
    next
    |> string.trim_start
    |> parse_obj
    |> result.map(fn(res) {
      ParseObjResult(res.result |> dict.insert(key, node), res.rest)
    })
  Ok(#("}", rest)) ->
    Ok(ParseObjResult(dict.from_list([#(key, node)]), rest))

  Ok(#(x, _)) -> Error("unexpected token " <> x)
  Error(_) -> Error("unexpected eof")
}

  1. Elixir 的进程是 Erlang 虚拟机提供的“轻量级”进程,甚至比一般语言的线程还轻量 ↩︎

Previous  Next

Loading...