Programming Languages PartB Week2学习笔记——用Racket编写解释器

2023-05-16

文章目录

      • Datatype-Programming in Racket Without Structs
        • (1)第一个例子:
        • (2)第二个例子
      • Datatype-Programming in Racket With Structs
      • Advantages of Structs
      • Implementing Programming Languages
      • What Your Interpreter Can and Cannot Assume
      • Implementing Variables and Environments
      • Implementing Closures
      • Optional: Are Closures Efficient?
      • Racket Functions As “Macros” For Interpreted Language
      • 小结本章内容

Datatype-Programming in Racket Without Structs

Racket没有提供类似ML的datatype来自定义one-of 类型,但由于它是动态类型语言,所以能够通过混合各种类型的值来实现这一点,不需要datatype

image-20220531060801122

ML中用datatype来包装基础类型,就生成了同种类型的不同值,然后就能够储存在list中。这时候的构造器的pattern充当了分离的tags,用来识别某种基础类型。

但在Racket中,list不进行类型检查,本来就能够存储多种类型值,然后不依赖于pattern match区分每类型的值,而是通过每种值对应的tag检查函数来实现,例如number? string? pair?等。

(1)第一个例子:

image-20220531120939003

Racket的实现

image-20220531130002625

(2)第二个例子

下面是ML中的datatype递归定义和函数递归调用(在PartA中常见),但该函数只返回一个int值(ML返回值必须是同一类型),exp->int。但我们想要让他返回一个更通用的类型,exp->exp

image-20220531130057569

因此需要一种新的方式来定义函数,始终返回一个Const of int(datatype定义的exp类型值)

image-20220531130836196

image-20220531131327962

在Racket中我们的做法是,使用一些辅助函数来定义数据类型构造器函数实现类似pattern match的测试函数实现数据extract函数

单引号加名称,是用来标记列表的symbol,例如下面的’Const,'Negate

整体实现思路其实类似,就是语法不同而已(借助辅助函数和函数传递,因为e可以是函数调用)

image-20220531131613587

image-20220531131943954

image-20220531132156967

image-20220531132317529

最后是关于Symbol的一些用法:

image-20220531132331303

Datatype-Programming in Racket With Structs

Racket的结构体,语法如下:

image-20220601105413830

struct定义时就会提供一些函数。结构体能够实现类似ML datatype的构造器的用法,但结构体定义的同时就已经同时提供了构造器、测试函数和extract函数。

image-20220601105334739

例子:

image-20220601144000140

另外,定义struct时用到了attribute,通过#: 加上关键词的形式使用。有点类似于java的注解,用来描述和定义struct的某些行为。例如transparent让struct values暴露出来,可以在REPL中print时显示内部的field值。如果没有transparent,struct内部field值必须通过函数调用之后才能看到

image-20220601144812274

image-20220601144915452

mutable可以为struct添加额外的函数,用于修改struct中fields的值(mcons就是一种预定义的可修改struct)

image-20220601144213924

Advantages of Structs

方法比较,对比strut和我们自定义函数(使用list作为数据结构)的方式。struct不是自定义函数方式的语法糖(那当然,这并不是两种互通的语法,只是类似datatype的两种实现方式而已)

image-20220601145115479

其核心区别在于,调用结构体的生成语句之后,得到的不是一个list,而是struct(可用于结构体定义时生成的各种相关函数)。结构体是一种不同于list的新的数据结构。

image-20220601145404500

所以对比上述两种实现方式,用list来作为数据结构更容易出错。由于car,cdr能操作list,数据封装会被打破,list的标准库函数还会对函数调用造成混淆。并且对数据约束太弱,即使包含list包含一些无关数据,只要有’add标签就会被认为是正确值。

image-20220601150103737

例子:下面Add和Multiply就是前面课程中介绍的函数方式

image-20220601151048818

image-20220601151253115

因此,struct相比之下具有多种优点

(1)更好的代码风格,更简洁的数据结构定义

