4 个版本 (破坏性更新)

0.5.0 2023 年 7 月 30 日
0.4.0 2023 年 7 月 30 日
0.3.0 2023 年 7 月 5 日
0.2.0 2023 年 7 月 3 日

#1764 in Rust 模式


2 个 crate 中使用 (通过 steel-interpreter)

MIT/Apache

2MB
40K SLoC

Steel

在 Rust 中构建的可嵌入和可扩展的 scheme 方言。

Actions Status Coverage Status Discord Chat

在 Playgroud 中尝试 · 阅读 Steel 书籍 (WIP)

入门

此 GitHub 仓库包含一个 CLI 解释器。要在线 Playgroud 中尝试,请访问 Steel Playgroud。要开始使用 crate 的 repl,请确保您已安装 rust。

然后,克隆仓库并运行以下命令

cargo run

这将启动一个类似于这样的 REPL 实例

如果您想安装和使用包,请设置 STEEL_HOME 环境变量。这将作为包安装的位置。Steel 目前不假设任何默认值。

关于

Steel 是一个可嵌入的 scheme 解释器,还包括一个独立的 CLI。受 Racket 启发很大,该语言旨在成为嵌入式应用程序中或单独使用的高性能函数的有用 scheme 变体。语言实现本身包含一个基于 syntax-rules 风格的相当强大的宏系统和一个字节码虚拟机。目前,它并不明确符合任何特定的 scheme 规范。

警告 API 不稳定,没有保证,可能在 1.0 之前随时更改。肯定存在一些错误,任何重大错误报告都将得到快速处理。尽管如此,我本人仍然将其用作许多脚本任务的日常驱动器。

功能

  • 支持有限的 syntax-rules 风格宏
  • 易于与 Rust 函数和结构体集成
  • 可以从 Rust 或通过单独的文件轻松调用脚本
  • 高效 - 常用函数和数据结构经过性能优化(如 mapfilter 等)
  • 高阶合约
  • 内置不可变数据结构包括
    • 列表
    • 向量
    • 哈希表
    • 哈希集

合约

受 Racket 的高阶合约启发,Steel 实现了高阶合约,以实现通过合约进行设计,通过 define/contract 宏来简化用户体验。Racket 利用一个名为 blame 的概念来识别违反方 - Steel 并没有完全实现 blame,但这是一个正在进行中的工作。以下是一些示例

;; Simple flat contracts
(define/contract (test x y)
    (->/c even? even? odd?)
    (+ x y 1))

(test 2 2) ;; => 5

(define/contract (test-violation x y)
    (->/c even? even? odd?)
    (+ x y 1))

(test-violation 1 2) ;; contract violation

合约作为 实现,因此它们绑定到函数上。这使得可以在函数本身上使用合约检查,因为函数可以传递

;; Higher order contracts, check on application
(define/contract (higher-order func y)
    (->/c (->/c even? odd?) even? even?)
    (+ 1 (func y)))

(higher-order (lambda (x) (+ x 1)) 2) ;; => 4

(define/contract (higher-order-violation func y)
    (->/c (->/c even? odd?) even? even?)
    (+ 1 (func y)))

(higher-order-violation (lambda (x) (+ x 2)) 2) ;; contract violation

函数上的合约只有在应用时才会检查,因此返回 合约 函数的函数不会在函数实际使用之前引起违反

;; More higher order contracts, get checked on application
(define/contract (output)
    (->/c (->/c string? int?))
    (lambda (x) 10))

(define/contract (accept func)
    (->/c (->/c string? int?) string?)
    "cool cool cool")

(accept (output)) ;; => "cool cool cool"

;; different contracts on the argument
(define/contract (accept-violation func)
    (->/c (->/c string? string?) string?)
    (func "applesauce")
    "cool cool cool")

(accept-violation (output)) ;; contract violation

;; generates a function
(define/contract (generate-closure)
    (->/c (->/c string? int?))
    (lambda (x) 10))

;; calls generate-closure which should result in a contract violation
(define/contract (accept-violation)
    (->/c (->/c string? string?))
    (generate-closure))

((accept-violation) "test") ;; contract violation

可能是一个更微妙的例子

(define/contract (output)
    (->/c (->/c string? int?))
    (lambda (x) 10.2))

(define/contract (accept)
    (->/c (->/c string? number?))
    (output))


((accept) "test") ;; contract violation 10.2 satisfies number? but _not_ int?

* 非常正在进行中

转换器

