Question about unconstrained-domain-> in the Racket guide

Hey! I am currently working my way through the Racket guide and I am struggling a bit to grok unconstrained-domain-> in the example 7.3 Contracts on Functions in General.

I'll paste the example here and work through my understanding thus far and hopefully someone can help fill in the gaps.

(define (n-step proc inits)
  (let ([inc (apply proc inits)])
    (when inc
      (n-step proc (map (λ (x) (+ x inc)) inits)))))

(provide (contract-out [n-step
                        (->i ([proc
                               (inits)
                               (and/c (unconstrained-domain-> (or/c #f number?))
                                      (λ (f) (procedure-arity-includes? f (length inits))))]
                              [inits (listof number?)])
                             ()
                             any)]))

I want to export n-step with this contract. We use the ->i to handle optional parameters, we have none so this could be ->? Or, do I need ->i to enable this syntactic form? proc is the first parameter of the function and I don't quite understand the (inits) syntax that follows it. Is this stating that proc takes a single argument inits? It can't (right?) otherwise we wouldn't need to check the arity matches the length of inits. Everything beyond that makes sense.

I took this example and attempted to write a contract with unconstrained-domain-> for the following stupid example,

(define (increment x)
  (+ x 1))

(provide (contract-out [increment
                        (->i ([x (and/c (unconstrained-domain-> number?) (λ (y) (> y 2)))]) () any)]))

i.e. increment will only work for numbers greater than 2. I understand this is silly, but I wanted to test my understanding. I guess ideally it would also check that y is a number?. The error I get from this is,

> (increment 1)
increment: contract violation
  expected: a procedure
  given: 1
  in: the 1st conjunct of
      the x argument of
      (->i
       ((x
         (and/c
          (unconstrained-domain-> number?)
          ...-guide/chapter7.rkt:237:73)))
       any)
  contract from: 
      /home/chiroptical/programming/racket/the-racket-guide/chapter7.rkt
  blaming: top-level
   (assuming the contract is correct)
  at: /home/chiroptical/programming/racket/the-racket-guide/chapter7.rkt:236:24
 [,bt for context]

It is entirely possible my syntax is just wrong.

I guess it can! The point is we are passing a list of arguments to the function proc and applying them.

->i is a dependent contract, meaning it can describe a dependency among the arguments to a function and its result(s).

In this example, the contract for the first arguments proc depends on the (length of the) second argument inits.
For historical reasons, the ->i form demands that a programmer must specify this dependency explicitly.

By contrast, the second argument does not depend on the value of anything else, so there’s no such “dependency clause.”

The and/c contract for proc states two constraints:

— (1) it is a procedure that can take any kind of value and any number
— (2) it is a procedure that must be able to take as many arguments as inits contains elements.

So the second part of the and/c refines the first one.

;; - - -

define (increment x) (+ x 1))

This function must consume a number, because its body says it increments the number by 1.

But, your contract says that its one and only argument must be a procedure and that this procedure
should be greater than 2 — which of course will always fail because a procredure can’t be compared to
a number.

The contract that you describe in English would look like this:

(provide
(contract-out
(increment
;; increments numbers greater than 2 by 1
(-> (lambda (y) (> y 2)) any/c)))

(This is my preferred format of specifying exports.)

1 Like

The complicated parts of this example have more to do with ->i than with unconstrained-domain->.

While ->i is capable of handling optional arguments, the reason to use the ->i combinator (instead of ->* or in this case, as you say, ->) is because you want one of the sub-contracts to depend on the actual value of one of the arguments when the function is called. This is introduced in § 7.3.6 Argument and Result Dependencies.

For comparison, let's consider a non-dependent contract that you might write for n-step:

(provide (contract-out
          [n-step
           (-> (unconstrained-domain-> (or/c #f number?))
               (listof number?)
               any)]))

This contract says that n-step takes two arguments:

  1. A procedure that returns (or/c #f number?); and
  2. A list of numbers.

In particular, using unconstrained-domain-> means the contract doesn't say anything about what kinds of arguments the procedure passed to n-step takes.

That's all correct! But there is an additional requirement you might want to check: that the procedure argument actually accepts the number of arguments that are given in the (listof number?) argument.

Let's take a closer look at ->i, with the sub-expressions replaced by emoji so we can focus on the syntax of ->i itself.

(->i ([proc
       (inits)
       🍎]
      [inits 🍏])
     ()
     any)

Both the [proc (inits) 🍎] clause and the [inits 🍏] clause are what the full documentation for ->i calls mandatory-dependent-dom[ain] clauses, which means they are about required arguments to n-step. In [proc (inits) 🍎], the (inits) part means that the :red_apple: expression depends on the actual value of the inits argument at runtime. When n-step is called, the contract will first check that the second argument satisfies the contract produced by the :green_apple: expression. Then, the contract will evaluate the :red_apple: expression with inits bound to the actual value of the second argument, after checking. So, in the actual :red_apple: expression:

(and/c (unconstrained-domain-> (or/c #f number?))
       (procedure-arity-includes/c (length inits)))

the reference to inits refers to the binding established by the (inits) part of [proc (inits) 🍎].

1 Like

This is so immensely helpful. Thank you!

Can the contract for increment be written with unconstrained-domain-> even if it is weird? (going to try a bit more this morning with my new information)

Ahh, okay, I am reading 8.2 Function Contracts and I see my confusion. It is designed to work for functions.

It was pointed out here,

but I missed the connection.

Thank you both! I understand this a lot better now.

I guess this is illustrative? The f takes two arguments and outputs a number. The output of increase must be greater than the sum of x and y.

(define (increase f x y)
  (f x y))

(provide (contract-out
          [increase
           (->i ([f (and/c (unconstrained-domain-> number?) (λ (f) (procedure-arity-includes? f 2)))]
                 [x number?]
                 [y number?])
                ()
                (lambda (x y) (>/c (+ x y))))]))

(increase (lambda (x y) (* 2 x y)) 1 2) ; would work

(increase (lambda (x y) (* x y)) 1 2) ; wouldn't work

Obviously, you could write f more directly because if x and y are number? than f must be (-> number? number? number?) but I'm learning.

Comparing (-> number? number? number?) and

(and/c (unconstrained-domain-> number?)
       (procedure-arity-includes/c 2))

both contracts check that:

  • The procedure must produce a single result, which must satisfy number?; and that
  • The procedure must accept two arguments.

However, the key difference is that second contract does not check that the arguments to the function satisfy number?.

The same thing is true of the contract in the Guide for the proc argument to n-step. Here's an alternate contract for the proc argument to n-step that does check the arguments:

(->i ([proc (inits)
            (dynamic->* #:mandatory-domain-contracts (make-list (length inits) number?)
                        #:range-contracts (list (or/c #f number?)))]
      [inits (listof number?)])
     any)

The main message of § 7.3.9 Fixed but Statically Unknown Arities is that the techniques from § 7.3.2 Rest Arguments don't solve the problem, because those contracts say "that the function must accept any number of arguments, not a specific but undetermined number".

The suggestion of unconstrained-domain-> (it seems a bit misleading that the Guide calls it "the correct contract") is one tool that can solve the problem; dynamic->* is another. You could write a dependent contract or leave the arity unchecked: how precise to make a contract is always a pragmatic decision.


It might be helpful to know that the name unconstrained-domain-> comes from the terminology of mathematical functions, which use "domain" and "range" for what programmers more often call a functions "arguments" and "results".