如何将我的 GUI 代码从我的功能中分离到 2 个 Rkt 文件中?
How can I separate my GUI code from my functionality into 2 Rkt files?
我有一个大型程序,它在修改对象状态的函数和调用函数的对象实例之间调用。
现在,它们都在一个 .rkt 文件中。一些函数引用对象来更新它们的内容,一些对象引用回调字段中的函数。有没有办法将功能与 GUI 代码分开?
正如我在评论中提到的,这个问题分为两部分:
- 首先是您如何构建具有 GUI 组件的程序,或者一般来说;
- 第二个是 Racket 如何帮助你解决第一个问题。
我认为第一部分是一个很大的话题:关于这方面的书籍和课程都有所讲授。我绝对不能回答那部分。
第二部分,嗯,我也不太愿意回答这个问题,因为我不是真正的球拍专家。但是,正如其他人所做的那样,也许我可以为简单的案例提供帮助(我认为大多数案例都很简单)。
警告:以下内容肯定过于简单,几乎肯定包含错误。我欢迎任何比我更了解 Racket 模块系统的人指正。本来应该很简单的答案也太长了:抱歉。
球拍中的模块
要认识到的基本事情是,你在 Racket 中编写的所有内容都是模块定义的一部分,(整个 #lang ...
是模块定义的语法糖)。
模块可以决定他们想要导出到其他模块的名称(?),以及他们想要如何导出它们的一些事情,以及他们依赖的其他模块以及他们如何依赖它们的一些事情.
模块可以嵌套在其他模块中,包括在同一个文件中。但最简单的情况是每个文件都包含一个模块,这就是我要尝试处理的所有内容。
引入模块here and the reference manual is here。
定义提供的名称:provide
这是由 provide
完成的。这是一些语法,它说明了从模块中导出的名称:模块中的所有其他定义都是私有的。
provide
有很多复杂性,但假设我有一些文件定义了 'doing something with a foo' 的一些概念。它想定义一个函数 call-with-foo
和一个宏 with-foo
,它们以正常方式相互关联。但是文件中还有很多其他内容与私有的 foo
s 的实现有关。所以我的文件 "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 ...)
您可以说 'export everything' (all-defined-out
) 或 'export everything except ...' (except-out
) 等等。您可以做很多事情。
指定你依赖的模块
所以一个模块通常有两种方法从其他模块导入名称。
第一个是通过 #lang ...
:类似于
#lang racket
...
我认为和
一样
(module <name> racket ...)
其中 <name>
来自文件名,这意味着 'start by using all the name that the racket
module exports (but be willing to override them)'。我认为还不止于此,因为您还可以在此处重新定义文件其余部分语法的基本方面。在任何情况下 #lang
都会告诉模块应该从哪里开始。
另一种方法是 require
。这比 provide
还要麻烦,因为它不仅需要能够指定 'I need only some things from this module' 和 'I need things from this module under some different names' 之类的东西,而且 还需要 能够指定 'this module' 的含义。
您看到的最常见的指定 'thos module' 的情况类似于 (require racket/tcp)
,这意味着 'I need the "tcp"
module from the "racket"
collection'(这与 (require (lib "racket/tcp")
秘密相同,有点像事实上,我认为更容易理解),其中整个 'collection' 的东西是 arcane and complicated 软件安装系统总是这样(但我认为这并非不可理解)。
但是对于您定义为程序一部分的模块,您正在编写的事情要简单得多:您通过(表示字符串的字符串)其文件的名称指定 'this module',该名称相对于正在执行的模块进行解释require
ing。如果我想从上面的 "foo.rkt"
模块导入东西,我只是说:
(require "foo.rkt")
现在我拥有了它愿意给我的一切(一切都以 provide
形式)。
与 provide
一样,我可以使用各种技巧来指定我想要获得的内容,以及重命名 &c &c。一个适用于 "foo.rkt"
的 provide
形式的简单案例是:
(require (only-in "foo.rkt" with-foo))
意思是“只要给我with-foo
,我不关心其他任何事情”。这很有用,因为它意味着您可以非常具体地指定您想要的名称,而不是让您的模块乱七八糟。
您可以使用 require
.
做很多其他事情
模块和合同
您可以做一件非常有用的事情来指定模块边界处的契约。合约介绍 here and reference material is here.
假设,对于我的 "foo.rkt"
模块,我知道 call-with-foo
需要一个过程作为其参数,并且该过程有一个参数,并且可能 return 任何东西。有两种方法可以做到这一点:您可以在 "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
形式:
#lang racket
(provide (contract-out
(call-with-foo
(-> (-> any/c any) any))))
(define (call-with-foo fn)
...)
所有 (require "x/y.rkt")
看起来都是*nix 特定的,但实际上它们都是平台不可知的:模块规范并不是真正的路径名,它们只是被翻译成路径名,而且翻译适合平台。
(这个 "main.rkt"
的原因是,如果它变成了一个库,那么 (require .../foo)
就意味着 'look for "main.rkt"
wherever .../foo
told you to go'。至少我是这么认为的。)
我有一个大型程序,它在修改对象状态的函数和调用函数的对象实例之间调用。
现在,它们都在一个 .rkt 文件中。一些函数引用对象来更新它们的内容,一些对象引用回调字段中的函数。有没有办法将功能与 GUI 代码分开?
正如我在评论中提到的,这个问题分为两部分:
- 首先是您如何构建具有 GUI 组件的程序,或者一般来说;
- 第二个是 Racket 如何帮助你解决第一个问题。
我认为第一部分是一个很大的话题:关于这方面的书籍和课程都有所讲授。我绝对不能回答那部分。
第二部分,嗯,我也不太愿意回答这个问题,因为我不是真正的球拍专家。但是,正如其他人所做的那样,也许我可以为简单的案例提供帮助(我认为大多数案例都很简单)。
警告:以下内容肯定过于简单,几乎肯定包含错误。我欢迎任何比我更了解 Racket 模块系统的人指正。本来应该很简单的答案也太长了:抱歉。
球拍中的模块
要认识到的基本事情是,你在 Racket 中编写的所有内容都是模块定义的一部分,(整个 #lang ...
是模块定义的语法糖)。
模块可以决定他们想要导出到其他模块的名称(?),以及他们想要如何导出它们的一些事情,以及他们依赖的其他模块以及他们如何依赖它们的一些事情.
模块可以嵌套在其他模块中,包括在同一个文件中。但最简单的情况是每个文件都包含一个模块,这就是我要尝试处理的所有内容。
引入模块here and the reference manual is here。
定义提供的名称:provide
这是由 provide
完成的。这是一些语法,它说明了从模块中导出的名称:模块中的所有其他定义都是私有的。
provide
有很多复杂性,但假设我有一些文件定义了 'doing something with a foo' 的一些概念。它想定义一个函数 call-with-foo
和一个宏 with-foo
,它们以正常方式相互关联。但是文件中还有很多其他内容与私有的 foo
s 的实现有关。所以我的文件 "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 ...)
您可以说 'export everything' (all-defined-out
) 或 'export everything except ...' (except-out
) 等等。您可以做很多事情。
指定你依赖的模块
所以一个模块通常有两种方法从其他模块导入名称。
第一个是通过 #lang ...
:类似于
#lang racket
...
我认为和
一样(module <name> racket ...)
其中 <name>
来自文件名,这意味着 'start by using all the name that the racket
module exports (but be willing to override them)'。我认为还不止于此,因为您还可以在此处重新定义文件其余部分语法的基本方面。在任何情况下 #lang
都会告诉模块应该从哪里开始。
另一种方法是 require
。这比 provide
还要麻烦,因为它不仅需要能够指定 'I need only some things from this module' 和 'I need things from this module under some different names' 之类的东西,而且 还需要 能够指定 'this module' 的含义。
您看到的最常见的指定 'thos module' 的情况类似于 (require racket/tcp)
,这意味着 'I need the "tcp"
module from the "racket"
collection'(这与 (require (lib "racket/tcp")
秘密相同,有点像事实上,我认为更容易理解),其中整个 'collection' 的东西是 arcane and complicated 软件安装系统总是这样(但我认为这并非不可理解)。
但是对于您定义为程序一部分的模块,您正在编写的事情要简单得多:您通过(表示字符串的字符串)其文件的名称指定 'this module',该名称相对于正在执行的模块进行解释require
ing。如果我想从上面的 "foo.rkt"
模块导入东西,我只是说:
(require "foo.rkt")
现在我拥有了它愿意给我的一切(一切都以 provide
形式)。
与 provide
一样,我可以使用各种技巧来指定我想要获得的内容,以及重命名 &c &c。一个适用于 "foo.rkt"
的 provide
形式的简单案例是:
(require (only-in "foo.rkt" with-foo))
意思是“只要给我with-foo
,我不关心其他任何事情”。这很有用,因为它意味着您可以非常具体地指定您想要的名称,而不是让您的模块乱七八糟。
您可以使用 require
.
模块和合同
您可以做一件非常有用的事情来指定模块边界处的契约。合约介绍 here and reference material is here.
假设,对于我的 "foo.rkt"
模块,我知道 call-with-foo
需要一个过程作为其参数,并且该过程有一个参数,并且可能 return 任何东西。有两种方法可以做到这一点:您可以在 "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
形式:
#lang racket
(provide (contract-out
(call-with-foo
(-> (-> any/c any) any))))
(define (call-with-foo fn)
...)
所有 (require "x/y.rkt")
看起来都是*nix 特定的,但实际上它们都是平台不可知的:模块规范并不是真正的路径名,它们只是被翻译成路径名,而且翻译适合平台。
(这个 "main.rkt"
的原因是,如果它变成了一个库,那么 (require .../foo)
就意味着 'look for "main.rkt"
wherever .../foo
told you to go'。至少我是这么认为的。)