在Typed / Racket中键入函数类型的谓词

时间:2014-12-31 22:22:18

标签: racket typed-racket

我正处于设计框架的早期阶段并且正在使用typed/racket进行愚弄。假设我有以下类型:

   (define-type Calculate-with-one-number (-> Number Number))
   (define-type Calculate-with-two-numbers (-> Number Number Number))

我希望有一个派遣类型的函数:

(: dispatcher (-> (U Calculate-with-one-number Calculate-with-two-numbers) Number))

(define (dispatcher f args)
   (cond [(Calculate-with-one-number? f)
          (do-something args)]
         [(Calculate-with-two-numbers? f)
          (do-something-else args)]
         [else 42]))

如何在Calculate-with-one-number?中创建类型谓词Calculate-with-two-numbers?Typed/Racket?对于非函数谓词,我可以使用define-predicate。但目前尚不清楚如何实现函数类型的谓词。

2 个答案:

答案 0 :(得分:10)

由于我自我回答,我正在冒昧地根据对作为解决方案的讨论来澄清我的问题的要点。 arity的差异是由于我在指定问题时没有考虑其影响。

问题

在许多Lisps,函数或更正确的#lang typed/racket #lang racket中,procedures是一流的数据表。

默认情况下,#lang typed/racketarity键入过程,参数类型中的任何其他特殊性必须通过合同完成。在 (define-type NN (-> Number Number)) 过程中,由于语言的“烘焙合同”,由arity及其参数类型和返回值输入过程。

以数学为例

Typed Racket Guide使用example提供define-type来定义过程类型:

 ;; Takes two numbers, returns a number
 (define-type 2NN (-> Number Number Number))

 (: trigFunction1 2NN)
 (define (trigFunction1 x s)
   (* s (cos x)))

 (: quadraticFunction1 2NN)
 (define (quadraticFunction1 x b)
   (let ((x1 x))
     (+ b (* x1 x1))))

这允许更简洁地指定程序:

cos

目标

在像数学这样的领域,使用更抽象的过程类型会更好,因为知道函数在上限和下限之间是循环的(如 (define-type Cyclic2NN (-> Number Number Number)) (define-type SingleBound2NN (-> Number Number Number)) (: trigFunction1 Cyclic2NN) (define (trigFunction1 x s) (* s (cos x))) (: quadraticFunction1 SingleBound2NN) (define (quadraticFunction1 x b) (let ((x1 x)) (+ b (* x1 x1)))) (: playTone (-> Cyclic2NN)) (define (playTone waveform) ...) (: rabbitsOnFarmGraph (-> SingleBound2NN) (define (rabbitsOnFarmGraph populationSize) ...) )而只有一个约束(例如我们的二次函数) )渐近(例如双曲函数)提供了关于问题域的更清晰的推理。很容易获得类似的抽象:

define-type

唉,Cyclic2NN?在程序方面没有提供这种级别的粒度。此外,我们可能很容易使用define-predicate手动对程序进行类型区分的错误希望破灭:

  

使用类型(Any - > Boolean:t)计算类型t的谓词。 t可能不包含可能引用可变数据的函数类型或类型,例如(Vectorof Integer)。

从根本上说,类型具有静态检查和合同之外的用途。作为该语言的一等成员,我们希望能够调度我们更精细的程序类型。从概念上讲,所需要的是SingleBound2NN?typed/racket的谓词。仅使用case-lambda进行调度是不够的。

Untyped Racket的指导

幸运的是,Lisps是用于编写Lisps的领域特定语言,一旦我们重新开启以展示向导,最终我们可以得到我们想要的东西。关键是以另一种方式处理问题并询问“如何使用谓词#lang racket为我们提供程序?”

结构是Racket的用户定义数据类型,是扩展其类型系统的基础。结构非常强大,即使在基于类的对象系统中,“classes and objects are implemented in terms of structure types。”

#:property个结构中,可以应用作为prop:procedure关键字使用> ;; #lang racket > (struct annotated-proc (base note) #:property prop:procedure (struct-field-index base)) > (define plus1 (annotated-proc (lambda (x) (+ x 1)) "adds 1 to its argument")) > (procedure? plus1) #t > (annotated-proc? plus1) #t > (plus1 10) 11 > (annotated-proc-note plus1) "adds 1 to its argument" 后跟其值的过程的过程。该文档提供了两个示例:

first example指定要作为过程应用的结构的字段。显然,至少有一次指出,该字段必须包含一个评估为过程的值。

