我有一个大型程序,该程序在修改对象状态的函数与调用函数的对象实例之间进行调用。
现在,它们都在一个.rkt文件中。一些函数引用对象以更新其内容,而某些对象引用其回调字段中的函数。有没有办法将功能与GUI代码分开?
答案 0 :(得分:1)
正如我在评论中提到的那样,此问题分为两部分:
我认为第一部分是一个巨大的话题:已经编写了有关该主题的书籍,并开设了课程。我绝对不愿意回答那部分。
第二部分,嗯,由于我不是一名真正的球拍专家,所以我还是不太愿意回答这个问题。但是,就像没有其他人一样,也许我可以为一些简单的案例提供帮助(我认为大多数案例都证明很简单)。
警告:肯定过分简化,几乎可以肯定包含错误。我欢迎任何比我更了解Racket模块系统的人进行纠正。答案很简单,很简单:抱歉。
要意识到的基本事情是,您在Racket中编写的所有内容都是模块定义的一部分(整个#lang ...
东西是模块定义的语法糖)。
模块可以决定要导出到其他模块的名称(?),以及有关如何导出它们的一些信息,还取决于它们依赖的其他模块以及它们如何依赖它们的一些信息。 / p>
模块可以嵌套在其他模块中,包括在同一文件中。但是最简单的情况是,每个文件都包含一个模块,这就是我什至会尝试处理的全部内容。
provide
这是由provide
完成的。这有点语法,它说明了从模块导出的名称:模块中的所有其他定义都是私有的。
provide
有很多复杂性,但是假设我有一个文件定义了“用foo做某事”的概念。它想定义一个函数call-with-foo
和一个宏with-foo
,它们以常规方式相互关联。但是文件中还有很多其他事情与foo
的实现有关,这是私有的。所以我的文件"foo.rkt"
可能看起来像这样:
#lang racket
(provide call-with-foo
with-foo)
(define (call-with-foo fn)
...)
(define-syntax-rule (with-foo (f) form ...)
(call-with-foo (λ (f) form ...)))
(define (make-foo ...)
...)
(define (validate-foo foo)
...)
...
因此,这意味着任何想使用该模块的模块只能看到call-with-foo
和with-foo
:所有其他定义都是内部的。
provide
可以做的还很多:例如,它可以在导出定义时重命名它们。如果您要重新定义语言的基本部分,这将很有用。例如,如果我定义的语言有点像Racket,但是define
不同,我可能会写:
#lang racket
(provide (rename-out [new-define define]))
(define-syntax new-define
...)
;;; This is Racket's define, not ours
;;;
(define ...)
您可以说诸如“导出所有内容”(all-defined-out
)或“导出除...以外的所有内容”(except-out
)之类的东西,依此类推。您可以做很多事情。
因此,通常有两种方法可以从其他模块导入名称。
第一个是通过#lang ...
:类似
#lang racket
...
我认为与
相同(module <name> racket ...)
<name>
来自文件名,这意味着“首先使用racket
模块导出的所有名称(但愿意覆盖它们)”。我认为还有更多,因为您还可以在此处重新定义文件其余部分的语法的基本方面。无论如何,#lang
告诉模块应该从哪里开始。
另一种方法是通过require
。它甚至比provide
还要毛茸茸,因为它不仅需要能够指定诸如“我只需要此模块中的某些东西”和“我需要此模块中的某些名称下的东西”之类的东西,它还< em> 还需要能够指定“此模块”的含义。
您看到的最常见的情况是指定“ thos模块”,类似于(require racket/tcp)
,这意味着“我需要"tcp"
集合中的"racket"
模块”(这是秘密地与(require (lib "racket/tcp")
相同,实际上我认为这样更容易理解),其中整个“集合”都是arcane and complicated,从某种意义上说,软件安装系统一直都是这样(尽管如此,我认为这并不是不可理解的)
但是对于您定义为程序一部分的模块,编写事情要简单得多:您可以通过(表示字符串的)文件名来指定“此模块”,该文件名相对于执行{{ 1}}。如果我想从上面的require
模块导入内容,我只是说:
"foo.rkt"
现在,我已经拥有了愿意给我的一切(一切都以其(require "foo.rkt")
形式存在)。
与provide
一样,我可以做各种技巧来指定我想要得到的东西,以及重命名&c&c。适用于provide
的{{1}}格式的简单情况是:
"foo.rkt"
这意味着“只要给我provide
,我什么都不关心”。这很有用,因为这意味着您可以非常具体地确定所需的名称,而不会因垃圾而使模块混乱。
(require (only-in "foo.rkt" with-foo))
可以做很多其他事情。
您可以执行一项非常有用的操作,以在模块边界指定合同。介绍了合同here,参考资料是here。
让我们说,对于我的with-foo
模块,我知道require
期望将过程作为其参数,并且该过程只获得一个参数,并且可能返回任何内容。有两种方法可以执行此操作:您可以在"foo.rkt"
中的函数上定义协定:
call-with-foo
或者您可以在"foo.rkt"
级别指定联系人:
(define/contract (call-with-foo fn)
(-> (-> any/c any) any))
...)
对于模块用户,这些基本相同。第一种情况看起来更好,因为即使在模块内也会执行合同。但是第一种情况,例如,允许您在模块边界执行合同,该合同比模块内的合同严格,这很有用。
在任何情况下,合同都是一个很不错的工具,可以尽早发现问题,并且在模块边界特别有用。
几乎不可避免地发生的一件事是,您的小型单文件模块最终变得太大,因此您希望它成为一个以上的文件。这很容易做到:您可以使您的主模块文件重新实现模块。因此,例如,provide
可能变为:
(provide (contract-out
(call-with-foo
(-> (-> any/c any) any)))
with-foo)
和"foo.rkt"
可能是:
#lang racket
(require "foo/main.rkt")
(provide (all-from-out "foo/main.rkt"))
最后,"foo/main.rkt"
可能已经实现了现在庞大的模块的一部分,并适当地以#lang racket
(require "simple.rkt" "complicated.rkt")
(provide (all-from-out "simple.rkt" "complicated.rkt"))
形式完成了>
"foo/simple.rkt"
所有provide
似乎都是* nix所特有的,但是实际上这一切都是平台无关的:模块规范并不是真正的路径名,它们只是被翻译成路径名,并且翻译发生了适合平台。
(#lang racket
(provide (contract-out
(call-with-foo
(-> (-> any/c any) any))))
(define (call-with-foo fn)
...)
的原因是,如果它变成了一个库,那么(require "x/y.rkt")
的意思是无论"main.rkt"
告诉您去哪里,(require .../foo)
'。至少我是这样认为的。)