日本综合一区二区|亚洲中文天堂综合|日韩欧美自拍一区|男女精品天堂一区|欧美自拍第6页亚洲成人精品一区|亚洲黄色天堂一区二区成人|超碰91偷拍第一页|日韩av夜夜嗨中文字幕|久久蜜综合视频官网|精美人妻一区二区三区

RELATEED CONSULTING
相關(guān)咨詢
選擇下列產(chǎn)品馬上在線溝通
服務時間:8:30-17:00
你可能遇到了下面的問題
關(guān)閉右側(cè)工具欄

新聞中心

這里有您想知道的互聯(lián)網(wǎng)營銷解決方案
編譯器開發(fā)語言選擇:Rust還是OCaml?

譯者 | 劉汪洋

審校 | 重樓

關(guān)于如何選擇最合適的編程語言來開發(fā)編譯器,這個話題在編程語言愛好者中經(jīng)常引起熱議。具體可參考以下討論:鏈接 1、鏈接 2 和鏈接 3。遺憾的是,許多人的回答要么局限于自己偏愛的語言,沒有提供合理解釋,要么給出模糊的解釋卻缺乏具體的例證。這兩種回答對提問者來說幾乎沒有任何幫助。在本文中,我將嘗試通過比較兩種語言——Rust 和 OCaml,來對這個話題提供更詳細的闡述。

CPS 轉(zhuǎn)換

在闡述我的具體觀點之前,我會展示一個非常簡單的語言的 CPS(延續(xù)傳遞風格)轉(zhuǎn)換的兩個相似實現(xiàn),并不做任何結(jié)論性陳述。這一通用方法源于 Andrew W. Appel 的“Compiling with Continuations”。即使你對這個概念不太了解,也不必擔心;你只需要關(guān)注 Rust 和 OCaml 中是如何具體實現(xiàn)這個理念的。

以下是用 Rust 編寫的 CPS 轉(zhuǎn)換:

use std::cell::RefCell;
use std::ops::Deref;
use std::rc::Rc;

// lambda 語言的變量標識符 `Term`。
type Var = String;

// lambda語言;直接風格。
type Term = Rc;

enum TermTree {
    Var(Var),
    Fix(Vec<(Var, Vec, Term)>, Term),
    Appl(Term, Vec),
    Record(Vec),
    Select(Term, u32),
}

use TermTree::*;

#[derive(Clone)]
enum CpsVar {
    // 在 CPS 轉(zhuǎn)換過程中從 lambda 項中獲取。
    CLamVar(Var),
    // 在 CPS 轉(zhuǎn)換過程中唯一生成。
    CGenVar(u32),
}

use CpsVar::*;

// 結(jié)果的 CPS 項。
enum CpsTerm {
    CFix(Vec<(CpsVar, Vec, CpsTerm)>, Box),
    CAppl(CpsVar, Vec),
    CRecord(Vec, Binder),
    CSelect(CpsVar, u32, Binder),
    CHalt(CpsVar),
}

use CpsTerm::*;

// 在 `CpsTerm` 內(nèi)部綁定唯一的 `CpsVar`。
type Binder = (CpsVar, Box);

// 根據(jù)當前的`i` 生成一個唯一的 CPS 變量。
fn gensym(i: RefCell) -> CpsVar {
    let x = CGenVar(i.clone().into_inner());
    i.replace_with(|&mut i| i + 1);
    x
}

// 將`Term`轉(zhuǎn)換為`CpsTerm`,并將`finish`應用于結(jié)果的CPS變量。
fn convert(gen: RefCell, finish: impl FnOnce(CpsVar) -> CpsTerm, term: Term) -> CpsTerm {
    match term.deref() {
        Var(x) => finish(CLamVar(x.to_string())),
        Fix(defs, m) => CFix(
            defs.iter()
            .map(|def| convert_def(gen.clone(), def.clone()))
            .collect(),
            Box::new(convert(gen, finish, m.clone())),
        ),
        Appl(f, args) => {
            let ret_k = gensym(gen.clone());
            let ret_k_x = gensym(gen.clone());
            CFix(
                vec![(ret_k.clone(), vec![ret_k_x.clone()], finish(ret_k_x))],
                Box::new(convert(
                    gen.clone(),
                    |f_cps| {
                        convert_list(
                            gen,
                            |args_cps| {
                                CAppl(f_cps, args_cps.into_iter().chain(vec![ret_k]).collect())
                            },
                            args,
                        )
                    },
                    f.clone(),
                )),
            )
        }
        Record(fields) => convert_list(
            gen.clone(),
            |fields_cps| {
                let x = gensym(gen);
                CRecord(fields_cps, (x.clone(), Box::new(finish(x))))
            },
            fields,
        ),
        Select(m, i) => convert(
            gen.clone(),
            |m_cps| {
                let x = gensym(gen);
                CSelect(m_cps, *i, (x.clone(), Box::new(finish(x))))
            },
            m.clone(),
        ),
    }
}

