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 日 |
#776 in 编程语言
在 2 个crate中使用(通过 steel-interpreter)
2MB
39K SLoC
Steel

入门
此 GitHub 仓库包含一个命令行解释器。要在在线 playground 上尝试,请访问 Steel playground。要使用 crates 启动 repl,请确保您已安装 rust。
然后,克隆仓库并运行以下命令
cargo run
这将启动一个类似于以下的 REPL 实例
包
如果您想安装和使用包,请设置 STEEL_HOME
环境变量。这将是要安装包的位置。目前 Steel 不假设任何默认值。
关于
Steel
是一个可嵌入的 scheme 解释器,还包括一个独立的命令行界面。主要受 Racket 启发,该语言旨在成为嵌入应用程序的有益的 scheme 变体,或者可以独立使用,并实现高性能函数。该语言实现本身包含一个相当强大的基于 syntax-rules
风格的宏系统和一个字节码虚拟机。目前,它并不明确符合任何特定的 scheme 规范。
警告 API 不稳定,无任何保证,可能在 1.0 之前随时更改。肯定存在错误,任何重大错误报告都将迅速解决。尽管如此,我仍然将其作为许多脚本任务的日常驱动程序。
特性
- 支持有限的
syntax-rules
风格宏 - 易于与 Rust 函数和结构体集成
- 可以从 Rust 或通过单独的文件轻松调用脚本
- 高效 - 常见函数和数据结构针对性能进行了优化(如
map
、filter
等) - 高阶合约
- 内置的不可变数据结构包括
- 列表
- 向量
- 哈希表
- 哈希集
合约
受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>
)和一个可以迭代的集合(list
、vector
、stream
、hashset
、hashmap
、string
、struct
)并应用转换器。
;; 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)
转换器还接受一个reducer函数。上面我们使用了into-list
和into-vector
,但下面我们可以使用任何任意的reducer
;; (-> 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借用的简写函数语法之外。defn
和fn
实际上是由我想要输入更少的字符所鼓励的。
;; 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
需要B
和C
,而B
需要C
,则C
将只编译一次并在A
和B
之间共享。 - 当模块更改时,将重新编译模块,并且任何依赖的文件也将在必要时重新编译
性能
初步基准测试显示在我的机器上以下结果
基准测试 | 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 License,版本 2.0 (LICENSE-APACHE 或 https://apache.ac.cn/licenses/LICENSE-2.0)
- MIT 许可证 (LICENSE-MIT 或 http://opensource.org/licenses/MIT)
由您选择。
贡献
除非您明确声明,否则根据 Apache-2.0 许可证定义的任何有意提交以包含在作品中的贡献,应按上述方式双许可,而无需任何附加条款或条件。
请参阅 CONTRIBUTING.md。
依赖项
~11–21MB
~297K SLoC