Arend - HoTT-based dependent type language (part 2)

In the first part of the article about the Arend language, we examined the simplest inductive types, recursive functions, classes and sets.



2. Sorting Lists in Arend



2.1 Ordered Lists in Arend



We define the type of ordered lists as a pair consisting of a list and proof of its ordering. As we said before, in Arend, dependent pairs are defined using the \Sigma



keyword. We will determine the type of Sorted



by comparing with the sample, inspired by the definition from the already mentioned article about ordered lists.



 \func SortedList (O : LinearOrder.Dec) => \Sigma (l : List O) (Sorted l) \data Sorted {A : LinearOrder.Dec} (xs : List A) \elim xs | nil => nilSorted | :-: x nil => singletonSorted | :-: x1 (:-: x2 xs) => consSorted ((x1 = x2) || (x1 < x2)) (Sorted (x2 :-: xs))
      
      





Note: Arend was able to automatically infer that the Sorted



type is contained in the \Prop



universe. This happened because all three patterns in the Sorted



definition are mutually exclusive, and the consSorted



constructor has two parameters, both of which belong to \Prop



.

Let us prove some obvious property of the Sorted



predicate, say that the tail of an ordered list is itself an ordered list (this property will be useful to us in the future).



 \func tail-sorted {O : LinearOrder.Dec} (x : O) (xs : List O) (A : Sorted (x :-: xs)) : Sorted xs \elim xs, A | nil, _ => nilSorted | :-: _ _, consSorted _ xs-sorted => xs-sorted
      
      





In the tail-sorted



we used pattern matching on the xs



list and the Sorted



predicate at the same time, in addition, we used the skip symbol “_”, which can be substituted for unused variables.



One may ask if it is possible in Arend to prove the property of ordered lists, mentioned in section 1.3 as an example of a fact that cannot be proved in Agda without annotations of immateriality. Recall that this property states that to prove the equality of ordered lists defined through dependent pairs, it is sufficient to verify the equality of the first components of the pairs.



It is argued that in Arend this property is easily obtained as a consequence of the inProp



construction already mentioned above and the extensionality property for dependent SigmaExt



pairs.



 \func sorted-equality {A : LinearOrder.Dec} (l1 l2 : SortedList A) (P : l1.1 = l2.1) : l1 = l2 => SigmaPropExt Sorted l1 l2 P
      
      





The SigmaPropExt



property SigmaPropExt



proved in the Paths module of the standard library, and many other facts from the second chapter of the HoTT book , including the property of functional extensionality , are also proved there.



The .n



operator .n



used in Arend to access the sigma type projector with number n (in our case, the sigma type is SortedList A



, and the expression l1.1



means the first component of this type is an expression of type List A



).



2.2 Implementation of the “be permutation” property



Now let's try to implement the list sorting function on Arend. Naturally, we want to have not a simple implementation of the sorting algorithm, but an implementation together with a proof of some properties.



Clearly, this algorithm must have at least 2 properties:

1. The result of the algorithm should be an ordered list.

2. The resulting list should be a permutation of the original list.



First, let’s try to implement the “be permutation” property of lists on Arend. To do this, we adapt the definition taken from here for Arend.



 \truncated \data InsertSpec {A : \Set} (xs : List A) (a : A) (ys : List A) : \Prop \elim xs, ys | xs, :-: y ys => insertedHere (a = y) (xs = ys) | :-: x xs, :-: y ys => insertedThere (x = y) (InsertSpec xs a ys) \truncated \data Perm {A : \Set} (xs ys : List A) : \Prop | permInsert (xs' ys' : List A) (a : A) (Perm xs' ys') (InsertSpec xs' a xs) (InsertSpec ys' a ys) | permTrivial (xs = ys)
      
      





The InsertSpec



predicate introduced by us has the following intuitive meaning: InsertSpec xs a ys



exactly means that the list ys



is the result of inserting the element a inside the list xs



(at any position). Thus, InsertSpec



can be taken as a specification of the insert function.



Clearly, the Perm



data type really defines the “be permutation” relationship: the permInsert



constructor permInsert



exactly that xs



and ys



are mutually permutable if xs



and ys



are obtained by inserting the same element a into some lists xs'



and ys'