> ;; #lang racket
> (struct greeter (name)
    #:property prop:procedure
    (lambda (self other)
       (string-append
         "Hi " other
          ", I'm " (greeter-name self))))
> (define joe-greet (greeter "Joe"))
> (greeter-name joe-greet)
"Joe"
> (joe-greet "Mary")
"Hi Mary, I'm Joe"
> (joe-greet "John")
"Hi John, I'm Joe

second example中,匿名过程[lambda]直接作为属性值的一部分提供。 lambda在第一个位置获取一个操作数,该操作数被解析为用作过程的结构的值。这允许访问存储在结构的任何字段中的任何值,包括那些评估为过程的字段。

typed/racket

将其应用于打字/球拍

唉,这两种语法都不适用typed/racket中实现的struct。似乎问题是当前实现的静态类型检查器不能同时定义结构并将其签名解析为过程。使用struct的{​​{1}}特殊表单时,正确的信息似乎无法提供。

要解决此问题,typed/racket提供的define-struct/exec大致对应于#lang racket中的第二个句法形式,而不是关键字参数和属性定义:

    (define-struct/exec name-spec ([f : t] ...) [e : proc-t])

      name-spec     =       name
                    |       (name parent)
  

与define-struct类似,但定义了一个过程结构。 procdure e用作prop:procedure的值,并且必须具有proc-t类型。

它不仅为我们提供了强类型的过程形式,而且比#lang racket中的关键字语法更优雅。解决此问题的示例代码在此答案中重申:

#lang typed/racket

(define-type 2NN (-> Number Number Number))

(define-struct/exec Cyclic2NN
   ((f : 2NN))
   ((lambda(self x s)
     ((Cyclic2NN-f self) x s))
      : (-> Cyclic2NN Number Number Number)))

 (define-struct/exec SingleBound2NN
   ((f : 2NN))
   ((lambda(self x s)
     ((SingleBound2NN-f self) x s))
       : (-> SingleBound2NN Number Number Number)))

 (define trigFunction1 
   (Cyclic2NN 
    (lambda(x s) 
      (* s (cos x)))))

(define quadraticFunction1
  (SingleBound2NN
    (lambda (x b)
      (let ((x1 x))
        (+ b (* x1 x1)))))

定义的程序在以下意义上是强类型的:

> (SingleBound2NN? trigFunction1)
- : Boolean
#f
>  (SingleBound2NN? quadraticFunction1)
- : Boolean
#t

剩下的就是写一个宏来简化规范。

答案 1 :(得分:6)

一般的情况下,由于在Racket中实现类型的方式,您想要的是不可能的。 Racket有契约,它们是运行时包装器,用于保护程序的其他部分。函数契约是一个将函数视为黑盒子的包装器 - (-> number? number?)形式的契约可以包装任何函数,新的包装器函数首先检查它是否收到一个number?,然后将其传递给包装函数,然后检查包装函数返回number?。每次调用该函数时,这都是动态完成的。 Typed Racket添加了静态检查类型的概念,但由于它可以提供和要求非类型化模块的值,因此这些值受到代表其类型的合同的保护。

在您的函数dispatcher中,您在运行时动态接受函数f ,然后根据具体情况执行某些操作你得到的功能。但是函数是黑盒子 - 合同实际上并不知道关于它们包装的函数的任何内容,它们只是检查它们的行为是否正常。无法判断dispatcher是否具有(-> number? number?)形式的函数或(-> string? string?)形式的函数。由于dispatcher可以接受任何可能的功能,因此这些功能是黑盒子,没有关于他们接受或承诺的信息。 dispatcher只能假定合同中的函数是正确的并尝试使用它。这也是define-type不为函数类型自动生成谓词的原因 - 没有办法证明函数具有动态类型,你只能将它包装在契约中并假设它的行为。

这个例外是arity信息 - 所有函数都知道它们接受了多少个参数。 procedure-arity函数会为您提供此信息。因此,虽然您无法在 general 中的运行时调度函数类型,但您可以 在函数 arity 上调度。这就是case-lambda所做的事情 - 它根据收到的参数数量生成一个调度函数:

(: dispatcher (case-> [-> Calculate-with-one-number Number Void]
                      [-> Calculate-with-two-numbers Number Number Void]))

(define dispatcher
  (case-lambda
    [([f : Calculate-with-one-number]
      [arg : Number])
     (do-something arg)]
    [([f : Calculate-with-two-numbers]
      [arg1 : Number]
      [arg2 : Number])
     (do-something-else arg1 arg2)]
    [else 42]))