(2)使用数据结构时(与list函数方法)同等的方便

(3)不容易误用数据结构

- 其一体现在存取函数不容易使用错误类型的数据(不容易因为只比较标签而使用错误类型的数据)
- 其二体现在测试函数不容易混淆(struct拥有专门的测试函数)

image-20220601151444031

另外,struct在抽象方面具有更多的作用。

例如,一是在module system中定义abstract types,需要使用struct(list的库函数例如cons难以向用户隐藏,用户能随意制造改变module成员变量)。而通过struct隐藏module内部成员,然后向用户开放函数接口能实现抽象和封装。

二是contract system可以自己建立struct的比较(例如比较struct内部某个field是否属于某个类型),但对于list,因为list是相似和通用的,不会想strut那样(因为自己定义而)区别很大,所以很难为list建立比较所有情况的方法。

image-20220601152001936

struct的特别在于他与其他特性和数据类型不同,每次定义struct都是生成一种新的数据类型。

比如 struct a ,和 struct b ,那么a 和b 也不一样,a? 判断b的话也会返回#f , 只有a才会返回#t。(跟C语言的结构体差不多)

image-20220601153628249

Implementing Programming Languages

week2的作业是实现一个小型编程语言,编程语言的典型工作流

(1)根据code 文件中的具体语法(string),判断是否有明显的语法错误

(2)根据具体语法解析,创建抽象语法(抽象语法树,AST),判断是否有错误(例如如果需要type checking,是否有类型错误等)

image-20220601160857514

(3)编译或者解释AST

下面是对解释器和编译器的介绍,解释器用语言A来对语言B的程序进行计算得到结果,而编译器用语言A来转换语言B成为语言C的程序(例如我们熟悉的C语言编译器将C语言转换为二进制程序),我们用来实现编译器或解释器的语言被称为metalanguage

image-20220601161200554

事实上,现代语言两种方式都使用到了,通常混合使用,比较复杂。下面是java的例子:

Racket也混合了两种方式。

image-20220601193128534

解释或编译是关于语言的执行方式,但不是语言的定义方式。从程序代码上是看不出来具体的执行方式的。

这里老师提到了很重要的一点,编译或解释不是衡量一个语言快慢的决定因素,(语言的快慢主要取决于语言的定义方式)不能说编译运行的语言就一定比解释运行的语言快,这是不可合理的。

不过某些时候,如果一个语言拥有“eval”函数(例如Racket的eval),可以用来承载并执行该语言的某些语句或者其他语言的语句,那么eval过程可能会慢一些。但这并不代表eval语句的执行一定是解释型的,它也可以用编译器实现。总而言之,就是语言的运行速度快慢和语言的执行方式关系不大

image-20220601193729901

回到编程语言的工作流程中,课程中我们只是实现解释器的方式,使用PL A来实现PL B,可以skip parsing,直接用PL A来实现PL B的AST(直接将B的程序按照树状嵌入A中)。这个过程可以借助ML的构造器或者Racket的struct来实现。

这一步实际上就是将抽象AST转换到PL A的过程,实际上与PL B的语法没关系(因为skip parsing,跳过了对PL B进行语法解析生成AST的步骤,这一步骤似乎就不是课程所涵盖的内容了(也许是难度比较大?))

image-20220601195714637

已经实现的例子:假设我们定义一种算术语言(Arithmetic Language),eval-exp就是解释器

image-20220601200419279

What Your Interpreter Can and Cannot Assume

首先,需要明确的一点是,AST的子树也是AST。

我们编写的解释器可以假设AST是合法的程序(可能有错误的结果或者不可预见错误),但不能假设参数或者结果是正确的类型,所以需要检查(错误类型的情况,抛出异常)。因为子表达式递归计算有可能返回一个不正确的类型(例如需要const xx却返回了一个bool xx)。

