Function contracts for multiple return values

I would like to specify the following (which doesn't work) for the return values of a function:

(or/c (values t1 t2)
      (values t3 t4))

In other words, I'd like to verify that either the function returns two values of types t1 & t2, respectively, or the function returns two values of types t3 & t4, respectively, but not, for example, two values of types t1 & t4.

or/c is expecting a single value. Any thoughts about how to accomplish this?

2 Likes

From Sorawee on Slack #general, using the dependent range feature of ->i.

#lang racket

(define f/c
  (->i ([_ number?]) (values [x (or/c 1 2)]
                             [y (x) (if (= x 1) 11 12)])))

(define/contract (f x) f/c
  (values 2 12))

(define/contract (g x) f/c
  (values 1 11))

(define/contract (h x) f/c
  (values 1 12))

(f 1) ; OK
(g 1) ; OK
(h 1) ; contract violation
3 Likes

It appears Sorawee's solution works perfectly. The contract is for a class method; hence, the entry for this. I'm very impressed with Racket's contract system!

[ get-user-data
  (->i ([ _             any/c       ] ; this
        [ conn          connection? ]
        [ reference-key string?     ]
        [ type          string?     ])
       (values [ hsh (or/c hash? #f) ]
               [ errors (hsh) (if (hash? hsh)
                                  '()
                                  (listof string?)) ])) ]

P.S. It looks like someone may have marked the previous reply solved prematurely, maybe an admin/moderator? May I humbly suggest we wait a period of time to allow the OP to mark a solution before doing so on their behalf? I was busy yesterday evening, so I was only able to test the solution today.

1 Like

Apologies, that was me. I will be more circumspect in the future!

John

Looks like this question came up again, in slightly different form, on racket-mode's github: "result arity mismatch" when using a contract with or/c and values · Issue #659 · greghendershott/racket-mode · GitHub

Is it documented anywhere that values can be used at all in contracts?

I took a quick look at the issue and yes, perhaps this was a bad choice on my part in the design of the syntax of the range of function contracts! The use of values there is purely syntactic; it is really a kind of "keyword" that's attached to -> and friends. This was done in the days before keywords, I belive (or maybe before I realized I should have used them). If it had been something like:

(-> integer? #:values boolean? boolean?)

instead of

(-> integer? (values boolean? boolean?))

maybe it would have been a lot cleaner. Or maybe even #:results or something to indicate where arguments end and results begin.

But to answer your question, here are the docs for -> and values appears in the blue box there.

Yikes - when I incorrectly opened that racket-mode github issue, I had completely forgotten about this discourse thread I started, and the fact that I have an example of what I wanted to do in another project - embarrassing!

I only saw it because I came here to ask my question about using values in contracts (occasioned by your recent issue) and it popped up on the "similar topics" list on the right while I was typing.

Further curiosity: when you want to return two values that are guaranteed to be either string and integer or #f and #f, is there a particular reason you want values rather than, say, cons/c?

Efficiency i.e. I don't want to allocate a cons cell.

I wonder how 'values' does it?

-- hendrik

Here's a paper by Ashley and Dybvig

Ok, I see. I hadn’t thought of that! But that kind of optimization in this particular context is still a head-scratcher for me. Clearly you want to make some guarantees about the return value here, or you wouldn’t be using contracts. But the contracts themselves introduce a performance penalty that I would assume is far higher than that of the allocation of a single cons cell. If you’re already willing to incur that penalty, why not go a little further and do it in a way that actually makes the guarantee you want to make?

(No shade here; again I’m just curious. I’ve already learned something by asking one dumb question so I figure I’ll just keep going. I’ve never had to deal with the kind of problems where avoiding allocating cons cells made a difference.)

Yeah, the overhead of ->i is pretty significant. The following program shows that the contracted values variant could be noticeably slower than the cons variant.

#lang racket

(module test racket
  (define f1/c
    (->i ([_ any/c])
         (values [x (or/c string? #f)]
                 [y (x) (if (string? x) string? #f)])))

  (define f2/c
    (-> any/c (or/c (cons/c string? string?) (cons/c #f #f))))

  (provide (contract-out
            [foo1 f1/c]
            [foo2 f2/c]))

  (define (foo1 b)
    (if b
        (values "answer" "x")
        (values #f #f)))

  (define (foo2 b)
    (if b
        (cons "answer" "x")
        (cons #f #f))))

(require 'test)

(define N 1000000)

(time
 (for ([i N])
   (foo1 #t)
   (foo1 #f)))

(time
 (for ([i N])
   (foo2 #t)
   (foo2 #f)))

produces:

cpu time: 882 real time: 887 gc time: 125
cpu time: 642 real time: 628 gc time: 114

But if these functions are also used within the module boundary a lot, avoiding the allocations could be very worthwhile

#lang racket

(module test racket
  (define f1/c
    (->i ([_ any/c])
         (values [x (or/c string? #f)]
                 [y (x) (if (string? x) string? #f)])))

  (define f2/c
    (-> any/c (or/c (cons/c string? string?) (cons/c #f #f))))

  (provide (contract-out
            [foo1 f1/c]
            [foo2 f2/c]))

  (define (foo1 b)
    (cond
      [(= 0 b) (values "answer" "x")]
      [(= 1 b) (values #f #f)]
      [else
       (define-values (v1 v2) (foo1 (sub1 b)))
       (values v2 v1)]))

  (define (foo2 b)
    (cond
      [(= 0 b) (cons "answer" "x")]
      [(= 1 b) (cons #f #f)]
      [else
       (define p (foo2 (sub1 b)))
       (cons (cdr p) (car p))])))

(require 'test)

(define N 100000)
(define S 100)

(time
 (for ([i N])
   (foo1 S)
   (foo1 (add1 S))))

(time
 (for ([i N])
   (foo2 S)
   (foo2 (add1 S))))

produces:

cpu time: 1279 real time: 1197 gc time: 406
cpu time: 2044 real time: 1991 gc time: 660

You can get an even more performant version by using multiple values within the module boundary, and switch to cons across module boundaries. But that seems cumbersome and not worthwhile doing, unless this part of the program is really performance-sensitive.

1 Like

I design my functions independently of contracts. Contracts are optional, and in some cases, I'll turn them off, for performance, or other, reasons. They're more of a benefit to me during development and testing, than in production.

So, when I consider the performance of a function, I do so w/o regard to contracts i.e. "what's the right thing for this function to do if there was no contract".

2 Likes