在X7中实现记录

2020-09-19 17:01:34

X7是我用来探索语言设计和解释器的LISP。以下是它的问候世界:

;;定义输入的平方函数(Defn Square(X)(*x x));谓词for x mod 4==1(Defn is-one-mod-4(X)(=1(%x 4);;过滤并映射前200个数字(filter is-one-mod-4(map square(范围200);;输出:(1 9 25 49 81 121 169 225...)。

X7拥有函数式语言提供的所有美好的、不变的、无状态的乐趣。它不能做的是表示内部有状态的类型,如文件或套接字。本文将解释如何以记录的形式向语言添加状态。

这样做的动机是,如果您想要操作文件(或实际上是任何IO),操作系统希望您保持某种状态。有一些技巧可以避免内部可变状态,但将其添加到语言中会更有趣。

记录或对象允许我们将状态封装在一个位置,您可以对其调用方法。我们将致力于在x7中打开、写入和读取文件。它看起来是这样的:

;;打开文件(def my-file(fs::open";my_file.txt";));;.write是Record File的一个方法,;会将字符串写入文件(.write my-file";Hello World";);;同样可以使用.read_to_string;;方法读取文件内容(.read_to_string my-file)。

要运行x7程序,我们需要将传入的字符串解析成我们可以实际计算的形式。这一点的核心是类型expr,它表示解释器中所有可能的类型:

所示的枚举成员是计算(+11)等简单表达式所需的最小变体。如果您不熟悉LISP,这就等同于1+1。

字符串";(+11)";需要转换成我们可以操作的类型。解析器的细节与本文无关。我们真正需要知道的就是(+11)被转换成下面的expr类型:

LISP的关键在于表达式,即类型只是存在并通过计算传播的概念。简单地说,求值是取一个表达式,然后返回另一个表达式的计算过程。例如,在x7解释器中键入3.3将返回3.3,这是另一个表达式。相比之下,(+11)要复杂得多,因为它表示将两个数字相加的函数调用。我们需要用我们的方式去那里。

要建立评估(+11)的方法,我们需要讨论表达式、符号和列表等核心概念。

如前所述,求值是x7功能的基础。更重要的是,不同类型的人评价不同!

>;>;3.3;;Num3.3>;>;>;(fn(X)(X));;函数Fn<;AnonFn,1,[x]>;>;+;SymbolFn<;+,1,[]>;>;(+1 1);列表2。

考虑到这种行为,我们需要更好地理解符号在解释器中是如何工作的。

符号充当对解释器中其他内容的引用-例如常量或函数。X7使用SymbolTable类型,该类型提供查找(&;self,key:&;expr)方法将符号映射到解释器中的表达式。

X7初始化过程的一部分是使用标准库填充符号表-或者使用make_stdlib_fns从RUST填充符号表!宏或stdlib/中的x7文件。如果我们禁用符号填充,您将看到x7运行良好,但不是很有用:

下一个领域是理解列表和符号之间的交互。X7(和LISP)中的列表求值是函数调用,约定如下:

然后,x7解释器的目标是将<;fn-expr>;求值为一个函数,然后使用参数调用该函数。绝大多数时间<;fn-expr>;将是一个符号,如+或-,因此它将是一个符号查找。然后,我们需要的过程:

If let OK(Mut List)=self.get_list(){//空列表Are_Not_Function调用if list.is_Empty(){return OK(self.clone());}let head=list.op_front().unwire();let ail=list;return head.eval(&;Symbol_table)?.call_fn(ail,Symbol_table);}。

最后一行是可操作的行-我们计算第一项(HEAD),然后使用CALL_FN方法调用函数。

评估HEAD,如果我们收到错误,请提前返回。最常见的错误只是符号没有解析。更奇怪的错误可能是,Head本身就是一个失败的函数调用。

更详细地说,Head可以对任何东西求值。虽然这种情况的目的是将符号映射到函数,但这里可能发生任何事情。如果我们不计算Head,Symbol(";+&34;)永远不会成为函数fn<;+,1,[]>;。对于好奇的人来说,((if(Random_Bool)+-)105)是一个有效的x7程序。它随机返回5或15。

值得注意的是,我们不评估尾巴。要允许像if或cond这样的条件构造不计算未采用的分支,我们需要一种选择退出计算的方法!这是作为函数结构上的一个标志来实现的,该标志可以由标准库的rust部分控制。

现在我们已经概述了x7解释器的内部结构,我们实际上可以向语言中添加记录了!

为了表示内部有状态的类型,我们将向语言添加一个称为记录的特性。它需要表达以下行为:

除了调用方法之外,这些项目中的大多数都与确保错误消息正确相关,或者将其与其他解释器机器干净利落地插在一起。

/记录的基本特征。/记录允许x7表示各种内部可变类型/同时不会过度扩展expr枚举。如果这些类型需要文档,则负责/实现RecordDoc。Pub(Crate)特征记录:SYNC+Send{/调用此记录的方法。/(.method_name<;rec>;arg1 arg2 arg3)/变成:/(&;self:<;rec>;,sym:";method_name";,args:Vector![arg1,arg2,arg3])fn call_method(&;self,sym:&;str,args:Vector<;expr>;)->;LispResult<;expr>;/唯一标识此记录的FN ID(&;self)->;U64{0}/正确显示记录类型。FN DISPLAY(&;Self)->;string;/添加有关调试打印FN DEBUG(&;Self)->;String;/克隆对象的更多信息。Fn clone(&;self)->;RecordType;/返回帮助消息的方法名称。Fn方法(&;self)->;vec<;&;#39;static str&>;/返回尼斯帮助消息的类型名称fn type_name(&;self)->;&;';static str;}。

现在我们有了一个特征,我们需要一个可以在整个x7中导出和使用的基本类型。由于我们要使用特征对象,因此Box是一个自然的选择:

我将省略为RecordType实现Record的实现细节,但是如果您很好奇,可以在这里找到它们。

要将RecordType集成到语言中,我们需要将其添加到前面提到的expr枚举中。下面是我们添加记录之前的情况:

就这么简单。我们可以使用编译器错误来找出我们遗漏了什么。

/我们可以随心所欲地将任何内容插入到哈希图中,因此我们需要为RecordType{fn hash<;H:hasher>;(&;self,state:&;mut H){self.id().hash(State);}}/我们还需要对RecordTypes进行相等检查/因为它们的内部状态可能不同,所以始终返回false。/这是可以改进的。RecordType的Iml PartialEq{fn eq(&;Self,_Other:&;RecordType)->;bool{false}}/x7到处克隆RecordType的Iml Clone{fn Clone(&;Self)->;RecordType{Record::Clone(Self)}}

太棒了!我们现在已经为RecordType实现了必要的特性,除了像我的自定义显示实现这样的一些更改之外,我们可以开始工作了。

我们最不想要的就是在需要的时候从枚举中获取RecordType的方法:

实施表达式{//...。躲避了..。Pub(Crate)fn get_Record(&;self)->;LispResult<;RecordType>;{if let expr::Record(R)=self{OK(r.clone())}Else{Bad_Types!(";Record";,&;Self)}}。

这将允许我们获取标准库中的记录类型,如果不这样做,则会有很好的错误消息。

我们要做的下一件事是添加一个标准库函数来调用方法!

既然RecordType已经嵌入到解释器的机器中,我们就可以实际使用它了!我们需要一种显式调用标准库stdlib::call_method中的方法的方法。

我们还没有.method-call语法糖,所以一个独立的x7函数就可以了。

我们只得到了一个参数列表,因此我们需要定义一个调用约定:

因此,我们将期望记录作为第一个成员,然后是方法名称,最后是参数。例如,下面是我们期望写入文件的方式:

Fn call_method(exprs:Vector<;expr>;,_Symbol_table:&;SymbolTable)->;LispResult<;expr>;{//第一个列表成员是记录。设rec=exprs[0].get_record()?;//第二个列表成员为方法字符串。Let method=&;exprs[1].get_string()?;//收集列表中的参数。设args=exprs.clone().Slice(2.。);//.clone()为O(1),.Slice需要一个&;mut//最后,用参数调用记录上的方法使用Crate::Records::Record::Record::Record;rec.call_method(method,args)}。

现在我们有了这个函数,我们需要使它可以从解释器访问。X7使用名为make_stdlib_fns的宏向解释器公开rust函数,因此我们只需将其插入:

好的!。我们真的不能用它做很多事情,因为我们还没有在任何类型上实现记录,所以让我们这样做吧!

将记录添加到x7的最初动机是能够打开、读取和写入文件。我们将通过rust File结构支持x7 File实现,因此让我们在x7-Records/file.rs中创建一个新文件:

#[Deriate(Clone,Debug)]pub(Crate)struct FileRecord{path:string,//记录特性需要同步+发送文件:arc<;mutex<;std::fs::file>;>;,}。

现在我们有了一个结构,让我们公开一种从x7生成结构的方法。我们希望下面的x7表达式起作用:

Impl FileRecord{/使用给定路径pub(Crate)fn open_file(path:string)->;LispResult<;expr>;{//使用自由权限打开文件。让f=OpenOptions::new().write(True).create(True).read(True).open(path.clone()).map_err(|e|无论如何!(";无法打开文件\";{}\";因为{}";,&;path,e)?;//使路径美观。让abs_path=fs::canonicalize(Path).map_err(|e|无论如何!(";无法规范化路径!{}";,e))?.to_str().ok_or_Else(||无论如何!(";无法将路径表示为UTF-8字符串";))?.into();//记录!是帮助制作LispResult<;expr::Record>;类型记录的宏!(FileRecord::New(f,abs_path))}/从x7打开文件/此函数签名允许我们将其直接暴露给_x7的解释器pub(Crate)fn(表达式:Vector<;expr>;,_Symbol_table:&;SymbolTable)->;LispResult<;expr>;{exact_len!(exprs,1);let path=exprs[0].get_string()?;FileRecord::open_file(Path)}}

现在我们有了创建FileRecord的能力,我们将需要实现Record以便解释器(expr::Record)能够理解它。

FileRecord{fn call_method(&;self,sym:&;str,args:Vector<;expr>;)->;LispResult<;expr>;{//我们还没有方法。UNKNOWN_METHOD!(SELF,sym)}FN TYPE_NAME(&;self)->;&;&39;static str{&;FileRecord";}FN DISPLAY(&;self)->;string{format!(";File<;{}>;";,self.path)}FN DEBUG(&;self)->;String{self.display()}FN CLONE(&;Self)->;RecordType{Box::New(Clone::Clone(Self))}FN方法(&;Self)->;Vec<;&;&39;Static str>;{Vec::New()}FN ID(&;Self)->;U64{使用STD::Collection::hash_map::DefaultHasher;使用std::hash::{hash,hasher};让mut h=DefaultHasher::New();self.path.hash(&;mut h);h.Finish()}}。

我们还需要向解释器公开FileRecord::from_x7,因此让';返回并将其添加到make_stdlib_fns:

Make_stdlib_fns!{//省略函数...。(";call_method";,2,call_method,true,";<;doc-string>;";),//打开文件(";fs::open";,1,FileRecord::from_x7,true,";打开文件。";),}。

好的!。我们已经打开了一个文件。我们现在可以在FileRecord上实现一些其他有用的方法,比如从文件中读取:

Impl FileRecord{/将文件内容读入字符串,/将光标倒带到前面。Fn read_all(&;self)->;LispResult<;string>;{let mut buf=string::new();let mut Guard=self.file.lock();Guard.read_to_string(&;mut buf).map_err(|e|无论如何!(";无法读取字符串{}";,e))?;rewind_file!(Guard);OK(Buf)}/将FileRecord的内容读取到字符串。Fn read_to_string(&;self,args:Vector<;expr>;)->;LispResult<;expr>;{//我们不需要参数。完全正确!(args,0);self.read_all().map(expr::string)}}。

FileRecord的IMPL记录{fn call_method(&;self,sym:&;str,args:Vector<;expr>;)->;LispResult<;expr>;{Match sym{";read_to_string";=>;self.read_to_string(Args),_=>;UNKNOWN_METHOD!(SELF,SYM),}。

太棒了!我们能够调用FileRecord上的方法。实现.write和其他有用的文件操作是相同的过程,因此我们将省略它。这是很棒的东西,加点语法糖就更好了。

我们可以修改解析器以识别句点,并将其解析成为我们调用我们的方法的函数,而不需要在解析器中获得太多信息,而是将.method解析成expr::symbol.我们可以修改解析器来识别句点。

Fn parse_Symbol<;';a>;(i:&;&39;a str)->;IResult<;&;&;a str,expr,VerboseError<;&;&39;a str;>;{map(taken_while e1(Is_Symbol_Char),|sym:&;str|{expr::Symbol(sym.into()}))(I)}。

因此,它所做的就是尝试识别一个符号,然后在完全解析一个符号时转换类型。我们将修改它以识别符号是否以句点开头,如果是,则调用make_method_call并返回一个expr::函数。

Fn make_method_call(method:string)->;expr{//导入一些有用的类型使用Crate::Symbols::Function;使用std::sync::Arc;let method_clone=method.clone();//这是很酷的部分。我们正在创建符合//X7FunctionPtr类型的闭包。//当我们调用.write时,它将调用此函数。让method_fn=Move|args:Vector<;expr>;,_sym:&;SymbolTable|{//第一项是记录,get_record(){OK(Rec)=>;rec,err(E)=>;return err(E),};//`rec`是记录,调用该方法。//请注意,我们将`method_clone`移到了this闭包中!Use Crate::Records::Record;//`args`的布局是:(<;record>;<;arg1>;<;arg2>;...),//我们有的类型签名是record::call_method(method,args)rec.call_method(&;method_clone,args.clone().Slice(1.。));};//创建函数结构let f=function::new(format!(";method_call<;{}>;";,method),//函数名1,//参数个数Arc::new(Method_Fn),//函数指针true,//eval args);//返回expr::function expr::function(F)}。

这相当酷--我们正在将一个符号转换成一个函数。我们所需要做的就是在parse_symbol中添加一个if-gate,然后我们就重新设置了!

Fn parse_Symbol<;(i:&;&39;a str)->;IResult<;&;#39;a str,expr,VerboseError<;&;&;{map(Take_While e1(Is_Symbol_Char),|sym:&;str|{if sym.starts_with(';.';){make_method_call(sym[1].';){make_method_call(sym[1],|sym:&;str|{if sym.start_with(';.';){make_method_call(sym[1.。].into()//sym[1..]=>;删除句点}否则{expr::Symbol(sym.into())}})(I)}。

就是这样!我们已经在x7中实现了记录。我希望你喜欢读这篇文章!