// 將`Vec`轉(zhuǎn)換為`Vec`并 調(diào)用`finish`
fn convert_list(
    gen: RefCell,
    finish: impl FnOnce(Vec) -> CpsTerm,
    terms: &[Term],
) -> CpsTerm {
    fn go(
        gen: RefCell,
        finish: impl FnOnce(Vec) -> CpsTerm,
        mut acc: Vec,
        terms: &[Term],
    ) -> CpsTerm {
        match terms.split_first() {
            None => finish(acc),
            Some((x, xs)) => convert(
                gen.clone(),
                |x_cps| {
                    acc.push(x_cps);
                    go(gen, finish, acc, xs)
                },
                x.clone(),
            ),
        }
    }
    let acc = Vec::with_capacity(terms.len());
    go(gen, finish, acc, terms)
}


// 將單個函數(shù)定義轉(zhuǎn)換為其 CPS 形式。
fn convert_def(
    gen: RefCell,
    (f, params, m): (Var, Vec, Term),
) -> (CpsVar, Vec, CpsTerm) {
    let k = gensym(gen.clone());
    (
        CLamVar(f),
        params
            .into_iter()
            .map(CLamVar)
            .chain(std::iter::once(k.clone()))
            .collect(),
        convert(gen, |m_cps| CAppl(k, vec![m_cps]), m),
    )
}

代碼包括注釋和空行共 145 行。

同樣的算法用地道的 OCaml 編寫:

(* lambda語言中的變量標識符[term]。 *)
type var = string

(* lambda語言;直接風格。 *)
type term =
  | Var of var
  | Fix of (var * var list * term) list * term
  | Appl of term * term list
  | Record of term list
  | Select of term * int

type cps_var =
  (* 在CPS轉(zhuǎn)換過程中從lambda項中提取。 *)
  | CLamVar of var
  (* 在CPS轉(zhuǎn)換過程中獨特生成。 *)
  | CGenVar of int

(* 生成的CPS項。 *)
type cps_term =
  | CFix of (cps_var * cps_var list * cps_term) list * cps_term
  | CAppl of cps_var * cps_var list
  | CRecord of cps_var list * binder
  | CSelect of cps_var * int * binder
  | CHalt of cps_var

(* 在[cps_term]內(nèi)部綁定唯一的[cps_var]。 *)
and binder = cps_var * cps_term

(* 根據(jù)當前的[i]生成唯一的CPS變量。 *)
let gensym i =
  let x = CGenVar !i in
  i := !i + 1;
  x

(* 將[term]轉(zhuǎn)換為[cps_term],并將[finish]應用于生成的CPS變量。 *)
let rec convert gen finish = function
  | Var x -> finish (CLamVar x)
  | Fix (defs, m) -> CFix (List.map (convert_def gen) defs, convert gen finish m)
  | Appl (f, args) ->
      let ret_k = gensym gen in
      let ret_k_x = gensym gen in
      CFix
        ( [ (ret_k, [ ret_k_x ], finish ret_k_x) ],
          f
          |> convert gen (fun f_cps ->
                 args
                 |> convert_list gen (fun args_cps ->
                        CAppl (f_cps, args_cps @ [ ret_k ]))) )
  | Record fields ->
      fields
      |> convert_list gen (fun fields_cps ->
             let x = gensym gen in
             CRecord (fields_cps, (x, finish x)))
  | Select (m, i) ->
      m
      |> convert gen (fun m_cps ->
             let x = gensym gen in
             CSelect (m_cps, i, (x, finish x)))