shorter lengths, which are already permutations of each other.



For our definition of the “be permutation” property, it is easy to verify the symmetry property.



 \func Perm-symmetric {A : \Set} {xs ys : List A} (P : Perm xs ys) : Perm ys xs \elim P | permTrivial xs=ys => permTrivial (inv xs=ys) | permInsert perm-xs'-ys' xs-spec ys-spec => permInsert (Perm-symmetric perm-xs'-ys') ys-spec xs-spec
      
      





The transitivity property is also fulfilled for Perm



, however, its verification is much more complicated. Since this property does not play any role in the implementation of our sorting algorithm, we leave it to the reader as an exercise.



 \func Perm-transitive {A : \Set} (xs ys zs : List A) (P1 : Perm xs ys) (P2 : Perm ys zs) : Perm xs zs => {?}
      
      





2.3 Change in homotopy levels when compared with the sample



Now let's try to implement a function that inserts an element into an ordered list so that the resulting list remains ordered. Let's start with the following naive implementation.



 \func insert {O : LinearOrder.Dec} (xs : List O) (y : O) : List O \elim xs | nil => y :-: nil | :-: x xs' => \case LinearOrder.trichotomy xy \with {  | byLeft x=y => x :-: insert xs' y  | byRight (byLeft x<y) => x :-: insert xs' y  | byRight (byRight y<x) => y :-: x :-: xs' }
      
      





The \case



construction allows matching with a sample of an arbitrary expression ( \elim



can be used only at the highest level of a function definition and only for its parameters). If you ask Arend to check the insert



type, the following error message will be displayed.



 [ERROR] Data type '||' is truncated to the universe \Prop  which does not fit in the universe of the eliminator type: List OE In: | byLeft x-leq-y => x :-: insert xs' y While processing: insert
      
      





The problem is that in the LinearOrder.Dec



class LinearOrder.Dec



definition of trichotomy



given using the ||



operator , which, in turn, is determined using propositional truncation. As already mentioned, for types belonging to the \Prop



universe, matching with a pattern in Arend is allowed only if the type of the resulting expression is itself an assertion (for the function above, the result is of type List OE



, and this type is a set).



Is there any way around this problem? The easiest way to solve this is to change the definition of the property of trichotomy. Consider the following definition of trichotomy, using the un-truncated type Or



instead of the truncated ||



:



 \func set-trichotomy {A : StrictPoset} (xy : A) => ((x = y) `Or` (x < y)) `Or` (y < x)
      
      





Does this definition trichotomy



in anything from the original trichotomy



definition through ||



? Why did we even use a propositionally truncated type if it only complicates our life and prevents us from using pattern matching?



Let's try to answer the first question for a start: for strict StrictPoset



orders StrictPoset



difference between trichotomy



and set-trichotomy



is actually not at all. Note that the set-trichotomy



type is a statement. This fact follows from the fact that all three alternatives in the definition of trichotomy are mutually exclusive due to axioms of order, and each of the three types x = y, x < y, y < x



is itself a statement ( x = y



is a statement, so as in the definition of the BaseSet



class BaseSet



we demanded that the media E



a set!).



 \func set-trichotomy-isProp {A : StrictPoset} (xy : A) (l1 l2 : set-trichotomy xy): l1 = l2 \elim l1, l2 | inl (inl l1), inl (inl l2) => pmap (\lam z => inl (inl z)) (Path.inProp l1 l2) | inl (inr l1), inl (inr l2) => pmap (\lam z => inl (inr z)) (Path.inProp l1 l2) | inr l1, inr l2 => pmap inr (Path.inProp l1 l2) | inl (inl l1), inl (inr l2) => absurd (lt-eq-false l1 l2) | inl (inr l1), inl (inl l2) => absurd (lt-eq-false l2 l1) | inl (inl l1), inr l2 => absurd (lt-eq-false (inv l1) l2) | inr l1, inl (inl l2) => absurd (lt-eq-false (inv l2) l1) | inl (inr l1), inr l2 => absurd (lt-lt-false l1 l2) | inr l1, inl (inr l2) => absurd (lt-lt-false l2 l1) \where {  \func lt-eq-false {A : StrictPoset} {xy : A} (l1 : x = y) (l2 : x < y) : Empty =>    A.<-irreflexive x (transport (x <) (inv l1) l2)  \func lt-lt-false {A : StrictPoset} {xy : A} (l1 : x < y) (l2 : y < x) : Empty =>   A.<-irreflexive x (A.<-transitive _ _ _ l1 l2) }
      
      