受 clojure 的转换器的启发,Steel 有一个类似的对象,它在转换器和迭代器之间。考虑以下


(mapping (lambda (x) (+ x 1))) ;; => <#iterator>
(filtering even?) ;; => <#iterator>
(taking 15) ;; => <#iterator>

(compose 
    (mapping add1)
    (filtering odd?)
    (taking 15)) ;; => <#iterator>

这些表达式中的每一个都会发出一个 <#iterator> 对象,这意味着它们与 transduce 兼容。 transduce 接受一个转换器(即 <#iterator>)和一个可迭代的集合(如 listvectorstreamhashsethashmapstringstruct)并将转换器应用于它。

;; Accepts lists
(transduce (list 1 2 3 4 5) (mapping (lambda (x) (+ x 1))) (into-list)) ;; => '(2 3 4 5 6)

;; Accepts vectors
(transduce (vector 1 2 3 4 5) (mapping (lambda (x) (+ x 1))) (into-vector)) ;; '#(2 3 4 5 6)

;; Even accepts streams!
(define (integers n)
    (stream-cons n (lambda () (integers (+ 1 n)))))

(transduce (integers 0) (taking 5) (into-list)) ;; => '(0 1 2 3 4)

Transduce 还接受一个还原函数。上面我们使用了 into-listinto-vector,但下面我们可以使用任何任意的还原器

;; (-> transducer reducing-function initial-value iterable)
(transduce (list 0 1 2 3) (mapping (lambda (x) (+ x 1))) (into-reducer + 0)) ;; => 10

组合只是将迭代器函数组合起来,使我们能够避免中间分配。组合从左到右工作 - 它将每个值通过函数传递,然后累积到输出类型中。请看以下

(define xf 
    (compose 
        (mapping add1)
        (filtering odd?)
        (taking 5)))

(transduce (range 0 100) xf (into-list)) ;; => '(1 3 5 7 9)

语法选择

Steel 在变量和函数定义方面有轻微的意见,有几种方式。除了我从 Racket 借用的简写函数语法之外,这些选择相当任意。我非常鼓励使用 defnfn,因为这样可以少打一些字。


;; All of the following are equivalent
(define (foo x) (+ x 1))
(define foo (lambda (x) (+ x 1)))
(defn (foo x) (+ x 1))
(defn foo (lambda (x) (+ x 1)))

;; All of the following are equivalent
(lambda (x) (+ x 1))
(λ (x) (+ x 1))
(fn (x) (+ x 1))

模块

为了支持不断增长的代码库,Steel 支持跨多个文件的项目的模块。Steel 文件可以 provide 值(附带合约)并从其他文件 require 模块

;; main.scm
(require "provide.scm")

(even->odd 10)


;; provide.scm
(provide 
    (contract/out even->odd (->/c even? odd?))
    no-contract
    flat-value)

(define (even->odd x) 
    (+ x 1))

(define (accept-number x) (+ x 10))

(define (no-contract) "cool cool cool")
(define flat-value 15)

(displayln "Calling even->odd with some bad inputs but its okay")
(displayln (even->odd 1))

在这里,如果我们运行 main,它将包括 provide 的内容,并且只有提供的值才能从 main 访问。合约在合约边界处附加,因此在 provide 模块内部可以违反合约,但模块外部将应用合约。

关于模块的几点说明

  • 不允许循环依赖
  • 模块将被编译一次并在多个文件中使用。如果 A 需要 BC,而 B 需要 C,则 C 将仅编译一次并在 AB 之间共享。
  • 当模块更改时,将重新编译模块,并且任何依赖的文件也将根据需要重新编译

性能

初步基准测试显示在我机器上的以下结果

基准测试 Steel Python
(fib 28) 63.383ms 65.10 ms
(ack 3 3) 0.303 ms 0.195 ms

在虚拟机中嵌入Rust值的示例

Rust值、类型和函数可以轻松嵌入到Steel中。使用register_fn调用,您可以轻松地嵌入函数

use steel_vm::engine::Engine;
use steel_vm::register_fn::RegisterFn;

fn external_function(arg1: usize, arg2: usize) -> usize {
    arg1 + arg2
}

fn option_function(arg1: Option<String>) -> Option<String> {
    arg1
}

fn result_function(arg1: Option<String>) -> Result<String, String> {
    if let Some(inner) = arg1 {
        Ok(inner)
    } else {
        Err("Got a none".to_string())
    }
}

