3
votes

In typed/racket I have a case like [(? procedure? p ) (apply p xv*)] It will cause error:

Type Checker: Function has no cases in: (apply p xv*)

So I write a test case to detect the reason:

#lang typed/racket

(: test-match-apply-0 (-> (-> Any * Any) (Listof Any) Any))
(define test-match-apply-0
  (lambda (x args)
    (match x
      [(? procedure? p) (apply p args)])))
;; Type Checker: Function has no cases in: (apply p args)

(test-match-apply-0 + (list 1 2 3))  ;; not ok

(apply + (list 2 4))  ;; ok



(: test-match-apply-1 (-> (-> (Listof Any) Any) (Listof Any) Any))
(define test-match-apply-1
  (lambda (x args)
    (match x
      [(? procedure? p) (apply p args)])))

(test-match-apply-1 + (list 1 2 3))  ;; not ok


;; For int is it right


(: test-match-apply-2 (-> (-> (Listof Any) Any) (Listof Number) Number))
(define test-match-apply-2
  (lambda (x args)
    (match x
      [(? procedure? p) (apply p args)])))

(test-match-apply-2 + (list 1 2 3))  ;; not ok



(: test-match-apply-3 (-> (-> Number * Number) (Listof Number) Number))
(define test-match-apply-3
  (lambda (x args)
    (match x
      [(? procedure? p) (apply p args)])))

(test-match-apply-3 + (list 1 2 3))  ;; it is ok 

I print the + itself:

> (:print-type +)
(case->
 (-> Zero)
 (-> Number Number)
 (-> Zero Zero Zero)
 (-> Number Zero Number)
 (-> Zero Number Number)
 (-> Positive-Byte Positive-Byte Positive-Index)
 (-> Byte Byte Index)
 (-> Positive-Byte Positive-Byte Positive-Byte Positive-Index)
 (-> Byte Byte Byte Index)
 (-> Positive-Index Index Positive-Fixnum)
 (-> Index Positive-Index Positive-Fixnum)
 (-> Positive-Index Index Index Positive-Fixnum)
 (-> Index Positive-Index Index Positive-Fixnum)
 (-> Index Index Positive-Index Positive-Fixnum)
 (->* (Index Index) (Index) Nonnegative-Fixnum)
 .....

Come back to my origin needs, How can I make it [(? procedure? p ) (apply p xv*)] possible in typed/racket? Because in the case I can't detect p 's type. Something like type-apply?

1

1 Answers

4
votes

The reason Typed Racket can’t apply that procedure is because it knows nothing about it aside from the fact that it is a procedure. It might not take any arguments, for example, in which case that apply would cause a runtime error. It might take a different kind of argument, or it might even have required keyword arguments. TR doesn’t know any of this just from the procedure? predicate succeeding, so it doesn’t allow you to invoke such a value.

This is tricky, because there is no predicate that will allow you to inspect enough details about the function that will make it safe to apply. You basically have two options:

  1. Constrain the type of the input so that procedure? will restrict it to a specific function type. You can do this by making the input a union of specific types. For example, this typechecks:

    (: constrained ((U String Number (String * -> String)) -> String))
    (define (constrained x)
      (match x
        [(? string?) x]
        [(? number?) (number->string x)]
        [(? procedure?) (apply x '("a" "b" "c"))]))
    

    Even though the type is a union type here, since there is only one possible case for which the procedure? predicate is true, TR can restrict the type to a properly applicable value.

    The type of the function itself can get pretty fancy, and TR can still figure it out. For example, it still works with a polymorphic type:

     (: poly-constrained (All [a] (U String Number (a * -> String)) (Listof a) -> String))
     (define (poly-constrained x lst)
       (match x
         [(? string?) x]
         [(? number?) (number->string x)]
         [(? procedure?) (apply x lst)]))
    
  2. Alternatively, you can use cast. This will allow you to tell TR to perform a dynamic check that a value matches a particular type.

    (: unconstrained (Any -> String))
    (define (unconstrained x)
      (match x
        [(? string?) x]
        [(? number?) (number->string x)]
        [(? procedure?) (apply (cast x (String * -> String)) '("a" "b" "c"))]))
    

    However, note that this is a little big dangerous! There are a couple pitfalls to using cast:

    • The check generates a typed/untyped boundary for a single value, effectively the same sort of boundary between typed and untyped modules. This means that cast generates a contract, which is checked at runtime, which, unlike static types, takes time and can reduce performance significantly if used in a tight loop.

    • Since cast performs the check dynamically, you lose one of the main benefits of Typed Racket: static type safety. If, for example, someone provides a procedure that does not match the given type, a runtime error will occur, which is precisely the sort of thing Typed Racket is designed to prevent.

If possible, you probably want to use the first approach so that you don’t compromise type safety, but in cases where predicates are not good enough, you can use cast. Just be aware of the downsides before you choose it.