(* 將[term list]轉(zhuǎn)換為[cps_var list]并將[finish]應用于它。 *)
and convert_list gen finish =
  let rec go acc = function
    | [] -> finish (List.rev acc)
    | x :: xs -> x |> convert gen (fun x_cps -> go (x_cps :: acc) xs)
  in
  go []

(* 將單個函數(shù)定義轉(zhuǎn)換為其CPS形式。 *)
and convert_def gen (f, params, m) =
  let k = gensym gen in
  ( CLamVar f,
    List.map (fun x -> CLamVar x) params @ [ k ],
    m |> convert gen (fun m_cps -> CAppl (k, [ m_cps ])) )

代碼包括注釋和空行總共有 74 行。這比 Rust 版本短了大概一半。

比較兩種實現(xiàn)

開發(fā)的核心特點涵蓋了:

  1. 大量使用遞歸定義的數(shù)據(jù)結(jié)構(gòu)。
  2. 復雜的數(shù)據(jù)轉(zhuǎn)換流程。

下面簡要概述 Rust 和 OCaml 在這兩方面的處理差異:

1.遞歸數(shù)據(jù)結(jié)構(gòu):

  • OCaml:直接支持遞歸數(shù)據(jù)結(jié)構(gòu)。
  • Rust:遞歸數(shù)據(jù)結(jié)構(gòu)的實現(xiàn)需要借助Rc 和 Box將TermTree和CpsTerm的遞歸結(jié)構(gòu)進行封裝。

2.復雜數(shù)據(jù)轉(zhuǎn)換:

  • OCaml
  • 遞歸廣泛應用,擁有尾調(diào)用和“ Tail Modulo Constructor (TMC )”優(yōu)化。
  • 模式匹配的實現(xiàn)便捷高效,無需額外縮進和復雜的參數(shù)描述。
  • 標準數(shù)據(jù)結(jié)構(gòu)主要為不可變類型,有助于代碼理解。
  • Rust
  • 遞歸使用較少,且并不保證尾遞歸。
  • 模式匹配的實現(xiàn)相對復雜,涉及額外的縮進和參數(shù)詳述。
  • 標準數(shù)據(jù)結(jié)構(gòu)大多為可變類型,傾向于使用命令式風格,需要進行迭代操作才能實現(xiàn)流水線編程。

與 OCaml 相比,Rust 的語法更冗長。作為一門無垃圾回收的語言,它要求開發(fā)者必須精確處理內(nèi)存管理。這確實增加了對執(zhí)行過程的透明度,但對理解算法本身的幫助卻不明顯。

在 Rust 中,修改變量或數(shù)據(jù)結(jié)構(gòu)的值也相對復雜:

fn gensym(i: RefCell) -> CpsVar {
    let x = CGenVar(i.clone().into_inner());
    i.replace_with(|&mut i| i + 1);
    x
}

與之相比,在 OCaml 中比較簡單:

let gensym i =
  let x = CGenVar !i in
  i := !i + 1;
  x

為何選擇RefCell而非&mut u32?因為 Rust 強制在任何時刻只允許一個可變引用指向特定值。盡管在多線程代碼中這是合理的,但在單線程的算法中,我們需用RefCell來繞過這個限制。

另外,Rust 中convert_list的實現(xiàn)也值得注意。由于fn本質(zhì)上只是代碼指針,所以我們每次調(diào)用go都必須明確傳遞gen和finish,導致了變量類型的重復聲明。與之相對,OCaml則會自動捕獲gen和finish。

雖然上述算法不算特別復雜,但已經(jīng)足以體現(xiàn) ML 家族語言在編程方面的便利性。下面,我們將深入探討兩種語言類型系統(tǒng)的更多細節(jié)。

類型安全性:GADTs

與 Rust 相比,OCaml 的類型系統(tǒng)通常更具表現(xiàn)力,并且在資源管理之外具有更多優(yōu)勢。例如,OCaml 支持廣義代數(shù)數(shù)據(jù)類型(GADTs),以強化數(shù)據(jù)結(jié)構(gòu)的某些不變性??紤]一種包含布爾值、整數(shù)及其操作的對象語言,其描述如下:

