191 lines
6.7 KiB
Plaintext
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
|