[elixir! #0042] 与 rust 的初次会面

Colourful 2019-06-21

[elixir! #0042] 与 rust 的初次会面

在看了许多安利 rust 的视频之后, 我决定花一个月的时间来尝试一下 rust. 一周过去了, 我对 Ownership, Lifetime 这些概念还是一头雾水, 但我也不求立刻理解 rust 里所有的概念, 因为它的其它部分同样引人注目.

学习一门新语言, 比较好的方法就是和你已经掌握的语言作对比. “The Rust Programming Language” 中有一个很好的例子. 让我们通过它来对比一下 elixir 和 rust 的异同.

我们知道在 linux 系统中有一个很好用的搜索工具: grep. 这个例子就是模仿 grep 编写一个命令行程序, 对一个 txt 文件进行全文搜索, 返回匹配到的所有行.

新建项目

  • rust
    cargo new --bin greprs

  • elixir
    mix new grepex

两种语言都提供了专门的构建工具, 非常方便.

从命令行读取参数

  • rust

src/main.rs

use std::env;
use std::process;

use greprs::Config;

fn main() {
    let args: Vec<String> = env::args().collect();

    let config = Config::new(&args).unwrap_or_else(|err| {
        println!("Problem parsing arguments: {}", err);
        process::exit(1);
    });

    println!("Searching for {}", config.query);
    println!("In file {}", config.filename);
    
    if let Err(e) = greprs::run(config) {
        println!("Application error: {}", e);

        process::exit(1);
    }
}

src/lib.rs

pub struct Config {
    pub query: String,
    pub filename: String,
}

impl Config {
    pub fn new(args: &[String]) -> Result<Config, &'static str> {
        if args.len() < 3 {
            return Err("not enough arguments");
        }

        let query = args[1].clone();
        let filename = args[2].clone();

        Ok(Config {
            query: query,
            filename: filename,
        })
    }
}
// ...
  • elixir

grepex.exs

import Grepex

args = System.argv()

config = new(Config, args) |> unwrap_or_else(fn err ->
  IO.puts "Problem parsing arguments: #{err}"
  System.stop 1
end)

IO.puts "Searching for #{config.query}"
IO.puts "In file #{config.filename}"

case Grepex.run(config) do
  {:error, e} ->
    IO.puts "Application error: #{e}"
    System.stop 1
  _ -> nil
end

lib/grepex.ex

defmodule Config do
  defstruct [:query, :filename]
end

defmodule Grepex do
  @moduledoc """
  Documentation for Grepex.
  """
  import Config

  def unwrap_or_else({:ok,    result}, _), do: result
  def unwrap_or_else({:error, reason}, f), do: f.(reason)

  def new(Config, args) when length(args) < 2 do
    {:error, "not enough arguments"}
  end
  def new(Config, args) do
    [query, filename|_] = args

    {:ok, %Config{
      query: query,
      filename: filename,
    }}
  end
  # ...

rust 中的 use 类似于 elixir 的 alias. 两种语言中都有 struct, 然而 elixir 作为动态类型语言, 它的 struct 是通过 map 来实现的. 而rust 中, struct 是一种集合类型. unwrap 是 rust 中很常见的一个错误处理的机制, 它的原理很简单, 就是根据参数的类型做出不同的操作. 这里我用 elixir 模拟了一个 unwarp_or_else 函数. 顺便说一下, rust 中有 enum 的概念, 表示几种类型的统称, 例如 Result 就包含了 OkErr 两种类型, 这是 elixir 里没有的.

打开文件读取数据

  • rust

src/lib.rs

pub fn run(config: Config) -> Result<(), Box<Error>>{
    let mut f = File::open(config.filename)?;

    let mut contents = String::new();
    f.read_to_string(&mut contents)?;

    for line in search(&config.query, &contents) {
        println!("{}", line);
    }

    Ok(())
}

// ...
  • elixir

lib/grepex.ex

def run(config) do
    with {:ok, f} <- File.open(config.filename),
         {:ok, contents} <- read_to_string(f) do
      for line <- search(config.query, contents) do
        IO.puts line
      end
      {:ok, nil}
    else
      error ->
        error
    end
  end

  defp read_to_string(file) do
    case IO.binread(file, :all) do
      {:error, _} = e -> e
      data -> {:ok, data}
    end
  end

# ...

rust 中非常注重错误处理, 为此还提供了?作为简写方式. 当函数返回Ok类型时, 会继续执行下面的代码; 而返回Err类型时, 就会不执行之后的代码, 直接返回错误值. 而 elixir 中没有 return 的概念, 但也提供了强大的 with 语句, 使我们可以在中途停止, 并返回错误值.

测试

src/lib.rs

#[cfg(test)]
mod test {
    use super::*;

    #[test]
    fn one_result() {
        let query = "duct";
        let contents = "\
Rust:
safe, fast, productive.
Pick three.";

        assert_eq!(
            vec!["safe, fast, productive."],
            search(query, contents)
        );
    }
}
  • elixir

test/grepex_test.exs

defmodule GrepexTest do
  use ExUnit.Case
  doctest Grepex

  test "one result" do
    query = "cala"
    contents = """
    Elixir:
    scalability, fault-tolerance.
    Functional programming.
    """

    assert Grepex.search(query, contents) ==
      ["scalability, fault-tolerance."]
  end
end

为了遵循TDD, 所以这里先说下test啦. 一般的顺序是先写好一个返回值为空的函数, 然后为该函数编写测试, 再重复 "运行测试, 实现函数", 直到测试通过. rust 将函数定义与测试代码写在同一个文件中, 这样会更方便查看. elixir 则有专门的 test 文件夹, 便于编写大量的测试. 另外, elixir 的 assert 宏十分优雅.

实现search函数

  • rust

src/lib.rs

fn search<'a>(query: &str, contents: &'a str) -> Vec<&'a str> {
    let mut results = Vec::new();

    for line in contents.lines() {
        if line.contains(query) {
            results.push(line);
        }
    }

    results
}
  • elixir

lib/grepex.ex

def search(query, contents) do
    contents
    |> String.split("\n")
    |> do_search(query, [])
  end

  def do_search([], _, result) do
    Enum.reverse result
  end
  def do_search([line|t], query, result) do
    case String.contains? line, query do
      true -> do_search(t, query, [line|result])
      false -> do_search(t, query, result)
    end
  end

很典型的过程式与函数式的区别.

小结

得益于强大的类型系统, rust 的 debug 效率非常高. 接下来的时间里, 我想尝试通过
NIF 把 elixir 和 rust 结合在一起.

Rust is fun!

相关推荐