newLISP/Contexts 简介
我们都喜欢将自己的物品整理到不同的区域或隔间。厨师将鱼、肉和甜点区域分开,电子工程师将电源供应器与射频和音频级分开,newLISP 程序员使用上下文来组织他们的代码。
newLISP 上下文为符号提供一个命名的容器。不同上下文中的符号可以具有相同的名称而不会冲突。因此,例如,在一个上下文中,我可以将名为meaning-of-life的符号定义为值为 42,但在另一个上下文中,同名符号的值可以是dna-propagation,在另一个上下文中可以是worship-of-deity。
除非你明确选择创建和/或切换上下文,否则所有 newLISP 工作都在默认上下文 MAIN 中进行。到目前为止,在本文件中,当创建新符号时,它们都已添加到 MAIN 上下文中。
上下文非常灵活——你可以根据手头的任务,将它们用于字典、软件对象或超级函数。
context 函数可用于多种不同的任务
- 创建新上下文
- 从一个上下文切换到另一个上下文
- 检索上下文中的现有符号的值
- 查看你所处的上下文
- 在上下文中创建新符号并为其分配值
newLISP 通常能够读懂你的想法,并根据你使用 context 函数的方式,了解你想要做什么。例如
(context 'Test)
创建名为 Test 的新上下文,正如你所期望的那样。如果你在交互模式下输入此命令,你会看到 newLISP 会更改提示,告知你现在正在另一个上下文中工作。
> (context 'Test) Test Test>
你可以自由地上下文之间切换
> (context MAIN) MAIN > (context Test) Test Test>
单独使用时,它只会告诉你你所在的位置
> (context) MAIN >
一旦上下文存在,你就不必引用其名称(但如果你愿意,可以引用)。请注意,我使用大写字母作为上下文名称。这不是强制性的,只是一个约定。
上下文包含符号及其值。创建符号并为其赋值的方法有很多。
> (context 'Doyle "villain" "moriarty") "moriarty" >
这将创建一个新的上下文——请注意引号,因为 newLISP 之前从未见过它——并创建一个名为“villain”的新符号,其值为“Moriarty”,但保留在 MAIN 上下文中。如果上下文已存在,可以省略引号。
> (context Doyle "hero" "holmes") "holmes" >
要获取符号的值,你可以执行以下操作
> (context Doyle "hero") "holmes" >
或者,如果你使用的是控制台,可以逐步执行以下操作
> (context Doyle) Doyle Doyle> hero "holmes" Doyle>
或者,从 MAIN 上下文执行以下操作
> Doyle:hero "holmes" >
符号的完整地址是上下文名称,后跟一个冒号 (:),再后跟符号名称。如果你在另一个上下文中,始终使用完整地址。
要查看上下文中的所有符号,请使用 symbols 生成列表
(symbols Doyle) ;-> (Doyle:hero Doyle:period Doyle:villain)
或者,如果你已在 Doyle 上下文中
> (symbols) ;-> (hero period villain)
你可以以通常的方式使用此符号列表,例如使用 dolist 逐个遍历它。
(dolist (s (symbols Doyle))
(println s))
Doyle:hero Doyle:period Doyle:villain
要查看每个符号的值,请使用 eval 查找其值,并使用 term 返回仅符号的名称。
(dolist (s (symbols Doyle))
(println (term s) " is " (eval s)))
hero is Holmes period is Victorian villain is Moriarty
循环遍历上下文中的符号有一种更有效(稍微快一点)的技术。使用 dotree 函数
(dotree (s Doyle)
(println (term s) " is " (eval s)))
hero is Holmes period is Victorian villain is Moriarty
除了使用 context 显式创建上下文之外,你还可以让 newLISP 自动为你创建上下文。例如
(define (C:greeting)
(println "greetings from context " (context)))
(C:greeting)
greetings from context C
这里,newLISP 创建了一个名为 C 的新上下文,以及该上下文中的一个名为 greeting 的函数。你也可以通过这种方式创建符号
(define D:greeting "this is the greeting string of context D")
(println D:greeting)
this is the greeting string of context D
在这两个示例中,请注意你一直保留在 MAIN 上下文中。
以下代码创建一个名为L 的新上下文,其中包含一个名为ls 的新列表,该列表包含字符串
(set 'L:ls '("this" "is" "a" "list" "of" "strings"))
;-> ("this" "is" "a" "list" "of" "strings")
上下文可以包含函数和符号。要在除 MAIN 以外的上下文中创建函数,可以执行以下操作
(context Doyle) ; switch to existing context
(define (hello-world) ; define a local function
(println "Hello World"))
或者执行以下操作
(context MAIN) ; stay in MAIN
(define (Doyle:hello-world) ; define function in context
(println "Hello World"))
此第二种语法允许你在上下文中创建上下文和函数,同时始终安全地保留在 MAIN 上下文中。
(define (Moriarty:helloworld)
(println "(evil laugh) Hello World"))
你无需在此处引用新的上下文名称,因为我们使用的是 define,而 define(根据定义)不会期望现有符号的名称。
要在另一个上下文中使用函数,请记住使用 context:function 语法调用它们。
如果上下文中的符号与上下文名称相同,则称为默认函数(尽管实际上它可以是函数,也可以是包含列表或字符串的符号)。例如,这里有一个名为 Evens 的上下文,它包含一个名为 Evens 的符号
(define Evens:Evens (sequence 0 30 2))
;-> (0 2 4 6 8 10 12 14 16 18 20 22 24 26 28 30)
Evens:Evens
;-> (0 2 4 6 8 10 12 14 16 18 20 22 24 26 28 30)
这里有一个名为 Double 的上下文,它包含一个名为 Double 的函数
(define (Double:Double x)
(mul x 2))
因此,Evens 和 Double 是它们上下文的默认函数。
默认函数有很多优点。如果默认函数与上下文名称相同,则每当你在表达式中使用上下文名称时,它都会被评估,除非 newLISP 期望上下文名称。例如,虽然你可以始终使用 context 函数以通常的方式切换到 Evens 上下文
> (context Evens)
Evens
Evens> (context MAIN)
MAIN
>
你可以将 Evens 用作列表(因为 Evens:Evens 是一个列表)
(reverse Evens)
;-> (30 28 26 24 22 20 18 16 14 12 10 8 6 4 2 0)
你可以使用默认函数,而无需提供其完整地址。同样,你可以将 Double 函数用作普通函数,而无需提供完整的冒号分隔地址
> (Double 3)
6
你仍然可以以通常的方式切换到 Double 上下文
> (context Double)
Double
Double>
newLISP 足够智能,能够从你的代码中判断是使用上下文的默认函数还是上下文本身。
当用作符号时,默认函数与它们更普通的同类函数之间存在重要区别。当使用默认函数将数据传递给函数时,newLISP 使用数据的引用而不是副本。对于较大的列表和字符串,引用对于 newLISP 在函数之间传递要快得多,因此如果你可以将数据存储为默认函数,并将上下文名称用作参数,则你的代码将更快。
此外,并且作为结果,函数会更改作为引用参数传递的任何默认函数的内容。普通符号在作为参数传递时会被复制。观察以下代码。我将创建两个符号,其中一个为“默认函数”,另一个为普通符号
(define Evens:Evens (sequence 0 30 2)) ; symbol is the default function for the Evens context
;-> (0 2 4 6 8 10 12 14 16 18 20 22 24 26 28 30)
(define odds (sequence 1 31 2)) ; ordinary symbol
;-> (1 3 5 7 9 11 13 15 17 19 21 23 25 27 29 31)
; this function reverses a list
(define (my-reverse lst)
(reverse lst))
(my-reverse Evens) ; default function as parameter
;-> (30 28 26 24 22 20 18 16 14 12 10 8 6 4 2 0)
(my-reverse odds) ; ordinary symbol as parameter
;-> (31 29 27 25 23 21 19 17 15 13 11 9 7 5 3 1)
到目前为止,它们看起来行为相同。但现在检查原始符号
> Evens:Evens
(30 28 26 24 22 20 18 16 14 12 10 8 6 4 2 0)
> odds
(1 3 5 7 9 11 13 15 17 19 21 23 25 27 29 31)
作为默认函数(作为引用)传递的列表被修改,而普通列表参数按预期复制,没有被修改。
在下面的示例中,我们创建一个名为Output的上下文,并在其中创建一个名为Output的默认函数。此函数打印其参数,并根据输出的字符数增加计数器。因为默认函数与上下文具有相同的名称,所以当我们在其他表达式中使用上下文的名称时,它就会被执行。
在此函数内部,如果变量counter(在Output上下文中)存在,则增加其值;如果不存在,则创建并初始化它。然后执行函数的主要任务——打印参数。counter符号用于统计输出的字符数。
(define (Output:Output) ; define the default function
(unless Output:counter
(set 'Output:counter 0))
(inc Output:counter (length (string (args))))
(map print (args))
(println))
(dotimes (x 90)
(Output ; use context name as a function
"the square root of " x " is " (sqrt x)))
(Output "you used " Output:counter " characters")
the square root of 0 is 0 the square root of 1 is 1 the square root of 2 is 1.414213562 the square root of 3 is 1.732050808 the square root of 4 is 2 the square root of 5 is 2.236067977 the square root of 6 is 2.449489743 the square root of 7 is 2.645751311 the square root of 8 is 2.828427125 the square root of 9 is 3 ... the square root of 88 is 9.38083152 the square root of 89 is 9.433981132 you used 3895 characters
Output函数能够有效地记住它自创建以来完成的工作量。它甚至可以将这些信息追加到日志文件中。
想想这些可能性。你可以记录所有函数的使用情况,并根据用户使用函数的频率向他们收费。
你可以覆盖内置的println函数,使其在被调用时使用此代码。请参见按你的方式。
字典和表格
[edit | edit source]上下文的一个常见用途是字典:一个有序的唯一键/值对集合,可以按顺序排列,以便你可以获取某个键的当前值,或者添加新的键/值对。newLISP 使创建字典变得容易。为了说明,我将求助于伟大的侦探,夏洛克·福尔摩斯。首先,我从古腾堡计划下载了阿瑟·柯南·道尔的四签名,然后将其加载为一个词语列表。
(set 'file "/Users/me/Sherlock Holmes/sign-of-four.txt")
(set 'data (clean empty? (parse (read-file file) "\\W" 0))) ;read file and remove all white-spaces, returns a list.
接下来,我定义一个空字典
(define Doyle:Doyle)
这定义了Doyle上下文和默认函数,但将默认函数保留为未初始化状态。如果默认函数为空,则可以使用以下表达式来构建和检查字典
- (Doyle key value) - 将键设置为值
- (Doyle key) - 获取键的值
- (Doyle key nil) - 删除键
要从词语列表中构建字典,你需要扫描词语,如果词语不在字典中,则将其作为键添加,并将值设置为1。但如果词语已在字典中,则获取其值,加1,并保存新值
(dolist (word data)
(set 'lc-word (lower-case word))
(if (set 'tally (Doyle lc-word))
(Doyle lc-word (inc tally))
(Doyle lc-word 1)))
这种更短的替代方案消除了条件语句
(dolist (word data)
(set 'lc-word (lower-case word))
(Doyle lc-word (inc (int (Doyle lc-word)))))
或者,更简短的写法
(dolist (word data)
(Doyle (lower-case word) (inc (Doyle (lower-case word)))))
每个词语都被添加到字典中,并且值(出现次数)增加1。在上下文中,键的名称前面都加了一个下划线 ("_")。这是为了避免人们混淆键的名称和 newLISP 保留字,其中许多保留字出现在柯南·道尔的文本中。
你可以通过多种方式浏览字典。要查看单个符号
(Doyle "baker")
;-> 10
(Doyle "street")
;-> 26
要查看符号在上下文中存储的方式,可以使用dotree遍历上下文,并评估每个符号
(dotree (wd Doyle)
(println wd { } (eval wd)))
Doyle:Doyle nil Doyle:_1 1 Doyle:_1857 1 Doyle:_1871 1 Doyle:_1878 2 Doyle:_1882 3 Doyle:_221b 1 ... Doyle:_your 107 Doyle:_yours 7 Doyle:_yourself 9 Doyle:_yourselves 2 Doyle:_youth 3 Doyle:_zigzag 1 Doyle:_zum 2
要将字典显示为关联列表,请单独使用字典名称。这会创建一个新的关联列表:
(Doyle)
;-> (("1" 1)
("1857" 1)
("1871" 1)
("1878" 2)
("1882" 3)
("221b" 1)
...
("you" 543)
("young" 19)
("your" 107)
("yours" 7)
("yourself" 9)
("yourselves" 2)
("youth" 3)
("zigzag" 1)
("zum" 2))
这是一个标准的关联列表,你可以使用列表章节中描述的函数来访问它(参见关联列表)。例如,要查找所有出现20次的词语,可以使用find-all
(find-all '(? 20) (Doyle) (println $0))
;-> ("friends" 20)
("gone" 20)
("seemed" 20)
("those" 20)
("turned" 20)
("went" 20)
由(Doyle)返回的关联列表是字典中数据的临时副本,而不是原始字典上下文。要更改数据,不要操作此临时列表,而是操作上下文的數據,使用键/值访问技术。
你还可以使用关联列表形式的数据向字典中添加新条目,或修改现有条目
(Doyle '(("laser" 0) ("radar" 0)))
保存和加载上下文
[edit | edit source]如果你想再次使用字典,可以将上下文保存到文件中
(save "/Users/me/Sherlock Holmes/doyle-context.lsp" 'Doyle)
这组数据被包装在一个名为Doyle的上下文中,可以被另一个脚本或newLISP会话快速加载,使用
(load "/Users/me/Sherlock Holmes/doyle-context.lsp")
newLISP会自动重新创建Doyle上下文中的所有符号,并在完成时切换回MAIN(默认)上下文。
使用newLISP模块
[edit | edit source]上下文被用作软件模块的容器,因为它们提供了词法分离的命名空间。与newLISP安装一起提供的模块通常定义一个上下文,其中包含一组函数,用于处理特定领域的任務。
以下是一个示例。POP3模块允许你检查POP3电子邮件帐户。首先加载模块
(load "/usr/share/newlisp/modules/pop3.lsp")
该模块现在已添加到newLISP系统中。你可以切换到上下文
(context POP3)
并调用上下文中的函数。例如,要检查你的电子邮件,请使用get-mail-status函数,提供用户名、密码和POP3服务器名称
(get-mail-status "[email protected]" "secret" "mail.example.com")
;-> (3 197465 37)
; (totalMessages, totalBytes, lastRead)
如果你不切换到上下文,仍然可以通过提供完整的地址来调用同一个函数
(POP3:get-mail-status "[email protected]" "secret" "mail.example.com")
作用域
[edit | edit source]你已经看到了newLISP动态查找符号的当前版本的方式(参见作用域)。但是,当你使用上下文时,可以使用一种不同的方法,程序员称之为词法作用域。使用词法作用域,你可以明确地控制使用哪个符号,而不是依赖 newLISP 自动为你跟踪类似命名的符号。
在以下代码中,width符号在Right-just上下文中定义。
(context 'Right-just)
(set 'width 30)
(define (Right-just:Right-just str)
(slice (string (dup " " width) str) (* width -1)))
(context MAIN)
(set 'width 0) ; this is a red herring
(dolist (w (symbols))
(println (Right-just w)))
! != $ $0 $1 ... write-line xml-error xml-parse xml-type-tags zero? | ~
第二行(set 'width ...)是一个误导:改变这里没有任何作用,因为实际由右对齐函数使用的符号在不同的上下文中。
你仍然可以进入Right-just上下文来设置宽度
(set 'Right-just:width 15)
关于两种方法的优缺点已经有很多讨论。无论你选择哪种方法,请确保你了解代码运行时符号将从哪里获取其值。例如
(define (f y)
(+ y x))
这里,y是函数的第一个参数,并且独立于任何其他y。但x呢?它是全局符号,还是其值在刚刚调用f的某个其他函数中定义了?或者它根本没有值!
最好避免使用这些free符号,并在可能的情况下使用局部变量(用let或local定义)。也许你可以采用一种约定,例如在全局符号周围加上星号。
对象
[edit | edit source]关于面向对象编程 (OOP) 的书籍比你一生所能阅读的还要多,所以本节只是对该主题的简要介绍。newLISP 足够灵活,可以支持多种 OOP 风格,你可以在网上轻松找到这些风格的参考资料,以及关于每种风格优点的讨论。
在本介绍中,我将简要概述其中一种风格:FOOP,即函数式面向对象编程。
FOOP 简介
[edit | edit source]FOOP 在 newLISP 版本 10.2(2010 年初)中发生了变化,所以如果你使用的是旧版本的 newLISP,请更新它。
在 FOOP 中,每个对象都存储为一个列表。类方法和类属性(即适用于该类所有对象的函数和符号)存储在上下文中。
对象存储在列表中,因为列表是 newLISP 的基本元素。对象列表中的第一个元素是一个符号,用于标识对象的类;其余元素是描述对象属性的值。
同一个类中的所有对象共享相同的属性,但这些属性可以具有不同的值。该类还可以具有在类中的所有对象之间共享的属性;这些是类属性。存储在类上下文中的函数提供了管理对象和处理它们所保存数据的各种方法。
为了说明这些概念,请考虑以下处理时间和日期的代码。它建立在 newLISP 提供的基本日期和时间函数的基础之上(参见使用日期和时间)。某个时刻用时间对象表示。一个对象保存两个值:自 1970 年初开始经过的秒数,以及时区偏移量,以格林威治以西的分钟数表示。所以表示典型时间对象的列表如下
(Time 1219568914 0)
其中 Time 是一个表示类名的符号,两个数字是这个特定时间的数值(这些数字表示 2008 年 8 月 24 日星期日上午 10 点左右,英国某地的时间)。
使用 newLISP 通用 FOOP 构造函数构建此对象所需的代码很简单
(new Class 'Time) ; defines Time context
(setq some-england-date (Time 1219568914 0))
但是,你可能想要定义一个不同的构造函数,例如,你可能想要给这个对象一些默认值。为此,你需要重新定义充当构造函数的默认函数
(define (Time:Time (t (date-value)) (zone 0))
(list Time t zone))
它是 Time 上下文的默认函数,它构建一个列表,其中第一个位置是类名,另外两个整数表示时间。当没有提供值时,它们默认为当前时间和零偏移量。现在你可以使用构造函数,而不提供任何参数
(set 'time-now (Time))
;-> your output *will* differ for this one but will be something like (Time 1324034009 0)
(set 'my-birthday (Time (date-value 2008 5 26)))
;-> (Time 1211760000 0)
(set 'christmas-day (Time (date-value 2008 12 25)))
;-> (Time 1230163200 0)
接下来,你可以定义其他函数来检查和管理时间对象。所有这些函数都一起存在于上下文中。它们可以通过使用self函数从对象中获取秒数和时区信息来提取它们。所以(self 1)获取秒数,并且(self 2)获取作为参数传递的对象的时间区域偏移量。请注意,定义不需要您声明对象参数。以下是一些显而易见的类函数
(define (Time:show)
(date (self 1) (self 2)))
(define (Time:days-between other)
"Return difference in days between two times."
(div (abs (- (self 1) (other 1))) (* 24 60 60)))
(define (Time:get-hours)
"Return hours."
(int (date (self 1) (self 2) {%H})))
(define (Time:get-day)
"Return day of week."
(date (self 1) (self 2) {%A}))
(define (Time:leap-year?)
(let ((year (int (date (self 1) (self 2) {%Y}))))
(and (= 0 (% year 4))
(or (!= 0 (% year 100)) (= 0 (% year 400))))))
这些函数通过使用冒号运算符并提供我们希望函数作用于的对象来调用
; notice 'show' uses 'date' which works with local time, so your output probably will differ
(:show christmas-day)
;-> Thu Dec 25 00:00:00 2008
(:show my-birthday)
;-> Mon May 26 01:00:00 2008
注意我们如何使用冒号运算符作为函数的前缀,而不使用空格隔开,这是一种风格问题,您可以使用或不使用空格
(:show christmas-day) ; same as before
;-> Thu Dec 25 00:00:00 2008
(: show christmas-day) ; notice the space between colon and function
;-> Thu Dec 25 00:00:00 2008
这种技术允许 newLISP 提供面向对象程序员喜欢的功能:多态性。
让我们添加另一个类,它处理持续时间 - 两个时间对象之间的间隔 - 以天为单位。
(define (Duration:Duration (d 0))
(list Duration d))
(define (Duration:show)
(string (self 1) " days "))
有一个新的类构造函数 Duration:Duration 用于创建新的持续时间对象,以及一个简单的 show 函数。它们可以与时间对象一起使用,如下所示
; define two times
(set 'time-now (Time) 'christmas-day (Time (date-value 2008 12 25)))
; show days between them using the Time:days-between function
(:show (Duration (:days-between time-now christmas-day)))
;-> "122.1331713 days "
将该 :show 函数调用与上一节中的 :show 进行比较
(:show christmas-day)
;-> Thu Dec 25 00:00:00 2008
您可以看到 newLISP 根据 :show 参数的类选择要评估的 show 函数的哪个版本。因为 christmas-day 是一个 Time 对象,所以 newLISP 评估 Time:show。但是当参数是 Duration 对象时,它会评估 Duration:show。这个想法是您可以在各种类型的对象上使用一个函数:您可能不需要知道正在处理的对象的类型。通过这种多态性,您可以将 show 函数应用于不同类型的对象的列表,而 newLISP 会每次选择适当的函数
(map (curry :show)
(list my-birthday (Duration (:days-between time-now christmas-day))))
;-> ("Mon May 26 01:00:00 2008" "123.1266898 days ")
注意:我们必须在这里使用curry,因为map 需要同时使用冒号运算符和show 函数。
我们称这种特定的 OOP 风格为FOOP,因为它被认为是函数式的。在这里,术语“函数式”指的是 newLISP 鼓励的编程风格,它强调函数的评估,避免状态和可变数据。正如您所见,许多 newLISP 函数返回列表的副本,而不是修改原始列表。但是,有一些函数被称为破坏性的,这些函数被认为不太纯粹地是函数式的。但 FOOP 不提供破坏性的对象方法,因此可以被认为更函数式。
关于 FOOP 需要注意的一点是,对象是不可变的;它们不能被类函数修改。例如,这里有一个用于 Time 类的函数,它将给定数量的天数添加到时间对象中
(define (Time:adjust-days number-of-days)
(list Time (+ (* 24 60 60 number-of-days) (self 1)) (self 2)))
当调用此函数时,它会返回对象的修改副本;原始对象保持不变
(set 'christmas-day (Time (date-value 2008 12 25)))
;-> (Time 1230163200 0)
(:show christmas-day)
;-> "Thu Dec 25 00:00:00 2008"
(:show (:adjust-days christmas-day 3))
;-> "Sun Dec 28 00:00:00 2008"
(:show christmas-day)
;-> "Thu Dec 25 00:00:00 2008"
; notice it's unchanged
christmas-day 对象的原始日期没有改变,尽管 :adjust-days 函数返回了一个调整了 3 天的修改副本。
换句话说,要更改对象,请使用熟悉的新LISP 方法,使用函数返回的值
(set 'christmas-day (:adjust-days christmas-day 3))
(:show christmas-day)
;-> "Sun Dec 28 00:00:00 2008"
christmas-day 现在包含修改后的日期。
您可以在 newLISP 论坛中搜索以下内容,找到此想法的更完整的阐述timeutilities。另外,请务必阅读参考手册中有关 FOOP 的部分,其中有一个关于嵌套对象的很好的例子,即包含其他对象的物体。