pub fn main() {
    let mut vm = Engine::new();

    // Here we can register functions
    // Any function can accept parameters that implement `FromSteelVal` and
    // return values that implement `IntoSteelVal`
    vm.register_fn("external-function", external_function);

    // See the docs for more information about `FromSteelVal` and `IntoSteelVal`
    // but we can see even functions that accept/return Option<T> or Result<T,E>
    // can be registered
    vm.register_fn("option-function", option_function);

    // Result values will map directly to errors in the VM and bubble back up
    vm.register_fn("result-function", result_function);

    vm.run(
        r#"
        (define foo (external-function 10 25))
        (define bar (option-function "applesauce"))
        (define baz (result-function "bananas"))
    "#,
    )
    .unwrap();

    let foo = vm.extract::<usize>("foo").unwrap();
    println!("foo: {}", foo);
    assert_eq!(35, foo);

    // Can also extract a value by specifying the type on the variable
    let bar: String = vm.extract("bar").unwrap();
    println!("bar: {}", bar);
    assert_eq!("applesauce".to_string(), bar);

    let baz: String = vm.extract("baz").unwrap();
    println!("baz: {}", baz);
    assert_eq!("bananas".to_string(), baz);
}

我们还可以嵌入结构体本身

use steel_vm::engine::Engine;
use steel_vm::register_fn::RegisterFn;

use steel_derive::Steel;

// In order to register a type with Steel,
// it must implement Clone, Debug, and Steel
#[derive(Clone, Debug, Steel, PartialEq)]
pub struct ExternalStruct {
    foo: usize,
    bar: String,
    baz: f64,
}

impl ExternalStruct {
    pub fn new(foo: usize, bar: String, baz: f64) -> Self {
        ExternalStruct { foo, bar, baz }
    }

    // Embedding functions that take self by value
    pub fn method_by_value(self) -> usize {
        self.foo
    }

    pub fn method_by_reference(&self) -> usize {
        self.foo
    }

    // Setters should update the value and return a new instance (functional set)
    pub fn set_foo(mut self, foo: usize) -> Self {
        self.foo = foo;
        self
    }
}

pub fn main() {
    let mut vm = Engine::new();

    // Registering a type gives access to a predicate for the type
    vm.register_type::<ExternalStruct>("ExternalStruct?");

    // Structs in steel typically have a constructor that is the name of the struct
    vm.register_fn("ExternalStruct", ExternalStruct::new);

    // register_fn can be chained
    vm.register_fn("method-by-value", ExternalStruct::method_by_value)
        .register_fn("method-by-reference", ExternalStruct::method_by_reference)
        .register_fn("set-foo", ExternalStruct::set_foo);

    let external_struct = ExternalStruct::new(1, "foo".to_string(), 12.4);

    // Registering an external value is fallible if the conversion fails for some reason
    // For instance, registering an Err(T) is fallible. However, most implementation outside of manual
    // ones should not fail
    vm.register_external_value("external-struct", external_struct)
        .unwrap();

    let output = vm
        .run(
            r#"
            (define new-external-struct (set-foo external-struct 100))
            (define get-output (method-by-value external-struct))
            (define second-new-external-struct (ExternalStruct 50 "bananas" 72.6))
            "last-result"
        "#,
        )
        .unwrap();

    let new_external_struct = vm.extract::<ExternalStruct>("new-external-struct").unwrap();
    println!("new_external_struct: {:?}", new_external_struct);
    assert_eq!(
        ExternalStruct::new(100, "foo".to_string(), 12.4),
        new_external_struct
    );

    // Can also extract a value by specifying the type on the variable
    let get_output: usize = vm.extract("get-output").unwrap();
    println!("get_output: {}", get_output);
    assert_eq!(1, get_output);

    let second_new_external_struct: ExternalStruct =
        vm.extract("second-new-external-struct").unwrap();
    println!(
        "second_new_external_struct: {:?}",
        second_new_external_struct
    );
    assert_eq!(
        ExternalStruct::new(50, "bananas".to_string(), 72.6),
        second_new_external_struct
    );

    // We also get the output of the VM as the value of every expression run
    // we can inspect the results just by printing like so
    println!("{:?}", output);
}

请参阅示例文件夹以获取更多关于嵌入值和与外部世界交互的示例。

许可证

以下任一许可证下授权

任选其一。

贡献

除非您明确声明,否则根据Apache-2.0许可证定义,您有意提交以包含在作品中的任何贡献,将按照上述方式双授权,不附加任何额外的条款或条件。

请参阅CONTRIBUTING.md

依赖

~43–60MB
~1M SLoC