[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
就包含了Ok
和Err
两种类型, 这是 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!