非容器中的容器和 Racket 中的 Hacking Contracts

Containees in nonContainers and Hacking Contracts in Racket

序言:我真的不知道自己在做什么。我是球拍和编程的新手。我学得慢。

我安装了 DrRacket,但我正在处理“racket/gui/base”。

我想将容器放入不是容器的对象中。

合同阻止我这样做。

我的第一个想法是想方设法破坏合同或修改合同。

  1. 这是一条富有成果的途径吗?
  2. 时间会不会太长?

我开始读这个:https://docs.racket-lang.org/guide/contract-boundaries.html

  1. 考虑到我想解决如下问题,这是在浪费时间吗?

例如,假设我想在菜单中放置一个按钮而不是菜单项。

  1. 在上述情况下,我该怎么做才能避免违反合同?

  2. 我可以将该解决方案概括为 4 个吗?还是特定于这些对象?

  3. 我可以在球拍中投掷物体来避免这个合同问题吗?

清楚合同的用途很重要。就像法律合同一样,Racket 的合同做两件事:

  • 他们告诉合同的所有各方他们必须做什么才能遵守合同;
  • 他们告诉合同的所有各方他们可以根据合同承担什么。

这两个方面都很重要,但第二个在这里更重要。例如考虑阶乘函数:

(define (factorial n)
  (if (= n 0)
      1
      (* n (factorial (- n 1)))))

这一切似乎都很好:

> (factorial 10)
3628800
> (factorial 100)
93326215443944152681699238856266700490715968264381621468592963895217599993229915608941463976156518286253697920827223758251185210916864000000000000000000000000

除了一点都不好:

> (factorial 12.2)

然后有很长的停顿,然后在某个时候 Racket 内存不足。

嗯,那是因为当我写 factorial 时,我是在理解阶乘函数的定义域和范围的情况下写的:对于这个版本的阶乘函数,它的定义域是自然数(整数大于或等于 0)并且它的范围是大于或等于 1 的整数。并且该实现严重依赖于 属性 自然数:自然数 n 要么为零,要么从中减去 1次数够多,这个数字就是零。

当我调用 factorial 时,我用一个不在其域中的号码调用它,但它不知道,所以它根本无法终止.好吧,我们可以通过为 factorial 提供合约来解决这个问题。我将通过直接在函数上而不是在模块级别提供合同来做到这一点,因为这意味着我必须输入更少的内容:

(define/contract (factorial n)
    (-> natural-number/c (integer-in 1 #f))
    (if (= n 0)
        1
        (* n (factorial (- n 1)))))

现在

> (factorial 10)
3628800
> (factorial 12.2)
; factorial: contract violation
;   expected: natural-number/c
;   given: 12.2
;   [...]

现在有一份合同可以保护 factorial 免受我的伤害。不幸的是,它也保护它不受自身的影响;每次它调用自己时,它都必须尽职地检查自己的合同。好吧,我们可以避免这种情况:

(define/contract (factorial n)
  (-> natural-number/c (integer-in 1 #f))
  (define (floop m r)
    (if (= m 0)
        r
        (floop (- m 1) (* r m))))
  (floop n 0))

factorial之内,合同的事情我可以假设,事情不需要执着去核对

除了我犯了一个错误:

> (factorial 10)
; factorial: broke its own contract
;   promised: exact-positive-integer?
;   produced: 0

所以现在 factorial 上的合约正在保护 me 免受错误实施的影响。我编写的使用 factorial 的代码可以安全地假设它的 return 值将是一个大于或等于 1 的整数。我当然可以解决这个问题:

(define/contract (factorial n)
  (-> natural-number/c (integer-in 1 #f))
  (define (floop m r)
    (if (= m 0)
        r
        (floop (- m 1) (* r m))))
  (floop n 1))

好的,所以这是一个很长的序言:

  • 合同规定了双方的义务;
  • 合同允许各方做出假设。

特别是,几乎可以肯定的是,容器 假设 放入其中的对象是容器,并且具有容器的行为,因为它们知道是一份使那成为现实的合同。

如果您以某种方式设法规避了该合同,那么将会发生的情况是容器仍会假定它是真实的,并且仍然会做出相同的假设。结果将是某种灾难:如果你幸运的话,你会得到错误,如果你不幸运,屏幕上只会出现垃圾或者程序会崩溃(或者,最糟糕的是,不会崩溃,但用废话填满它的记忆)。

所以答案很简单:合同的存在是有原因的,如果你想把东西放进容器里,你需要确保它是一个容器,而且它不是谎称是一个容器– 它需要正确地实际实现容器的行为,因为(几乎可以肯定)容器将依赖于该行为来工作。这就是合同的用途。

For example, let's say that I want to put a button in a menu instead of a menu item.

既然如此,答案就是你的追求不是一条有成果的大道。

菜单对象背后的想法是保存将被发送到要在屏幕上绘制的基础 GUI 的“内容”。各种 GUIS(macOS、Linux、Windows)不允许在菜单中绘制任意元素,因此 Racket 中的 GUI 层必须检查菜单是否只包含菜单项这就说得通了。因此,假设您设法绕过合同检查器并将一个按钮作为菜单项传递。最终该菜单项将从 Racket GUI 层传递到 OS,然后您将 运行 陷入错误 - 很可能是崩溃(程序因核心转储而停止)。

换句话说:合同已经到位,以确保菜单项是放置在菜单中有意义的东西。它可以防止您意外地将错误类型的对象存储在菜单中。