enum Term {
    Bool(bool),
    Not(Box),
    And(Box, Box),
    Int(i32),
    Neg(Box),
    Add(Box, Box),
}

enum Value {
    Bool(bool),
    Int(i32),
}

那么,我們該如何編寫該語言的求值器呢?以下是一個可能的解決方案:

fn eval(term: &Term) -> Value {
    use Term::*;

    match term {
        Bool(b) => Value::Bool(*b),
        Not(m) => match eval(m) {
            Value::Bool(b) => Value::Bool(!b),
            _ => panic!("`Not`運算符應用于非布爾值"),
        },
        And(m, n) => match (eval(m), eval(n)) {
            (Value::Bool(b1), Value::Bool(b2)) => Value::Bool(b1 && b2),
            _ => panic!("`And`運算符應用于非布爾值"),
        },
        Int(i) => Value::Int(*i),
        Neg(m) => match eval(m) {
            Value::Int(i) => Value::Int(-i),
            _ => panic!("`Neg`運算符應用于非整數(shù)值"),
        },
        Add(m, n) => match (eval(m), eval(n)) {
            (Value::Int(i1), Value::Int(i2)) => Value::Int(i1 + i2),
            _ => panic!("`Add`運算符應用于非整數(shù)值"),
        },
    }
}

雖然該解決方案相對簡單,但并不夠穩(wěn)健。如果對eval的輸入類型不合適會怎樣呢?以下示例說明了問題:

fn main() {
    use Term::*;
    let term = Not(Box::new(And(Box::new(Bool(true)), Box::new(Int(42)))));
    dbg!(eval(&term));
}

程序會因為“And運算符的第二操作數(shù)必須為布爾值”而出現(xiàn)問題。

為了避免此類錯誤,我們可以在 OCaml 中采用 GADTs :

type _ term =
  | Bool : bool -> bool term
  | Not : bool term -> bool term
  | And : bool term * bool term -> bool term
  | Int : int -> int term
  | Neg : int term -> int term
  | Add : int term * int term -> int term

let rec eval : type a. a term -> a = function
  | Bool b -> b
  | Not m -> not (eval m)
  | And (m, n) ->
      let b1, b2 = (eval m, eval n) in
      b1 && b2
  | Int i -> i
  | Neg m -> -eval m
  | Add (m, n) ->
      let i1, i2 = (eval m, eval n) in
      i1 + i2

現(xiàn)在,嘗試構(gòu)造一個不合適的類型會是什么情況呢?

let () =
  let _term = Not (And (Bool true, Int 42)) in
  ()

類型檢查不會通過!

File "bin/main.ml", line 22, characters 35-41:
22 |   let _term = Not (And (Bool true, Int 42)) in
                                        ^^^^^^
Error: This expression has type int term
       but an expression was expected of type bool term
       Type int is not compatible with type bool

之所以會這樣,是因為我們在term的定義中實質(zhì)上編碼了對象語言的類型系統(tǒng)。OCaml 知道And只接受布爾類型的項,而不是整數(shù)類型。在實際應用場景中,我們可以有一個無限制的、類似 Rust 的Term項,該項是解析生成的,并可進一步詳細闡述為合適的 GADT 風格的term。無論采用eval還是compile,后者均可被處理。

類型靈活性:First-Class Modules

OCaml 中具備一項 Rust 所不具備的獨特功能:First-Class Modules。First-Class Modules允許模塊存儲于變量、作為參數(shù)傳遞,甚至可以從常規(guī)函數(shù)返回。假設(shè)你的目標語言涵蓋了各種固定大小的整數(shù),如i8/u8、i16/u16等,那么你可以通過 OCaml 中的(常規(guī))模塊來表示這些類型:

fixed_ints.mli

(* [u8], [u16]等由我們定義。*)

module type S = sig
  type t

  val add : t -> t -> t
  val sub : t -> t -> t
  val mul : t -> t -> t
  val div : t -> t -> t
  val rem : t -> t -> t

  (* 這里還有一些操作。*)
end

module U8 : S with type t = u8
module U16 : S with type t = u16
module U32 : S with type t = u32
module U64 : S with type t = u64
module U128 : S with type t = u128
module I8 : S with type t = i8
module I16 : S with type t = i16
module I32 : S with type t = i32
module I64 : S with type t = i64
module I128 : S with type t = i128

