module Cat.Instances.Assemblies where
Assemblies over a PCA🔗
When working over a partial combinatory algebra it’s often the case that we’re interested in programs as concrete implementations of some mathematical datum For example, we can implement the successor function on natural numbers to be representing a numeral as a Church numeral, taking the defining property of to be that if we have some iterable process starting at then the iteration is applied to the iteration; But we could just as well implement representing a numeral as a Curry numeral, a pair containing the information of whether the number is zero and its predecessor (if any). These implementations are extensionally identical, in that they both denote the same actual natural number, but for a concrete pca they might genuinely be different — we could imagine measuring the time complexity of the predecessor function, which is for Curry numbers and for Church numbers. Therefore, if we are to investigate the computational content of constructive mathematics, we need a way to track the connection between the mathematical elements and the programs which denote them.
An assembly over a pca is a set equipped with a propositional relation between values and elements when this holds, we say realises Moreover, for every we require that there be at least one which realises it.
The construction of assemblies over and the category works regardless of which pca we choose — but we only get something interesting if is nontrivial: the category over the trivial pca is the category
Therefore, when making natural-language statements about we generally assume that is nontrivial. A statement like “the category is not univalent” should be read as saying “univalence of implies is trivial.”
record Assembly (𝔸 : PCA ℓA) ℓ : Type (lsuc ℓ ⊔ ℓA) where no-eta-equality field Ob : Type ℓ has-is-set : is-set Ob realisers : Ob → ℙ⁺ 𝔸 realised : ∀ x → ∃[ a ∈ ↯ ⌞ 𝔸 ⌟ ] (a ∈ realisers x)
A prototypical example is the assembly of booleans, 𝟚
,
defined below. Its set of
elements is Bool
, and
we fix realisers
see booleans in a pca for the details of
the construction. This is not the only possible choice: we could, for
example, say that the value true
is implemented by the
program
(and vice-versa). This results in a genuinely different assembly
over Bool
1,
though with the same denotational data.
module _ {x : Ob} where open ℙ⁺ (realisers x) using (def) public open Assembly public private variable X Y Z : Assembly 𝔸 ℓ instance Underlying-Assembly : Underlying (Assembly 𝔸 ℓ) Underlying-Assembly = record { ⌞_⌟ = Assembly.Ob } hlevel-proj-asm : hlevel-projection (quote Assembly.Ob) hlevel-proj-asm .hlevel-projection.has-level = quote Assembly.has-is-set hlevel-proj-asm .hlevel-projection.get-level _ = pure (quoteTerm (suc (suc zero))) hlevel-proj-asm .hlevel-projection.get-argument (_ ∷ _ ∷ _ ∷ c v∷ []) = pure c hlevel-proj-asm .hlevel-projection.get-argument (_ ∷ c v∷ []) = pure c {-# CATCHALL #-} hlevel-proj-asm .hlevel-projection.get-argument _ = typeError [] module _ (X : Assembly 𝔸 ℓ) (a : ↯ ⌞ 𝔸 ⌟) (x : ⌞ X ⌟) where open Ω (X .realisers x .mem a) renaming (∣_∣ to [_]_⊩_) public -- This module can't be parametrised so this display form can fire -- (otherwise it gets closed over pattern variables that aren't solvable -- from looking at the expression, like the level and the PCA): {-# DISPLAY realisers X x .ℙ⁺.mem a = [ X ] a ⊩ x #-} subst⊩ : {𝔸 : PCA ℓA} (X : Assembly 𝔸 ℓ) {x : ⌞ X ⌟} {p q : ↯ ⌞ 𝔸 ⌟} → [ X ] p ⊩ x → q ≡ p → [ X ] q ⊩ x subst⊩ X {x} hx p = subst (_∈ X .realisers x) (sym p) hx
To understand the difference— and similarity— between the ordinary assembly of booleans and the swapped booleans, we define a morphism of assemblies to be a function satisfying the property that there exists a program which sends realisers of to realisers of
record Assembly-hom {𝔸 : PCA ℓA} (X : Assembly 𝔸 ℓ) (Y : Assembly 𝔸 ℓ') : Type (ℓA ⊔ ℓ ⊔ ℓ') where open Realisability.Base 𝔸 using ([_]_⊢_) field map : ⌞ X ⌟ → ⌞ Y ⌟ tracked : ∥ [ map ] X .realisers ⊢ Y .realisers ∥
Note the force of the propositional truncation in this definition: maps of assemblies are identical when they have the same underlying function, regardless of which programs potentially implement them. Since we can not, for a general show that the programs and are identical, would not be a category if the choice of realiser mattered for identity of computable maps.
This consideration is necessary for assemblies and assembly morphisms to be a category: in an arbitrary pca composition of programs need not be unital or associative.
private unquoteDecl eqv = declare-record-iso eqv (quote Assembly-hom) instance H-Level-Assembly-hom : ∀ {n} → H-Level (Assembly-hom X Y) (2 + n) H-Level-Assembly-hom = basic-instance 2 $ Iso→is-hlevel 2 eqv (hlevel 2) Extensional-Assembly-hom : ∀ {ℓr} ⦃ _ : Extensional (⌞ X ⌟ → ⌞ Y ⌟) ℓr ⦄ → Extensional (Assembly-hom X Y) ℓr Extensional-Assembly-hom ⦃ e ⦄ = injection→extensional! (λ p → Iso.injective eqv (Σ-prop-path! p)) e Funlike-Assembly-hom : Funlike (Assembly-hom X Y) ⌞ X ⌟ λ _ → ⌞ Y ⌟ Funlike-Assembly-hom = record { _·_ = Assembly-hom.map } {-# DISPLAY Assembly-hom.map f x = f · x #-} -- Helper record for constructing an assembly map when the realiser is -- known/does not depend on other truncated data. record make-assembly-hom {𝔸 : PCA ℓA} (X : Assembly 𝔸 ℓ) (Y : Assembly 𝔸 ℓ') : Type (ℓA ⊔ ℓ ⊔ ℓ') where open Realisability.PCA.Sugar 𝔸 using (_⋆_) field map : ⌞ X ⌟ → ⌞ Y ⌟ realiser : ↯⁺ 𝔸 tracks : {x : ⌞ X ⌟} {a : ↯ ⌞ 𝔸 ⌟} (ah : [ X ] a ⊩ x) → [ Y ] realiser ⋆ a ⊩ map x open Assembly-hom public to-assembly-hom : ∀ {𝔸 : PCA ℓA} {X : Assembly 𝔸 ℓ} {Y : Assembly 𝔸 ℓ'} → make-assembly-hom X Y → Assembly-hom X Y {-# INLINE to-assembly-hom #-} to-assembly-hom f = record { make-assembly-hom f using (map) ; tracked = inc record { make-assembly-hom f } } module _ (𝔸 : PCA ℓA) where open Realisability.Base 𝔸 open Realisability.PCA.Sugar 𝔸 open Realisability.Data.Bool 𝔸 open Assembly-hom open Precategory
Assemblies : ∀ ℓ → Precategory (lsuc ℓ ⊔ ℓA) (ℓA ⊔ ℓ) Assemblies ℓ .Ob = Assembly 𝔸 ℓ Assemblies ℓ .Hom = Assembly-hom Assemblies ℓ .Hom-set x y = hlevel 2 Assemblies ℓ .id = record where map x = x tracked = inc id⊢ Assemblies ℓ ._∘_ f g = record where map x = f · (g · x) tracked = ⦇ f .tracked ∘⊢ g .tracked ⦈ Assemblies ℓ .idr f = ext λ _ → refl Assemblies ℓ .idl f = ext λ _ → refl Assemblies ℓ .assoc f g h = ext λ _ → refl
Unlike most other categories constructed on the 1Lab, the category of assemblies is not univalent; see univalence of categories of assemblies.
The assembly of booleans🔗
The assembly of booleans, is the simplest example of an assembly which contains actual computability data. Its construction is entirely straightforward:
𝟚 : Assembly 𝔸 lzero 𝟚 .Ob = Bool 𝟚 .has-is-set = hlevel 2 𝟚 .realisers true = singleton⁺ `true 𝟚 .realisers false = singleton⁺ `false 𝟚 .realised true = inc (`true .fst , inc refl) 𝟚 .realised false = inc (`false .fst , inc refl)
We define the realisability relation as a function from Bool
, by cases: the only
option for realising the boolean true
is with the `true
program, and
similarly the false
boolean is realised
by the `false
program.
Indiscrete assemblies🔗
However, the assembly of booleans is not the only assembly we can construct on the type of booleans. As mentioned above, we could also have inverted which program realises each boolean; but this is still an assembly with nontrivial computability data. Now, we show that the “ambient” world of sets and functions embeds fully faithful into the category of assemblies over any pca
This is, perhaps, a bit surprising: maps of assemblies are computable by definition, but arbitrary functions between sets need not be! The catch is that, when equipping a set with the structure of an assembly, we get to choose which programs compute which elements; and, above, we have made a sensible choice. But we can always make an adversarial choice, letting every program at all realise any element.
We denote the indiscrete assembly on a set as following the literature. Note however that Bauer (2022) refers to these as constant assemblies, while de Jong (2024) does not assign them a name but merely singles them out as embedding classical logic in
∇ : ∀ {ℓ} (X : Type ℓ) ⦃ _ : H-Level X 2 ⦄ → Assembly 𝔸 ℓ ∇ X .Ob = X ∇ X .has-is-set = hlevel 2 ∇ X .realisers x = defineds ∇ X .realised x = inc (expr ⟨ x ⟩ x , abs↓ _ _)
The important thing to know about these is that any function of sets extends to a computable map of assemblies — this is because the only requirement for is that is defined, and assemblies are defined so that if then is defined.
extend : ∀ {ℓ ℓ'} {X : Assembly 𝔸 ℓ} {Y : Type ℓ'} ⦃ _ : H-Level Y 2 ⦄ → (⌞ X ⌟ → Y) → Assembly-hom X (∇ Y) extend {X = X} f = to-assembly-hom record where map x = f x realiser = val ⟨ x ⟩ x tracks ha = subst ⌞_⌟ (sym (abs-β _ [] (_ , X .def ha))) (X .def ha)
Following the general logic of adjoint functors, this means that is a functor for any at all — and moreover that is a right adjoint to the functor which projects the underlying set of each assembly.
Cofree : Functor (Sets ℓ) (Assemblies ℓ) Cofree .F₀ X = ∇ ⌞ X ⌟ Cofree .F₁ f = extend f Cofree .F-id = ext λ _ → refl Cofree .F-∘ f g = ext λ _ → refl Forget : Functor (Assemblies ℓ) (Sets ℓ) Forget .F₀ X = el! ⌞ X ⌟ Forget .F₁ f = f ·_ Forget .F-id = refl Forget .F-∘ f g = refl Forget⊣∇ : Forget {ℓ} ⊣ Cofree Forget⊣∇ .unit .η X = extend λ x → x Forget⊣∇ .unit .is-natural x y f = ext λ _ → refl Forget⊣∇ .counit .η X a = a Forget⊣∇ .counit .is-natural x y f = refl Forget⊣∇ .zig = refl Forget⊣∇ .zag = ext λ _ → refl
The indiscrete assemblies are generally poor as domains for computable functions, since a realiser for would have to choose realisers for given no information about Indeed, we can show that if there are non-constant maps then is trivial.
non-constant-nabla-map : (f : Assembly-hom (∇ Bool) 𝟚) → f · true ≠ f · false → is-trivial-pca 𝔸 non-constant-nabla-map f x = case f .tracked of λ where record { realiser = (fp , f↓) ; tracks = t } → let a = t {true} {`true .fst} (`true .snd) b = t {false} {`true .fst} (`true .snd) cases : ∀ b b' (x : ↯ ⌞ 𝔸 ⌟) → [ 𝟚 ] x ⊩ b → [ 𝟚 ] x ⊩ b' → b ≠ b' → `true .fst ≡ `false .fst cases = λ where true true p → rec! λ rb rb' t≠t → absurd (t≠t refl) true false p → rec! λ rb rb' _ → rb ∙ sym rb' false true p → rec! λ rb rb' _ → rb' ∙ sym rb false false p → rec! λ rb rb' f≠f → absurd (f≠f refl) in cases (f · true) (f · false) _ a b x
The assembly
𝟚
and its “flipped” variant obtained by swapping which boolean program realises each boolean value are isomorphic (even identical) in the category of assemblies, since the`not
program is a computable involution.They are only distinct if considered as “assemblies over
Bool
”, where (following the logic of vertical maps) we restrict our attention to the isomorphisms with underlying function the identity.↩︎
References
- Bauer, Andrej. 2022. “Notes on Realizability.” Midlands Graduate School. https://github.com/andrejbauer/notes-on-realizability.
- de Jong, Tom. 2024. “Categorical Realizability.” Midlands Graduate School. https://github.com/tomdjong/MGS-categorical-realizability.