In the listing above, absurd



is the designation for the ex falso quodlibet principle, which is defined in the Logic module. Since the Empty



type does not have constructors in the definition (see section 1.2), it is not necessary to go through cases in the definition of absurd



:



 \func absurd {A : \Type} (x : Empty) : A
      
      





Since we now know that set-trichotomy



is a statement, we can derive the set-trichotomy



property from the usual trichotomy



property of decidable orders. To do this, we can use the \return \level



construction, which tells the Arend timer that at this point the pattern matching is a permitted operation (we have to show proof that the result of the set-trichotomy-property



function is a statement).



 \func set-trichotomy-property {A : LinearOrder.Dec} (xy : A) : set-trichotomy xy => \case A.trichotomy xy \return \level (set-trichotomy xy) (set-trichotomy-isProp xy) \with {  | byLeft x=y => inl (inl x=y)  | byRight (byLeft x<y) => inl (inr x<y)  | byRight (byRight y<x) => inr (y<x) }
      
      





Let us now try to answer the second question, namely, why, when formulating the properties of mathematical objects, it is preferable to use not ordinary, but propositionally truncated constructions. Consider for this a fragment of the definition of non-strict linear orders (full definitions of Lattice



and TotalOrder



can be found in the LinearOrder module):



 \class TotalOrder \extends Lattice { | totality (xy : E) : x <= y || y <= x }
      
      





Now let's try to TotalOrder



how the meaning of the TotalOrder



class would have changed if we had written the definition of the totality field through the un-truncated Or



construction.



 \class BadTotalOrder \extends Lattice { | badTotality (xy : E) : (x <= y) `Or` (y <= x) }
      
      





In this case, the type (x <= y) `Or` (y <= x)



no longer a statement, because in case of equal values ​​of x



and y



both alternatives in the definition of badTotality



can be implemented, and the choice of the left or right branch in the proof of badTotality



absolutely arbitrary and remains at the discretion of the user - there is no reason to prefer one Or



constructor to another.



Now it’s clear what the difference between TotalOrder



and BadTotalOrder



. Two ordered sets O1 O2



: TotalOrder



are always equal when it is possible to prove the equality of the sets O1.E, O2.E



and the orders O1.<, O2.<



Given on them (this is the desired property). On the other hand, for O1 O2



: BadTotalOrder



it is BadTotalOrder



to prove the equality of O1



and O2



only in cases where, in addition to all elements x



from E



equality O1.badTotality xx



and O2.badTotality xx



.



Thus, it turns out that the class BadTotalOrder



intuitively needs to be considered not as a “linearly ordered set”, but as a “linearly ordered set together with the choice for each element x



field E



left or right branch Or



in the implementation of badTotality xx



".



2.4 Sort Algorithm



We now proceed to implement the sorting algorithm. Let's try to fix the naive implementation of the insert



function from the previous section using the proved set-trichotomy-property



(in this case, due to the more successful arrangement of brackets in the definition of set-trichotomy



, we have reduced the number of cases considered).



 \func insert {O : LinearOrder.Dec} (xs : List O) (y : O) : List O \elim xs | nil => y :-: nil | :-: x xs' => \case set-trichotomy-property xy \with {  | inr y<x => y :-: x :-: xs'  | inl x<=y => x :-: insert xs' y }
      
      





Let us now try to implement an analog of this definition for ordered lists. We will use the special \let … \in



construct, which allows us to add new local variables to the context.



 \func insert {O : LinearOrder.Dec} (xs : SortedList O) (y : O) : SortedList O \elim xs | (nil, _) => (y :-: nil, singletonSorted) | (:-: x xs', xs-sorted) => \case set-trichotomy-property xy \with {  | inr y<x => (y :-: x :-: xs', consSorted (byRight y<x) xs-sorted)  | inl x<=y => \let (result, result-sorted) => insert (xs', tail-sorted x xs' xs-sorted) y         \in (x :-: result, {?})
      
      