在抽象語法樹(AST)中,整數(shù)值可以按照以下方式表示:

type generic =
  | U8 of u8
  | U16 of u16
  | U32 of u32
  | U64 of u64
  | U128 of u128
  | I8 of i8
  | I16 of i16
  | I32 of i32
  | I64 of i64
  | I128 of i128

那么,在存在諸多算術(shù)運算符add/sub/mul/div/rem和多種類型的操作數(shù)時,該如何高效地實現(xiàn)評估呢?

解決方案如下:

  1. 定義函數(shù)pair_exn,將兩個generic映射為一個一等模塊Pair。
  2. 為特定的整數(shù)對定義模塊Pair,并實現(xiàn)S。
  3. 定義函數(shù)do_int_bin_op,接收Pair作為參數(shù),并執(zhí)行整數(shù)對上的操作op。
  4. 從eval中調(diào)用do_int_bin_op。

在 OCaml 中:

fixed_ints.mli

module type Pair = sig
  type t

  include S with type t := t

  val pair : t * t
end

val pair_exn : generic * generic -> (module Pair)

pair的實現(xiàn)如下:

fixed_ints.ml

let pair_exn =
  let make (type a) (module S : S with type t = a) (x, y) =
    (module struct
      include S

      let pair = x, y
    end : Pair)
  in
  function
  | U8 x, U8 y -> make (module U8) (x, y)
  | U16 x, U16 y -> make (module U16) (x, y)
  | U32 x, U32 y -> make (module U32) (x, y)
  | U64 x, U64 y -> make (module U64) (x, y)
  | U128 x, U128 y -> make (module U128) (x, y)
  | I8 x, I8 y -> make (module I8) (x, y)
  | I16 x, I16 y -> make (module I16) (x, y)
  | I32 x, I32 y -> make (module I32) (x, y)
  | I64 x, I64 y -> make (module I64) (x, y)
  | I128 x, I128 y -> make (module I128) (x, y)
  | _, _ -> raise (invalid_arg "Fixed_ints.pair_exn")
;;

現(xiàn)在,我們可以按如下方式編寫 eval:

(* 在 [eval] 的定義中的某處。*)
| IntBinOp (op, ty, m, n) ->
  let x = extract_int_exn (eval m) in
  let y = extract_int_exn (eval n) in
  let (module Pair) = Fixed_ints.pair_exn (x, y) in
  do_int_bin_op op (module Pair)

extract_int_exn 取出一個值,并提取一個整型 generic,如果該值不是整數(shù)則拋出異常。

最后,do_int_bin_op 定義如下:

let do_int_bin_op op (module S : Fixed_ints.Pair) =
  let x, y = S.pair in
  match op with
  | Add -> S.add x y |> S.to_value
  | Sub -> S.sub x y |> S.to_value
  | Mul -> S.mul x y |> S.to_value
  | Div -> S.div x y |> S.to_value
  | Rem -> S.rem x y |> S.to_value
;;

S.to_value 將類型化的整數(shù)轉(zhuǎn)換回持有 generic 的值。

通過借助 First-Class Modules,我們能夠在無需過多樣板代碼的前提下實現(xiàn)固定大小整數(shù)的評估。而在 Rust 中的最佳實踐則是使用macro_rules!。然而,該方法因其難以理解的語法、與編程語言其他部分集成不深入,以及較差的 IDE 支持而備受詬病。

結(jié)束語

雖然 Rust 在資源管理方面表現(xiàn)優(yōu)秀,但 OCaml 更適合于編譯器的開發(fā)。這其中涉及許多引人注目的特性,如多態(tài)變體、自定義綁定操作符和Effect Handlers。得益于完全靜態(tài)且靈活的類型系統(tǒng),OCaml 在歷史上已廣泛應用于眾多項目,包括  Frama-C 工具鏈、Coq 定理證明器,以及 Rust 編譯器的早期版本。

