!import "prelude" !Local !import "view" !Local -- ============================================================================ -- View Contracts in tricu -- ============================================================================ -- -- Verify this guide passes checking with: -- -- tricu check demos/viewContracts.tri -- -- Expected output: -- -- ok -- -- This file uses tricu syntax sugar. The lower-level portable View Tree -- form is shown in demos/viewContracts/complete.tri. -- ============================================================================ -- 1. What's the problem? -- ============================================================================ -- -- Programs grow by connecting definitions. A common mistake is connecting a -- value with one shape to code that expects another shape: -- -- a function expects Bool, but receives String -- a function returns String, but its caller expects Bool -- a list is expected to contain bytes, but contains strings -- -- In a large program, those mistakes are often far away from where the bad value -- was first introduced. View Contracts give tricu a portable way to check those -- boundaries. -- ============================================================================ -- 2. Views: useful built-in shapes -- ============================================================================ -- -- A View is a description of the shape we expect at a boundary. tricu includes -- built-in Views for common shapes such as: -- -- Bool -- String -- Byte -- Unit -- List View -- Maybe View -- Pair View1 View2 -- Fn [View1] View2 -- -- tricu has unconventional but intuitive sugar for annotations: -- -- name =@View value -- function argument@View =@ResultView body -- -- These examples are ordinary checked source definitions. message =@String "hello" names =@(List String) [("Ada") ("Grace")] chooseFirst left@String right@String =@String left stringIdentity =@(Fn [String] String) (x : x) -- Uncommenting the below definition demonstrates a plain View mismatch: -- -- bad =@Bool "not a Bool" -- -- `tricu check` reports that the value is known as String where Bool was -- required. -- ============================================================================ -- 3. Why don't you just have Types? -- ============================================================================ -- -- tricu is built on Tree Calculus. A defining feature of Tree Calculus is -- intensionality: programs can inspect and construct program-shaped trees directly. -- That intensional power is useful, but it makes ordinary sound static typing a -- hard fit. A value can be both data and executable structure, and code can make -- decisions based on tree shape in ways a conventional type checker may not be -- able to predict soundly. This is an area of active research, not a settled -- claim that Tree Calculus languages cannot ever have useful typed variants. -- -- View Contracts are not advertised as "the type system for tricu". They are -- a practical contract layer: portable metadata plus checker/runtime boundaries -- that catch many real mistakes while leaving the underlying language intact. -- For more information about sound typing for Tree Calculus: -- https://github.com/barry-jay-personal/typed_tree_calculus -- ============================================================================ -- 4. What are the Contracts about, then? -- ============================================================================ -- -- `List String` tells us that every element is a String. It does not tell us the -- list has at least one element. -- -- That matters for functions like `head`. Calling `head` on an empty list is a -- bug. We want to express the stronger requirement: -- -- this is a List String, and it is non-empty -- -- That is what a guarded View is for. -- A guard is ordinary tricu code. It receives the runtime value and returns: -- -- guardOk value -- accept the value -- guardFail -- reject the boundary -- -- The guard does not write diagnostics. The checked runner reports where the -- failing boundary came from. requireNonEmpty = (xs : lazyBool (_ : guardFail) (_ : guardOk xs) (emptyList? xs)) -- A user-defined View can be parameterized just like an ordinary function. -- -- NonEmptyList String -- -- means "a List String guarded by requireNonEmpty". NonEmptyList elem = viewGuarded (viewList elem) requireNonEmpty -- ============================================================================ -- 5. Using a custom View in normal annotations -- ============================================================================ -- -- This value satisfies the custom contract. contributors =@(NonEmptyList String) [("Ada") ("Grace")] -- This function requires NonEmptyList String before its body can run. In a -- library, this is the kind of contract you would put on an operation like -- `head`: callers must prove the list is non-empty first. acceptNames xs@(NonEmptyList String) =@String "accepted non-empty names" primaryContributor =@String acceptNames contributors -- Uncommenting this definition demonstrates a guarded View failure: -- -- nobody =@(NonEmptyList String) [] -- -- The structure is fine (`[]` is a List String), but the runtime guard rejects -- it because the list is empty. -- ============================================================================ -- 6. Contracts protect callers too -- ============================================================================ -- -- Contracts can describe function results as well as arguments. If a function -- promises to return `NonEmptyList String`, checked execution guards that result -- before callers depend on it. mkContributors name@String =@(NonEmptyList String) [(name)] fromSingleName =@String acceptNames (mkContributors "Evelyn") -- Uncommenting this version would fail because the result contract is too -- strong for the implementation: -- -- badContributors name@String =@(NonEmptyList String) [] -- ============================================================================ -- 7. Writing your own Views and Contracts -- ============================================================================ -- -- The pattern is: -- -- 1. Start with the closest structural View. -- 2. Write a guard for the runtime fact the structure cannot express. -- 3. Package them with viewGuarded. -- 4. Use the new View in normal annotations. -- -- Examples of useful guarded Views: -- -- NonEmptyList String -- SortedList Byte -- FixedLengthBytes 32 -- ValidUserId -- NonEmptyString -- -- Guards are intentionally runtime checks. Use plain Views for ordinary shape -- checking, and guarded Views when a boundary really must enforce a stronger -- invariant. main =@String primaryContributor