Pure functions and promises
(require delay-pure) | package: delay-pure |
syntax
(delay/pure/stateless expression)
syntax
(delay/pure/stateful expression)
procedure
v : any/c
procedure
v : any/c
syntax
(pure/stateless expression)
syntax
(pure/stateful expression)
Note that the expressions can refer to variables mutated with set! by other code. Placing the expression in a lambda function and calling that function twice may therefore yield different results, if other code mutates some free variables between the two invocations. In order to produce a pure thunk which caches its inputs (thereby shielding them from any mutation of the external environment), use pure-thunk/stateless and pure-thunk/stateful instead.
The first form, pure/stateless, checks that once fully-expanded, the expression does not contain uses of set!. Since the free variables can never refer to stateful functions, this means that any function present in the result is guaranteed be a stateless function. The results of two calls to a stateless function with the same arguments should be indistinguishable, aside from the fact that they might not be eq?. In other words, a stateless function will always return the “same” (not necessarily eq?) value given the same (eq?) arguments. If the result contains functions, these functions are guaranteed to be stateless too.
With the second form pure/stateful, uses of set! are allowed within the expression (but may not alter free variables). The resulting value will be an immutable value which may contain both stateless and stateful functions. Stateful functions may be closures over a value which is mutated using set!, and therefore calling a stateful function twice with the same (eq?) arguments may produce different results. Since Typed/Racket does not use occurrence typing on function calls, the guarantee that the result is immutable until a function value is reached is enough to safely build non-caching promises that return the “same” value, as far as occurrence typing is concerned.
Promises created with delay/pure/stateless and delay/pure/stateful re-compute their result each time, which yields results that are not necessarily eq?. This means that calling eq? twice on the same pair of expressions may not produce the same result. Fortunately, occurrence typing in Typed/Racket does not rely on this assumption, and does not "cache" the result of calls to eq?. If this behaviour were to change, this library would become unsound.
TODO: add a test in the test suite which checks that Typed/Racket does not "cache" the result of eq? calls, neither at the type level, nor at the value level.
syntax
(pure-thunk/stateless thunk)
(pure-thunk/stateless thunk #:check-result)
syntax
(pure-thunk/stateful thunk)
(pure-thunk/stateful thunk #:check-result)
syntax
(define-pure/stateless (name . args) maybe-result body ...)
(define-pure/stateless (: name . type) (define (name . args) maybe-result body ...))
syntax
(define-pure/stateful (name . args) maybe-result body ...)
(define-pure/stateful (: name . type) (define (name . args) maybe-result body ...))
maybe-result =
| : result-type
Due to the way the function is defined, a regular separate type annotation of the form (: name type) would not work (the function is first defined using a temporary variable, and name is merely a rename transformer for that temporary variable).
It is therefore possible to express such a type annotation by placing both the type annotation and the definition within a define-pure/stateless or define-pure/stateful form:
(define-pure/stateless (: square : (→ Number Number)) (define (square x) (* x x)))
The define identifier can either be define from typed/racket or define from type-expander.
For now only a few built-in functions are recognized as pure:
Patches adding new functions to the set are welcome.
for-syntax value
built-in-pure-functions-free-id-set : immutable-free-id-set?
procedure
((immutable/stateless/c varref) v) → Boolean
varref : Variable-Reference v : Any
This predicate detects whether the functions contained within the value v are pure or not, based on the built-in-pure-functions-set set and a few special cases:
The low-level functions used to build pure promises are always accepted. Their valid use is guaranteed by the macros wrapping them.
Predicates for struct types are always accepted
Field accessors for struct types are always accepted
Constructors for struct types are accepted only if immutable/stateless/c can determine that the struct type is immutable.
There seems to be no combination of built-in functions in Racket which would reliably associate a struct constructor (as a value) with its corresponding struct type. Instead, immutable/stateless/c uses a heuristic based on object-name: if struct-constructor-procedure? returns #true for a function, and that function’s object-name is st or make-st, then st is expected to be an identifier with static struct type information.
To achieve this, it is necessary to access the call-site’s namespace, which is done via the varref parameter. Simply supplying the result of (#%variable-reference) should be enough.
procedure
((immutable/stateful/c varref) v) → Boolean
varref : Variable-Reference v : Any
This predicate needs to access the call-site’s namespace, which is done via the varref parameter. Simply supplying the result of (#%variable-reference) should be enough.
See the documentation for immutable/stateless/c for an explanation of the reason for this need.
syntax
(unsafe-pure/stateless expression)
The unsafe-pure/stateless form can be used within pure/stateless, pure/stateful and their derivatives, to prevent any check on a portion of code.
The expression should be a pure, stateless expression.
Note that in the current implementation, the expression is lifted (in the sense of syntax-local-lift-expression.
syntax
(unsafe-operation/mutating expression)
The expression should not vary its outputs and effects based on external state (i.e. its outputs and effects should depend only on the arguments passed to it).
The expression function may internally use mutation. It may return freshly-created stateful objects (closures over freshly-created mutable variables, closures over mutable arguments, and mutable data structure which are freshly created or extracted from the arguments). It may mutate any mutable data structure passed as an argument.
Note that in the current implementation, the expression is lifted (in the sense of syntax-local-lift-expression.
syntax
(unsafe-declare-pure/stateless identifier)
Note that this has a global effect. For one-off exceptions, especially when it’s not 100% clear whether the function is always pure and stateless, prefer unsafe-pure/stateless.
syntax
(unsafe-declare-allowed-in-pure/stateful identifier)
The identifier function should not vary its outputs and effects based on external state (i.e. its outputs and effects should depend only on the arguments passed to it).
The identifier function may internally use mutation. It may return freshly-created stateful objects (closures over freshly-created mutable variables, closures over mutable arguments, and mutable data structure which are freshly created or extracted from the arguments). It may mutate any mutable data structure passed as an argument.
Note that this has a global effect. For one-off exceptions, prefer unsafe-operation/mutating.