Add a first draft of the IsSomething article
This commit is contained in:
parent
f093868da1
commit
032453c4d0
71
code/agdaissomething/example.agda
Normal file
71
code/agdaissomething/example.agda
Normal file

@ 0,0 +1,71 @@


open import Agda.Primitive using (Level; lsuc)


open import Relation.Binary.PropositionalEquality using (_≡_)




variable


a : Level


A : Set a




module FirstAttempt where


record Semigroup (A : Set a) : Set a where


field


_∙_ : A → A → A




isAssociative : ∀ (a₁ a₂ a₃ : A) → a₁ ∙ (a₂ ∙ a₃) ≡ (a₁ ∙ a₂) ∙ a₃




record Monoid (A : Set a) : Set a where


field semigroup : Semigroup A




open Semigroup semigroup public




field


zero : A




isIdentityLeft : ∀ (a : A) → zero ∙ a ≡ a


isIdentityRight : ∀ (a : A) → a ∙ zero ≡ a




record ContrivedExample (A : Set a) : Set a where


field


 first property


monoid : Monoid A




 second property; Semigroup is a standin.


semigroup : Semigroup A




operationsEqual : Monoid._∙_ monoid ≡ Semigroup._∙_ semigroup




module SecondAttempt where


record IsSemigroup {A : Set a} (_∙_ : A → A → A) : Set a where


field isAssociative : ∀ (a₁ a₂ a₃ : A) → a₁ ∙ (a₂ ∙ a₃) ≡ (a₁ ∙ a₂) ∙ a₃




record IsMonoid {A : Set a} (zero : A) (_∙_ : A → A → A) : Set a where


field


isSemigroup : IsSemigroup _∙_




isIdentityLeft : ∀ (a : A) → zero ∙ a ≡ a


isIdentityRight : ∀ (a : A) → a ∙ zero ≡ a




open IsSemigroup isSemigroup public




record Semigroup (A : Set a) : Set a where


field


_∙_ : A → A → A


isSemigroup : IsSemigroup _∙_




record Monoid (A : Set a) : Set a where


field


zero : A


_∙_ : A → A → A


isMonoid : IsMonoid zero _∙_




module ThirdAttempt {A : Set a} (_∙_ : A → A → A) where


record IsSemigroup : Set a where


field isAssociative : ∀ (a₁ a₂ a₃ : A) → a₁ ∙ (a₂ ∙ a₃) ≡ (a₁ ∙ a₂) ∙ a₃




record IsMonoid (zero : A) : Set a where


field


isSemigroup : IsSemigroup




isIdentityLeft : ∀ (a : A) → zero ∙ a ≡ a


isIdentityRight : ∀ (a : A) → a ∙ zero ≡ a




open IsSemigroup isSemigroup public

161
content/blog/agda_is_pattern.md
Normal file
161
content/blog/agda_is_pattern.md
Normal file

@ 0,0 +1,161 @@





title: "The \"Is Something\" Pattern in Agda"


date: 20230828T21:05:3907:00


draft: true


tags: ["Agda"]


description: "In this post, I talk about a pattern I've observed in the Agda standard library."







Agda is a functional programming language with a relatively Haskelllike syntax


and feature set, so coming into it, I relied on my past experiences with Haskell


to get things done. However, the languages are sufficiently different to leave


room for useful design patterns in Agda that can't be brought over from Haskell,


because they don't exist there. One such pattern will be the focus of this post;


it's relatively simple, but I came across it by reading the standard library code.


My hope is that by writing it down here, I can save someone the trouble of


recognizing it and understanding its purpose. The pattern is "unique" to Agda