然而,OCaml 也不是完美的。 OCaml 的標準庫和整體生態(tài)系統(tǒng)與 Rust 相比顯得略遜一籌。在 OCaml 中直接使用 Rust 的完整固定大小整數(shù)集合并不可行。不過,通過整合原生 OCaml 整數(shù)、Int32、Int64模塊和 C FFI,可以達到同樣的效果。(專業(yè)提示:避免使用[ocaml-stdint],截至 2023 年 8 月 6 日,該庫未維護且存在多個錯誤。[ocaml-integers]是更穩(wěn)定的選擇,但缺乏Int8、Int16和 128 位整數(shù)的支持,并在文檔方面有所不足。)

隨著 Rust 的日益普及,更多的開發(fā)者開始在 GitHub 上使用它來啟動編譯器項目。我認為,如果你試圖借助 Rust 編寫大量編譯器來深入學習 Rust ,或者確切了解自己的目標,這可能是明智之舉。 如果你主要關(guān)注的是編譯器開發(fā),那么 OCaml 將能夠為你節(jié)省大量時間和精力。

其他值得考慮的選項包括 Haskell 和各類 Lisp 方言。 如果你已經(jīng)熟練掌握了 Haskell(對此我同時表示祝賀和哀悼),那么僅為了編寫編譯器而學習 OCaml 可能是不必要的。如果你尚未掌握 Haskell,OCaml 可能是更容易上手的選擇。盡管 Lisps 極具靈活性, 但由于它們通常缺少靜態(tài)類型安全性,運行時錯誤可能成為一個棘手問題。

參考文獻

  1. CPS 是編譯器 Standard ML of New Jersey 的核心表示形式。
  2. 代碼和附帶的測試可在此處訪問。
  3. 選擇 Rc 是為了減輕在某些地方昂貴的克隆 TermTree 的負擔。
  4. 從嚴格意義上講,OCaml 中的所有函數(shù)都是柯里化(Currying)的,因此 function 只是定義了一個單一參數(shù)的函數(shù),并在其上進行模式匹配。
  5. 在此處閉包未能提供解決方案,因為它們不能遞歸調(diào)用,至少在未進行一些復雜操作之前不能調(diào)用。

廣大網(wǎng)友激烈討論

網(wǎng)友們圍繞 Rust 和 OCaml 的優(yōu)劣以及如何選擇展開了激烈討論。
關(guān)于 Rust和 OCaml 優(yōu)劣對比。有些網(wǎng)友認為 Rust 的優(yōu)點在于其內(nèi)存安全性和性能,這使得它在系統(tǒng)編程和高性能計算中非常有用。然而,Rust 的學習曲線相對較陡,可能需要更多的時間和精力去掌握。有網(wǎng)友表示,OCaml 在函數(shù)式編程和類型推斷方面非常強大,它的語法簡潔,易于學習。然而,OCaml 的生態(tài)系統(tǒng)相對較小,可能沒有 Rust 那么多的庫和工具可供選擇。也有網(wǎng)友認為,Rust 和 OCaml 都有一些獨特的特性,如 Rust 的所有權(quán)系統(tǒng)和 OCaml 的模式匹配,這些特性在編譯器開發(fā)中可能非常有用。
關(guān)于如何選擇。有網(wǎng)友認為,如果項目的主要目標是快速開發(fā)和原型設(shè)計,那么 OCaml 可能是更好的選擇。如果項目需要處理復雜的并發(fā)和內(nèi)存管理問題,那么 Rust 可能更適合。也有網(wǎng)友認為,Rust 和 OCaml 都是優(yōu)秀的編程語言,但在編譯器開發(fā)中,它們各有優(yōu)勢和劣勢,選擇編程語言不僅僅是技術(shù)問題,還涉及到團隊動力、項目管理和人力資源等多個方面。因此,選擇哪種語言需要綜合考慮多個因素。

譯者介紹

劉汪洋,社區(qū)編輯,昵稱:明明如月,一個擁有 5 年開發(fā)經(jīng)驗的某大廠高級 Java 工程師,擁有多個主流技術(shù)博客平臺博客專家稱號。

標題:Compiler Development: Rust or OCaml?,作者:Hirrolot


當前標題:編譯器開發(fā)語言選擇:Rust還是OCaml?
轉(zhuǎn)載來源:http://www.dlmjj.cn/article/dpsjhhs.html