{-- -- TalTech ITI0212 -- -- Functional Programming -- -- week 16, 2022-05-11 -- -- Decidability and Automation --} module Lecture16 import Data.Nat import Data.List import Data.Maybe import Decidable.Equality import Lecture13 -- for the IsEven predicate import Lecture15 -- for the IsOdd predicate %default total -- for proof validity {- Decidable Propositions -} -- A proposition is called *decidable* if we can -- either *affirm* it by providing a proof that it is true -- or else *refute* it by providing a proof that it is false. -- Under the propositions-as-types interpretation of logic this means -- either producing an element of the given type, -- or else showing that the type is empty by defining -- a total function from it to the canonical empty type Void. data Decide : (proposition : Type) -> Type where -- like the boolean True, but with evidence: Affirm : (prf : proposition) -> Decide proposition -- like the boolean False, but with evidence: Refute : (ref : Not proposition) -> Decide proposition two_is_even : Decide (IsEven 2) two_is_even = Affirm $ IsEvenSS IsEvenZ four_is_not_five : Decide (2 + 2 = S 4) four_is_not_five = Refute $ \ e => case e of Refl impossible -- The Idris standard library has a version of -- our Decide type constructor called Dec, -- with constructors called Yes and No. {- Decidable Predicates -} -- A predicate is *decidable* if we can either -- prove or disprove that the predicate holds -- of each possible argument. -- Under propositions-as-types, this corresponds to -- either producing an element of the type determined by the index, -- or else proving that type is empty. -- A function that decides a predicate -- is called a *decision procedure*. -- a decision procedure for the IsEven predicate: decide_even : (n : Nat) -> Dec (IsEven n) decide_even Z = Yes IsEvenZ decide_even (S n) = case decide_even n of Yes n_even => No $ isOddSuccEven n_even No n_odd => Yes $ isEvenSuccOdd n_odd where -- from lab 15: isEvenSuccOdd : {n : Nat} -> IsOdd n -> IsEven (S n) isEvenSuccOdd {n = 0} zero_odd = void $ zero_odd IsEvenZ isEvenSuccOdd {n = 1} one_odd = IsEvenSS IsEvenZ isEvenSuccOdd {n = S (S n)} ssn_odd = let IH = isEvenSuccOdd {n = n} $ ssn_odd . IsEvenSS in IsEvenSS IH -- Once we have a decision procedure for a predicate -- we don't need to write proofs of its instance propositions -- by hand anymore: -- If the proposition is true then the decision procedure -- will find a proof for us. -- If the proposition is false then the decision procedure -- will tell us why. -- is_four_even = decide_even 4 -- is_three_even = decide_even 3 {- Decidable Equality -} -- lemma: the successor function is injective: pred_equal : S m = S n -> m = n pred_equal Refl = Refl -- a decision procedure for Nat equality: decide_nat_eq : (m , n : Nat) -> Dec (m = n) decide_nat_eq Z Z = Yes Refl decide_nat_eq Z (S n) = No uninhabited decide_nat_eq (S m) Z = No uninhabited decide_nat_eq (S m) (S n) = case decide_nat_eq m n of Yes m_n_equal => Yes $ cong S m_n_equal No m_n_differ => No $ m_n_differ . pred_equal -- does_four_equal_four = decide_nat_eq (2 + 2) (2 * 2) -- does_three_equal_four = decide_nat_eq 3 4 -- The module Decidable.Equality provides an interface -- for types with decidable equality called DecEq -- along with many common instances. -- does_true_equal_false = decEq True False -- does_just_three_equal_itself = decEq (Just 3) (Just 3) -- does_nil_equal_cons = decEq (the (List Nat) []) [1,2,3] {-- Constraint Arguments --} -- An *implicit argument* is written between braces -- e.g. {n : Nat} -> ... -- Idris tries to infer implicit arguments with a *unification algorithm* -- based on syntactic matching and recursion on subterms. -- e.g. -- Vect n Bool -- Vect 3 a -- ----------- -- n = 3 , a = Bool -- A *constraint argument* is different kind of implicit argument -- (also called an *auto implicit argument*). -- It is written using the double-shafted arrow -- e.g. (n : Nat) => ... -- Idris tries to infer constraint arguments with a *search algorithm* -- that (by default) tries to build a term of a given type -- using constructors, recursion, and function literals. -- This is the same search algorithm used by the editor integration. list_head : (xs : List a) -> (nonempty : NonEmpty xs) => a list_head [] impossible list_head (x :: xs) = x -- You can augment the search algorithm by using %hint directives -- to specify additional terms that you want Idris to try. %hint -- needed to satisfy the constraint below uninhabited_is_empty : Uninhabited a => Not a uninhabited_is_empty = uninhabited -- the predecessor of a Nat under the constraint that it is not zero: nat_pred : (n : Nat) -> (nonzero : Not (n = 0)) => Nat nat_pred Z = void $ nonzero Refl nat_pred (S n) = n -- A longer example (time permitting): -- find the square root of a perfect square -- using decidable equality and constraints: -- a predicate for perfect squares: data IsSquare : Nat -> Type where SquareOf : (m : Nat) -> IsSquare (m * m) -- e.g.: four_is_a_square : IsSquare 4 four_is_a_square = SquareOf 2 search_square_root : (n : Nat) -> Maybe (IsSquare n) search_square_root n = search_square_root_below n (S n) where -- search for the square root of n strictly below the bound m: search_square_root_below : (n , m : Nat) -> Maybe (IsSquare n) search_square_root_below n Z = Nothing search_square_root_below n (S m) = case decEq n (m * m) of Yes Refl => Just $ SquareOf m No n_not_mm => search_square_root_below n m square_root : (n : Nat) -> (is_square : IsJust (search_square_root n)) => Nat square_root n with (search_square_root n) square_root n | Nothing = absurd is_square square_root (m * m) | Just (SquareOf m) = m