还有一点需要注意,我们要检查的是表达式(expression)计算后的值(value)是否是某个类型(例如const xx)(详见本节最后的例子),而不是表达式本身是否是某个结构体或者什么其他类型。

同时,理论上,能被具体语法parsing检查出来的错误都应该算非法AST,例如 程序中使用 3 而不是 const 3作为某个输入(非本语言的输入结构)

image-20220602103316563

解释器的结果一定是一个计算后的值value而不是表达式本身。这个value在大型语言中可能比较多样。

image-20220602103639667

例子:解释器必须检查运行时的类型错误(这里的类型指的是PL B的类型,而不是PL A的类型),并给出错误信息

image-20220602104021539

例如,下图定义的两段test代码都是合法的AST,但test2中使用了错误的类型(test2之所以合法是因为错误的不可预见但符合语法,if-then-else中有正确的出口const 7,和错误的出口bool #t,但两者在PL B中都是合法的表达式(符合语法)),因为错误分支中用了 bool b 而不是 const i ,multiply运行时理应判断两个参数是否都是const,所以这里是我们需要处理的错误。

image-20220602104752672

image-20220602110001236

比较上面的例子,下面这个例子中,non-test就不是一个合法的AST,因为const #t不符合我们定义的PL B的语法,这种错误是我们的解释器不需要处理的(解释器假设程序是合法的AST,如果AST不合法,应该直接终止解释器,而不是由我们定义抛出异常)。

image-20220602110233331

这里的error是由PL A给出的,而不是我们在解释器中定义的。也就是说eval-exp这个解释器被终止了。

image-20220602110445658

因此,(由于Racket是动态语言,我们的变量类型都是口头上说说而已),解释器应该真正去判断变量类型。

; 错误的解释器,没有对类型进行判断
(define (eval-exp-wrong e)
  (cond [(const? e) 
         e] 
        [(negate? e) 
         (const (- (const-int (eval-exp-wrong (negate-e1 e)))))]
        [(add? e) 
         (let ([i1 (const-int (eval-exp-wrong (add-e1 e)))]
               [i2 (const-int (eval-exp-wrong (add-e2 e)))])
           (const (+ i1 i2)))]
        [(multiply? e) 
         (let ([i1 (const-int (eval-exp-wrong (multiply-e1 e)))]
               [i2 (const-int (eval-exp-wrong (multiply-e2 e)))])
           (const (* i1 i2)))]
        [(bool? e) 
         e]
        [(eq-num? e) 
         (let ([i1 (const-int (eval-exp-wrong (eq-num-e1 e)))]
               [i2 (const-int (eval-exp-wrong (eq-num-e2 e)))])
           (bool (= i1 i2)))] ; creates (bool #t) or (bool #f)
        [(if-then-else? e)
         (if (bool-b (eval-exp-wrong (if-then-else-e1 e)))
             (eval-exp-wrong (if-then-else-e2 e))
             (eval-exp-wrong (if-then-else-e3 e)))]
        [#t (error "eval-exp expected an exp")] ; not strictly necessary but helps debugging
        ))
; 添加对类型进行判断,错误时抛出异常
(define (eval-exp e)
  (cond [(const? e) 
         e] 
        [(negate? e) 
         (let ([v (eval-exp (negate-e1 e))])
           (if (const? v)
               (const (- (const-int v)))
               (error "negate applied to non-number")))]
        [(add? e) 
         (let ([v1 (eval-exp (add-e1 e))]
               [v2 (eval-exp (add-e2 e))])
           (if (and (const? v1) (const? v2))
               (const (+ (const-int v1) (const-int v2)))
               (error "add applied to non-number")))]
        [(multiply? e) 
         (let ([v1 (eval-exp (multiply-e1 e))]
               [v2 (eval-exp (multiply-e2 e))])
           (if (and (const? v1) (const? v2))
               (const (* (const-int v1) (const-int v2)))
               (error "multiply applied to non-number")))]
        [(bool? e) 
         e]
        [(eq-num? e) 
         (let ([v1 (eval-exp (eq-num-e1 e))]
               [v2 (eval-exp (eq-num-e2 e))])
           (if (and (const? v1) (const? v2))
               (bool (= (const-int v1) (const-int v2))) ; creates (bool #t) or (bool #f)
               (error "eq-num applied to non-number")))]
        [(if-then-else? e) 
         (let ([v-test (eval-exp (if-then-else-e1 e))])
           (if (bool? v-test)
               (if (bool-b v-test)
                   (eval-exp (if-then-else-e2 e))
                   (eval-exp (if-then-else-e3 e)))
               (error "if-then-else applied to non-boolean")))]
        [#t (error "eval-exp expected an exp")] ; not strictly necessary but helps debugging
        ))

Implementing Variables and Environments

前面的例子都是一些常量以及他们的运算,本节需要实现变量

本节只讲原理,需要自己在作业中实现代码

image-20220602115652763

environment是一个变量名(string)到值values的映射,在这里我们通过一个pair的list来实现。

environment作为参数传递到解释器的helper function中,也同时要传递到递归的subexpression中。因为他们都使用一套同样的environment。如果需要的变量不在environment中,解释器需要给出error。

当然,如果有let-expression,那么也会用到不同的environment。

image-20220602130158319

由于Environment我们定义成一个list,其中包含由变量名和变量值构成的多个pair(类似于一种字典结构),所以week1中的练手作业正好就能派上用场,可见老师对课程的编排非常巧妙。

image-20220602130701033

最后是一些关于作业的细节,正常情况下,我们将eval-under-env作为一个helper function(封装在eval-exp中),但作业中由于需要对eval-under-env进行评分,会在外部调用它,所以需要让它保持top-level

image-20220602131740002

Implementing Closures

image-20220602133614545

关于闭包的实现:

值得注意的是,我们实现的语言中(事实上不只是我们的语言,Racket这些语言也是一样),单独的函数定义不是一个值,而事实上闭包才是一个值(闭包必须包含函数定义和函数定义的环境)

我们调用function时的环境并不是函数定义的环境,所以我们定义一个closure结构,来保存函数的环境以便我们之后使用。

image-20220602133954579

关于函数调用,实际上就是要注意call e1 e2中,e1和e2是表达式,不是值,所以要先求值。先用当前环境计算e1,e1计算后需要得到一个闭包c,再用当前环境计算e2得到值v。再使用闭包c的环境来计算闭包c中的函数体的值(所以不算其中的递归调用的话,这里的call一共计算三次,其中两次使用当前环境,一次使用闭包环境):

image-20220602135025769

Optional: Are Closures Efficient?

如果environment非常大,那么闭包可能会非常大

一种替代的方法是只在闭包中记录函数体中出现的free variables(指的是出现在function body 中,但不在function body 中定义或者shadowing的变量(也即是不包括函数参数和函数local变量))

image-20220602221233253

例子:

image-20220602222349465

解释器并不会每创建一次闭包就分析一次代码(不会每次创建闭包就在函数中寻找一次free variable),而是在全部计算之前,先计算每个函数的free variables存储起来。

这样通过时间换空间。

image-20220602222454023

在将某个有闭包的语言编译成没有闭包的语言时,此时就没有解释器来存储environment了,因此必须显式地让编译后的函数携带额外的显式参数(extra explicit argument),这些被称为environment。

让程序自身携带environment,而不是我们之前让解释器携带environment的方式。

image-20220602223437263

Racket Functions As “Macros” For Interpreted Language

回顾前面几节的内容

image-20220602224313598

使用PL A在PL B中定义macro实际上就是使用PL A的函数来将某些具有PL B语言结构的表达式,生成一段PL B语言的“代码”,直接说可能有点抽象,看下面的例子就能理解了。

image-20220602224508293

例子:使用的语言定义和解释器仍然是之前例子的代码(arithmetic language)

这里andalso e1 e2 就相当于是一种macro,调用函数andalso时就会先将其生成一段函数体定义好的PL B的“代码”,实现了宏展开。这个过程是在PL A中运行的,是在PL B程序运行(计算)之前,因此也符合macro的特点。

image-20220602224937016

例2:

image-20220602225941927

当这些函数被调用时,就会生成一个PL B的“代码“。

image-20220602230047902

image-20220602230059584

image-20220602230345593

小结本章内容

本章主要是讲如何用Racket实现一种语言解释器。

编程语言的实现过程包含三大步骤:

其一是将某种语言B(PL B)的具体语法进行语法检测,排除一些明显的语法错误。然后解析(parsing),转化为抽象语法树(AST)。

其二是检查抽象语法树(AST)的错误,比如如果需要type checking就在这里检测。理论上这里应该得到合法的AST。

其三是通过编译器对程序进行编译(用一种语言A将语言B转换为另一种语言C的code)或通过解释器对程序进行解释(使用一种语言A计算语言B的值,然后返回计算结果)。编译其实应该叫做transfer,解释应该叫做evaluate。

本章就是围绕解释器来讲的,包括作业部分也是。

事实上,在课程中,我们假设第一步已经进行了(不是我们考虑的范畴),我们的解释器理论上应该获得一个合法的AST(否则终止解释器),合法的含义其实就是AST中的表述遵循语言PL B的语法,以课程的例子来表述,就是const 2是合法的,但const #f 就不合法(因为我们假定PL B的语法中const后必须跟上一个数值(至少是数值形式的量),如果跟上#f,那就是语法错误,应该在步骤一中排除,所以课程中老师说这种错误不需要我们来判断和处理,只要让Racket抛出异常就行了(终止编译器))。

运行时需要判断类型是否正确,这里的类型指的是PL B中各种参量的类型,因为我们用的PL A是Racket,是动态类型语言,没有设定每个结构体的参数或者返回值的类型(我们只是在假设他们有某种类型,但没有实际约束)。比如我们认为(Add e1 e2) 这个函数应该接受两个表达式,两个表达式计算之后的值都应该是const num(PL B的某种参量类型),但这只是假设,并没有实际约束。如果这两个表达式计算出来是bool b(PL B的某种参量类型)的类型也能够传入这个函数中(作为一段PL B的程序),也就是说这个程序没有语法错误(得到合法的AST),但我们知道,类型错误至少在运行时应该抛出异常,这个异常就应该由我们的解释器来抛出(而不是让Racket强行终止解释器),所以在编写时要尤为注意这些类型判断(一定不要假定程序不会出现类型错误)。

课程中用Racket的结构体struct来定义许多PL B的“语句”,实际上不是PL B真正的“语法”,而是一种抽象语法树的表示结构,例如,在Racket中用结构体(struct const num)来表示PL B的数字常量,似乎看起来const num就是PL B中定义数字常量的语句,但实际上PL B的语法可以是其他语句,比如类似C语言一样写成const int num。这样的不同是允许的,因为AST是抽象出来的(经过了步骤一的解析parsing,const int num能被解析成AST中的const num,再在PL A(这里也就是Racket)作为一个结构体被解释(计算))。

所以课程中(包括作业中),定义的PL B的AST的各种结构都是一系列Racket(PL A)的结构体,所谓PL B的程序(课程中指的是已经经过parsing的PL B 的AST)其实就是这些结构体的嵌套(因为AST是树状结构)得到的一个树数据结构,然后定义PL B的解释器就是对嵌套好的Racket结构体树进行判断(判断是什么结构体)、求值(根据每种结构体规定好的行为来进行计算)的一个Racket函数(比如课程中的eval-exp)。

所以本次作业表面上是编写一个新的编程语言及其解释器,看起来很难,但理解其实质,其实就是对Racket结构体定义和解释应用。当然,这个过程也确实能学到解释器的运行原理和编程语言的实现过程。再一次感叹这门课的精妙设计。

不过不得不说的是,这周的课程作业还是挺有难度的,有一部分原因是英文的作业文档表述不太清楚,看半天不知道到干什么。

另外,必须认为作业中定义的语言PL B的各种参量是不能改变的(immutable),事实上也是如此,PL B中的所有表达式最终的出口都是int num,认为是一个常量。否则因为在struct中存储env实际上存储的是一个env copy,如果可以改变参量的值,那么真正env中的参量虽然不会被同名的local binding所覆盖,但有可能在后续的程序中被mutate(在函数定义之后、调用之前被修改,导致数值变化),这也是lexical scope的特点之一。因此假定PL B具备immutable的特性(env中不能有重复定义、不能修改某个已定义参量的值)简化了这个问题,让作业更简单。下面是例子:

image-20220603181312822

关于使用Racket(PL A)对PL B实现宏定义,实际上就是定义一个Racket函数,把已有的一些结构体表达式和一些之前结构体定义重新组合(生成新的结构体实例),得到一个新的结构体表达式(PL B程序语句)。调用这个函数的过程就是宏展开过程。例如:ifaunit函数就是现在PL B的宏定义,e1 e2 e3是已有的结构体表达式,ifgreater、isaunit、int xx这些就是已有的结构体定义。因为宏展开过程是调用Racket函数的过程,所以实在eval-exp解释器运行之前,符合宏的定义。

image-20220603215558937

题外话:学完Racket我写文档也莫名其妙用上了一大堆嵌套圆括号 (?),笑死

这夸张的括号。

image-20220603233801952

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

Programming Languages PartB Week2学习笔记——用Racket编写解释器 的相关文章

  • Boyer-Moore算法的C++实现

    BM算法 阮一峰的网络日志 以上给出了通俗易懂的算法讲解 xff0c 下面给出代码实现 xff0c 使用的宽字符 xff0c 这样就不限于英文字母了 stdafx h编译不过去就屏蔽掉 StringSearch BoyerMoore cpp
  • Kafka学习笔记之Kafka Consumer设计解析

    0x00 摘要 本文主要介绍了Kafka High Level Consumer xff0c Consumer Group xff0c Consumer Rebalance xff0c Low Level Consumer实现的语义 xff
  • Kafka学习笔记之Kafka性能测试方法及Benchmark报告

    0x00 概述 本文主要介绍了如何利用Kafka自带的性能测试脚本及Kafka Manager测试Kafka的性能 xff0c 以及如何使用Kafka Manager监控Kafka的工作状态 xff0c 最后给出了Kafka的性能测试报告
  • 用Clion运行C++代码时输出中文乱码解决方法

    用Clion运行C 43 43 代码时输出中文乱码解决方法 1 File gt setting 2 在页面最下面找到UTF 8 xff0c 将其改成GBK 3 根据提示选择Convert 4 问题解决啦
  • Java核心技术卷1:基础知识(原书第10版)

    本书为专业程序员解决实际问题而写 xff0c Java基础知识面覆盖很完整 xff0c 可以帮助你深入了解Java语言和库 在卷I中 xff0c Horstmann主要强调基本语言概念和现代用户界面编程基础 xff0c 深入介绍了从Java
  • 斜率K的意义

    夹角公式 设直线l1 l2的斜率存在 xff0c 分别为k1 k2 xff0c l1到l2的转向角为 则tan 61 k2 k1 xff09 1 43 k1k2 xff09 l1与l2的夹角为 xff0c 则tan 61 k2 k1 xff
  • 阿里巴巴 2014校招 研发工程师 笔试

    刚杭州这边阿里巴巴校招笔试回来 回忆一下题 xff0c 为大家将来的笔试做点准备 选择题 xff1a 1 字符串 alibaba 的huffman编码有几位 2 以下哪些用到贪婪算法 xff1a 最小生成树的Prim算法最小生成树的Krus
  • Bag of Words(词袋模型)

    词袋模型的提出是为了解决文档分类 xff0c 主要应用在 NLP Natural Language Process xff0c IR Information Retrival xff0c CV xff08 Computer Vision x
  • 全向和定向天线区别,何为天线增益

    文 白浪 为什么网络信号弱 速率低 时断时续 xff1f 为什么购买了大量的AP xff0c 但是还有地方信号不好 xff0c 而有的地方信号多到互相干扰 xff1f 为什么布置了大增益的天线 xff0c 结果还未能得偿所愿 xff1f 无
  • 2017电赛国赛可见光定位装置(精确度达到1cm)

    先不多说上源码 Github网址 xff1a https github com DerrickRose25 Indoor positioning of visible light 用的畸变摄像头 xff0c 用Matlab做线性拟合和畸变矫
  • 如何给图片添加黑色边框

    有时候图片有了黑色边框才能与相近的背景区分开 xff0c 这个时候给图片添加边框就是必须的了 你好这位朋友 xff01 很简单 xff0c 要这样操作 xff1a 将照片打开 xff0c 裁切好后 xff0c 应用工具箱中的矩形选择工具 x
  • STLINK怎么与STM32单片机连接

    STLink是ST官方开发的单片机仿真工具 xff0c 可以烧写程序 在线仿真 xff0c 使用非常方便 STLink具有两种接口 xff0c 分别为 1 SWD模式 2 SWIM单总线模式 SWD模式主要针对STM32系列的单片机 xff
  • 【树莓派4B为例的树莓派接口认识】

    1 xff1a SOC芯片 树莓派采用博通 xff08 Broadcom xff09 BCM2711芯片作为SOC芯片 xff0c 芯片上集成了CPU GPU DSP及SDRAM内存等 xff0c 其中CPU和GPU共享内存 xff0c 可
  • 仿真阿克曼小车+3D激光雷达velodyne并使用lego-LOAM与octomap建图

    仿真的学习内容基于以下项目 xff0c 然后将2D激光雷达换成3D的velodyne试了试效果 xff0c 想跑lego loam然后用octomap转成2D栅格地图看能不能导航 阿克曼小车仿真开源 首先需要下载当前ros版本的velody
  • 树莓派3B安装Ubuntu 18.04

    这里展示的是使用显示器的方法 xff0c 不用ssh 树莓派3b安装Ubuntu18 04完全遵照的Ubuntu wiki中的步骤 如果产生显示器显示问题可以看树莓派与电视之间的显示问题 xff08 1 xff09 下载并写入 下载Ubun
  • ls-remote --tags --heads git://github.com/adobe-webplatform/eve.git

    报错日志 leepandar 64 localhost ant design vue jeecg yarn install yarn install v1 22 19 1 4 x1f50d Resolving packages 2 4 x1
  • python遇到‘\u’开头的unicode编码

    web信息中常会遇到 u4f60 u597d 类型的字符 首先 u 开头就基本表明是跟unicode编码相关的 xff0c u 后的16进制字符串是相应汉字的utf 16编码 python里decode 和encode 为我们提供了解码和编
  • gVim配色和字体选择

    本文简述如何跟换gVim的字体和选择喜欢的配色方案 xff1a 1 下载配色方案 xff1a gVim官网提供了很多配色方案 xff0c 可以根据自己的需要来选择下载 xff0c 本人比较喜欢深色背景系列的 xff0c 所以以下列举一些 x
  • Python中使用下划线定义变量的各种含义

    分类 在本文中 xff0c 我将讨论以下五种下划线模式和命名约定 xff0c 以及它们如何影响Python程序的行为 xff1a 单前导下划线 xff1a var单末尾下划线 xff1a var 双前导下划线 xff1a var双前导和末尾
  • Python 函数参数后面加冒号,函数后面跟箭头的含义

    span class token keyword def span span class token function f span span class token punctuation span text span class tok

随机推荐