Files
tricu/demos/viewContracts.tri
James Eversole fdebb6c13d Tricu 2.0.0
Sorry for squashing all of this but 🤷
2026-05-25 12:44:24 -05:00

191 lines
6.7 KiB
Plaintext

!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