We left an incomplete fragment in the proof (indicated by the expression {?}



) In the place where we need to show that the list x :-: result



ordered. Although in the context there is evidence of the ordering of the result



list, it remains for us to verify that x



does not exceed the value of the first element of the result



list, which is not so easy to follow from the premises in the context (to see all the premises in the current target - this is what we call present branch of calculations - you need to request type checking from the insert



function).



It turns out that insert



much easier to implement if we prove the ordering of the resulting list in parallel with the proof of the insert



specification. Change the signature of insert



and write proof of this specification in the simplest cases:



 \func insert {O : LinearOrder.Dec} (xs : SortedList O) (y : O) : \Sigma (ys : SortedList O) (InsertSpec xs.1 y ys.1) \elim xs | (nil, _) => ((y :-: nil, singletonSorted), insertedHere idp idp) | (:-: x xs', xs-sorted) => \case set-trichotomy-property xy \with {  | inr y<x => ((y :-: x :-: xs', consSorted (byRight y<x) xs-sorted), insertedHere idp idp)  | inl x<=y =>   \let ((result, result-sorted), result-spec) => insert (xs', tail-sorted x xs' xs-sorted) y   \in ((x :-: result, {?}), insertedThere idp result-spec)
      
      





For a single fragment left without proof, Arend will output the following context value:



 Expected type: Sorted (x :-: (insert (\this, tail-sorted x \this \this) \this).1.1) Context:  result-sorted : Sorted (insert (\this, tail-sorted \this \this \this) \this).1.1  xs-sorted : Sorted (x :-: xs')  x : O  x<=y : Or (x = y) (O.< xy)  O : Dec  result : List O  y : O  xs' : List O  result-spec : InsertSpec xs' y (insert (xs', tail-sorted \this xs' \this) y).1.1
      
      





To complete the proof, we will have to use the power of the \case



operator to the full extent: we will use pattern matching with 5 different variables, and since the types of some variables may depend on the values ​​of other variables, we will use dependent pattern matching.



The colon construction explicitly indicates how the type of some variables to be compared depends on the values ​​of other variables (thus, in the type of variables xs-sorted, result-spec



and result-sorted



in each of \case



instead of xs'



and result



will match the corresponding samples).



The \return



construct associates the variables used to match the pattern with the type of expected result. In other words, for the current target, in each of the \case



clauses, the corresponding sample will be substituted for the result



variable. Without this construction, such a replacement would not be carried out, and the goal of all the \case



clauses would coincide with the goal in place of the \case



expression itself.



 \func insert {O : LinearOrder.Dec} (xs : SortedList O) (y : O) : \Sigma (ys : SortedList O) (InsertSpec xs.1 y ys.1) \elim xs  | (nil, _) => ((y :-: nil, singletonSorted), insertedHere idp idp)  | (:-: x xs', xs-sorted) => \case set-trichotomy-property xy \with {   | inr y<x => ((y :-: x :-: xs', consSorted (byRight y<x) xs-sorted), insertedHere idp idp)   | inl x<=y =>     \let ((result, result-sorted), result-spec) => insert (xs', tail-sorted x xs' xs-sorted) y     \in ((x :-: result,       \case result \as result, xs' \as xs', xs-sorted : Sorted (x :-: xs'), result-spec : InsertSpec xs' y result, result-sorted : Sorted result       \return Sorted (x :-: result) \with {        | nil, _, _, _, _ => singletonSorted        | :-: r rs, _, _, insertedHere y=r _, result-sorted => consSorted (transport (\lam z => (x = z) || (x < z)) y=r (Or-to-|| x<=y)) result-sorted        | :-: r rs, :-: x' _, consSorted x<=x' _, insertedThere x2=r _, result-sorted => consSorted (transport (\lam z => (x = z) || (x < z)) x2=r x<=x') result-sorted }), insertedThere idp result-spec)
      
      





In the above block of code, the complicated first arguments to the consSorted



constructor in the last two paragraphs of pattern matching deserve additional comment. To understand what both of these expressions mean, we replace them with the expression {?}



And ask the Arend timer for determining targets at both positions.



You can see that both there and there the current target is the type (x = r) || O.< xr



(x = r) || O.< xr



. Moreover, in the context of the first goal, there are premises



 x<=y : Or (x = y) (O.< xy) y=r : y = r
      
      





and in the context of the second - premises



 x<=x' : (x = x') || O.< xx' x2=r : x' = r.
      
      





Intuitively clear: to prove the first goal, it is enough to substitute the variable r



into the correct statement Or (x = y) (O.< xy)



instead of the y variable, and then switch to the propositionally truncated type ||



using the Or-to-||



function defined in Section 1.3 . To prove the second goal, just substitute in (x = x') || O.< x x'



(x = x') || O.< x x'



instead of the variable x'



variable r



.



To formalize the described expression substitution operation, a special transport



function exists in the standard Arend library. Consider her signature:



 \func transport {A : \Type} (B : A -> \Type) {aa' : A} (p : a = a') (b : B a) : B a'
      
      





In our case, instead of the variable A



we need to substitute the type OE



(it can be omitted explicitly if the other transport



arguments are specified), and instead of B



, the expression \lam (z : O) => (x = z) || (x < z)



\lam (z : O) => (x = z) || (x < z)



.



Implementation of the insertion sorting algorithm together with the specification does not cause any special difficulties: in order to sort the list x :-: xs'



, we first sort the tail of the list xs'



using a recursive call to insertSort



, and then insert the element x



inside this list with preserving the order for help access to the already implemented insert



function.



 \func insertSort {O : LinearOrder.Dec} (xs : List O) : \Sigma (result : SortedList O) (Perm xs result.1) \elim xs | nil => ((nil, nilSorted), permTrivial idp) | :-: x xs' => \let | (ys, perm-xs'-ys) => insertSort xs'                      | (zs, zs-spec) => insert ys x                  \in (zs, permInsert perm-xs'-ys (insertedHere idp idp) zs-spec)
      
      





We fulfilled the original goal and implemented sorting lists on Arend. The entire Arend code given in this section can be downloaded in one file from here .



One might ask how one would have to change the implementation of the insert



function if instead of the strict LinearOrder.Dec



orders we used the non-strict TotalOrder



orders? As we recall, in the definition of the totality function, the use of the truncated operation ||



was quite significant, that is, this definition is not equivalent to a definition in which instead of ||



used by Or



.



The answer to this question is as follows: it is still possible to build an insert



analogue for TotalOrder



, however, for this we would have to prove that the type of the insert



function is a statement (this would allow us in the definition of insert



to make a comparison with the sample according to the totality xy



statement).



In other words, we would have to prove that there is only one ordered list, up to equality, which is the result of inserting the element y



into the ordered list xs



. It is easy to see that this is a true fact, but its formal proof is no longer so trivial. We leave verification of this fact as an exercise for the interested reader.



3. Concluding observations



In this introduction, we got acquainted with the main constructs of the Arend language, and also learned how to use the class mechanism. We managed to implement the simplest algorithm along with the proof of its specification. Thus, we have shown that Arend is quite suitable for solving "everyday" problems, such as, for example, program verification.



We have mentioned not all Arend features and features. For example, we said almost nothing about types with conditions that allow you to “glue” various type constructors with some special parameter values ​​for these constructors. For example, an implementation of the integer type in Arend is given using types with conditions as follows:



 \data Int | pos Nat | neg Nat \with { zero => pos zero }
      
      





This definition says that integers consist of two copies of the type of natural numbers, in which "positive" and "negative" zeros are identified. Such a definition is much more convenient than the definition from the standard Coq library, where the “negative copy” of natural numbers has to be “shifted by one” so that these copies do not intersect (it is much more convenient when neg 1



notation stands for -1, not -2) .



We did not say anything about the algorithm for deriving predicative and homotopy levels of classes and their instances. We also hardly mentioned the type of interval I



, although it plays a key role in the theory of types with an interval, which are the logical basis of Arend. To understand how important this type is, it’s enough to mention that in Arend type equality is defined through the concept of an interval.Comparison with the sample in a variable having the type of interval is carried out according to rather specific rules, which we also did not say anything about in this article (the so-called homotopy comparison with the sample).



Posted by Sergey Sinchuk , Senior Researcher , HoTT and Dependent Types at JetBrains Research.



All Articles