Effects and contracts¶
This example shows what @ppy.pure refuses, and how a value from a dynamic
boundary gets back into typed code.
Run it¶
Effects are a set¶
Every function carries an inferred effect set. I/O, global writes,
randomness, time, and mutation of arguments are each tracked on their own.
A @ppy.pure decorator is a claim against that set, and the checker either
proves it or names the effect that breaks it.
@ppy.pure
def clamp(x: Annotated[int, ppy.Range(0, 255)]) -> Annotated[int, ppy.Range(0, 255)]:
if x < 0:
return 0
if x > 255:
return 255
return x
ppy.Range(0, 255) is a refinement the checker propagates. Inside clamp
the arithmetic on x needs no overflow guard, and a caller passing 300 is
told so at check time.
The other functions in the file:
scalebuilds a new list and is pure. Local allocation is fine; mutating an argument is not.mixis float arithmetic onf32, pure by construction.log_and_totalprints, so it carriesio. With@ppy.pureon it the checker answersE1601with the effect named.
The rule is interprocedural. A pure function that calls something with
unknown effects is E1602, and a callee that mutates the caller's argument
charges the write to the caller.
A boundary for the dynamic part¶
@ppy.dynamic
def evaluate(source: str) -> int:
with ppy.dynamic:
return ppy.check[int](eval(source))
eval is rejected in strict code (E1501). Inside a ppy.dynamic boundary
it is allowed, but what comes out is Dynamic, and Dynamic does not fit a
declared -> int.
ppy.check[int] validates the value at run time and hands back a typed one.
If the value is not an int, it raises TypeError. The boundary suspends
the dynamic-feature rules; it does not suspend the types.
What it prints¶
python effects.ppy, ppy effects.ppy, ppy run effects.ppy
Read on: Effects and purity · The subset · Dynamic boundaries
effects.ppy is hand-written; there is no .py source and no conversion step.
03_effects_and_contracts/effects.ppy¶
from typing import Annotated
import ppy
from ppy import f32
@ppy.pure
def scale(values: list[float], factor: float) -> list[float]:
return [value * factor for value in values]
@ppy.pure
def clamp(x: Annotated[int, ppy.Range(0, 255)]) -> Annotated[int, ppy.Range(0, 255)]:
if x < 0:
return 0
if x > 255:
return 255
return x
@ppy.pure
def mix(a: f32, b: f32) -> f32:
return a * 0.75 + b * 0.25
def log_and_total(values: list[int]) -> int:
total: int = 0
for value in values:
print(value)
total += value
return total
@ppy.dynamic
def evaluate(source: str) -> int:
with ppy.dynamic:
return ppy.check[int](eval(source))
def main() -> None:
print(scale([1.0, 2.0], 2.0), clamp(300), mix(1.0, 3.0))
print(log_and_total([1, 2]))
print(evaluate("6 * 7"))
if __name__ == "__main__":
main()
Source: examples/03_effects_and_contracts.