(in the sense that it isn't present in Haskell) because it relies on dependent types.




In my head, I call this the `IsSomething` pattern. Before I introduce it, let


me try to provide some motivation.




### Type Classes for Related Operations


Suppose you wanted to define a type class for "a type that has an associative


binary operation". In Haskell, this is the famous `Semigroup` class. Here's


a definition I lifted from the [Haskell docs](https://hackage.haskell.org/package/base4.18.0.0/docs/src/GHC.Base.html#Semigroup):




```Haskell


class Semigroup a where


(<>) :: a > a > a


a <> b = sconcat (a : [ b ])


```




It says that a type `a` is a semigroup if it has a binary operation, which Haskell


calls `(<>)`. The language isn't expressive enough to encode the associative


property of this binary operation, but we won't hold it against Haskell: not


every language needs dependent types or SMTbacked refinement types. If


we translated this definition into Agda (and encoded the associativity constraint),


we'd end up with something like this:




{{< codelines "Agda" "agdaissomething/example.agda" 9 13 >}}




So far, so good. Now, let's also encode a more specific sort of typewithbinaryoperation:


one where the operation is associative as before, but also has an identity element.


In Haskell, we can write this as:




```Haskell


class Semigroup a => Monoid a where


mempty :: a


```




This brings in all the requirements of `Semigroup`, with one additional one:


an element `mempty`, which is intended to be said identity element for `(<>)`.


Once again, we can't encode the "identity element" property; I say this only


to explain the lack of any additional code in the preceding code snippet.




In Agda, there isn't really a special syntax for "superclass"; we just use a field.


The "transliterated" implementation is as follows:




{{< codelines "Agda" "agdaissomething/example.agda" 15 24 >}}




This code might require a little bit of explanation. Like I said, the "parent"


class is brought in as a field, `semigroup`. Then, every field of `semigroup`


is also made available within `Monoid`, as well as to users of `Monoid`, by


using an `open public` directive. The subsequent fields mimic the Haskell


definition amended with proofs of identity.




We get our first sign of awkwardness here. We can't refer to the binary operation


very easily; it's nested inside of `semigroup`, and we have to access its fields


to get ahold of (∙). It's not too bad at all  it just cost us an extra line.


However, the bookkeeping of whatoperationiswhere gets frustrating quickly.




I will demonstrate the frustrations in one final example. I will admit to it


being contrived: I am trying to avoid introducing too many definitions and concepts


just for the sake of a motivating case. Suppose you are trying to specify


a type in which the binary operation has _two_ properties (e.g. it's a monoid


_and_ something else). Since the only two type classes I have so far are


`Monoid` and `Semigroup`, I will use those; note that in this particular instance,


using both is a contrivance, since one contains the latter.




{{< codelines "Agda" "agdaissomething/example.agda" 26 32 >}}




However, note the problem: nothing in the above definition ensures that the


binary operations of the two fields are the same! As far as Agda is concerned


(as one would quickly come to realize by trying a few proofs with the code),


the two operations are completely separate. One could perhaps add an equality


constraint:




{{< codelines "Agda" "agdaissomething/example.agda" 26 34 >}}




However, this will get tedious quickly. Proofs will need to leverage rewrites


(via the `rewrite` keyword, or via `cong`) to change one of the binary operations


into the other. As you build up more and more complex algebraic structures, on


in which the various operations are related in nontrivial ways, you start to


look for other approaches. That's where the `IsSomething` pattern comes in.




### The `IsSomething` Pattern: Parameterizing By Operations


The pain point of the original approach is data flow. The way it's written,


data (operations, elements, etc.) flows from the fields of a type to the record


that contains them: `Monoid` has to _read_ the (∙) operation from `Semigroup`.


The more fields you add, the more reading and reconciliation you have to do.


It would be better if the data flowed the other direction: from `Monoid` to


`Semigroup`. `Monoid` could say, "here's a binary operation; it must satisfy


these constraints, in addition to having an identity element". To _provide_


the binary operation to a field, we use type application; this would look


something like this:




{{< codelines "Agda" "agdaissomething/example.agda" 42 42 >}}




Here's the part that's not possible in Haskell: we have a `record`, called `IsSemigroup`,


that's parameterized by a _value_  the binary operation! This new record


is quite similar to our original `Semigroup`, except that it doesn't need a field


for (∙): it gets that from outside. Note the additional parameter in the


`record` header:




{{< codelines "Agda" "agdaissomething/example.agda" 37 38 >}}




We can define an `IsMonoid` similarly:




{{< codelines "Agda" "agdaissomething/example.agda" 40 47 >}}




Note that we want to make an "is" version for each algebraic property; this way,


if we want to use "monoid" as part of some other structure, we can pass it


the required binary operation the same way we passed it to `IsSemigroup`.




Of course, these new records are not quite original to our original ones. They


need to be passed a binary operation; a "complete" package should include the


binary operation _in addition_ to its properties encoded as `IsSemigroup` or


`IsMonoid`. Such a complete package would be moreorless equivalent to our


original `Semigroup` and `Monoid` instances. Here's what that would look like:




{{< codelines "Agda" "agdaissomething/example.agda" 49 58 >}}




Agda calls records that include both the operation and its `IsSomething` record


_bundles_ (see [`Algebra.Bundles`](https://agda.github.io/agdastdlib/Algebra.Bundles.html), for example).


Notice that the bundles don't contain other bundles; that would lead right back


to the "bottomup" data flow in which a parent record has to access the operations and


values stored in its fields. Thus, bundles occur only at the top level; you use


them if they represent _the whole_ algebraic structure you need, rather than


an aspect of it.




### Bonus: Using Parameterized Modules to Avoid Repetitive Arguments




One annoying thing about our definitions above is that we had to accept our


binary operation, and sometimes the zero element, as an argument to each one,


and to thread it through to all the fields that require it. Agda has a nice


mechanism to help alleviate some of this repetition: [parameterized modules](https://agda.readthedocs.io/en/latest/language/modulesystem.html#parameterisedmodules).


We can define a _whole module_ that accepts the binary operation as an argument;


it will be implicitly passed as an argument to all of the definitions within.


Thus, our entire `IsMonoid` and `IsSemigroup` code could look like this:




{{< codelines "Agda" "agdaissomething/example.agda" 60 71 >}}




The more `IsSomething` records you declare, the more effective this trick becomes.




### Conclusion


That's all I have! The pattern I've described shows up all over the Agda


standard library; the example that made me come across it was


the [`Algebra.Structures` module](https://agda.github.io/agdastdlib/Algebra.Structures.html).


I hope you find it useful.




Happy (dependently typed) programming!

Loading…
Reference in New Issue
Block a user