© 2021 Steven Obua

Practical Types

by Steven Obua
Cite as: https://doi.org/10.47757/practical.types.1
July 29th, 2021
Abstract
These are preliminary notes about Practal’s logic and its system of practical types. This is ongoing research, and I expect updates to this document over time. Practal Light is a research prototype implementing (part of) what is presented in this document.

Introduction

I have outlined in the initial design document for Practal [1] how I expect types to work in Practal. The basic idea is that they should be similar to sets; but unlike sets, they should support data abstraction.
Types in Practal are designed to be extensional. That is, two types are the same if they have the same members. This is the same as in set theory. But unlike in standard set theory, the members of a type do not need to be sets or types themselves, and do not expose structure beyond what is required to make them proper citizens of their type.
Subtyping can be seen as a direct consequence of type extensionality. We write x : A to denote that x has type A. In this case we also say that x is a member of A. We say that A is a subtype of B, in symbols A ⊆ B, if for all members x of A we also have that x is a member of B. That means that for all types A and B the following is a theorem:
(A ⊆ B) = (∀ x : A. x : B)
Equality between two types A and B is thus characterized by
(A = B) = (A ⊆ B ∧ B ⊆ A)
An often posed question is, what is wrong with coercions [2]? Why not just use them instead of subtyping? There is a simple argument for subtyping, and another simple argument against coercions:
The system of practical types I propose for Practal is not a type system in the conventional sense. In fact, there are no illtyped terms in Practal, just illformed terms and wellformed terms. All wellformed terms have a type, although you might not be able to determine or prove what it is.

Practal Light

Practal’s logic is new territory for me, and Practal Light is an ITP prototype I am implementing in Swift to test ideas. I will use source code excerpts from Practal Light to introduce and explain concepts. The full source code of Practal Light is available at https://github.com/practal/practal-light under an MIT license.

Terms

An important choice is to decide what the terms of Practal’s logic look like. I have finally arrived at the following design I am very pleased with:
enum Term {
    case variable(Var, params: [Term])
    case constant(Const, binders: [Var], params: [Term])
}
Here Var represents variable names, and Const constant names. A Term is either
For example, a lambda term is represented as
.constant("abs", binders: ["x"], 
  params: [.variable("T", params: []), .variable("P", params: ["x"])])
We use the more concise abstract syntax (abs x. T P[x]) instead, or rather the concrete syntax λ x : T. P[x].
Every term, type or formula is represented in Practal Light via this minimalistic Term data structure. I call this way of representing terms first-order abstract syntax.
A term is wellformed if the following conditions are true:
A wellformedness check has been implemented in Practal Light and can be examined for more detailed information.
The following code in Practal Light introduces all primitive constants that all other constants will be defined in terms of:
introduce("(eq. A B)", syntax: "A = B", priority: REL_PRIO)
introduce("(abs x. T B[x])", syntax: "λ x : T. `B", priority: BINDER_PRIO)
introduce("(app. A B)", syntax: "`A B", priority: APP_PRIO)
introduce("(in. A T)", syntax: "A : T", priority: REL_PRIO)
introduce("(all x. P[x])", syntax: "∀ x. `P", priority: BINDER_PRIO)
introduce("(choose x. P[x])", syntax: "ε x. `P", priority: BINDER_PRIO)
introduce("(imp. A B)", syntax: "A ⟶ `B", priority: LOGIC_PRIO + IMP_RPRIO)
introduce("(false.)", syntax: "⊥")
introduce("(Prop.)", syntax: "ℙ")
introduce("(Fun. U V)", syntax: "U → `V", priority: TYPE_PRIO + FUN_RPRIO)
introduce("(Pred x. T P[x])", syntax: "{ x : T | P }")
introduce("(Type. i)", syntax: "Type i", priority: TYPE_PRIO + TYPE_RPRIO)
introduce("(Union i. I T[i])", syntax: "⋃ i : I. `T", priority: TYPE_PRIO + UNION_RPRIO)
introduce("(Nat.)", syntax: "ℕ")
introduce("(Nat-zero.)", syntax: "0")
introduce("(Nat-succ.)", syntax: "succ")
The constants are introduced by specifying their abstract and their concrete syntax. The concrete syntax can have optional priority and associativity annotations. From these, a pyramid grammar [3] is constructed for parsing both abstract and concrete syntax simultaneously. As an example for how this works, consider the case
theory.introduce("(app. A B)", syntax: "`A B", priority: APP_PRIO)
The priority is given as a floating point value APP_PRIO. This means that the expression A B is understood to be an expression of that priority. Without any further annotation the default assumption is that A and B are themselves expressions of the next-higher priority compared to APP_PRIO. But that would rule out an expression like A B C. Therefore, the actual syntax given is
`A B
where the tick in front of A signifies that A has priority APP_PRIO as well. Therefore the expression A B C is parsed as (A B) C. Note that A (B C) is ruled out, because B C has priority APP_PRIO, but is required to have a priority just higher than APP_PRIO. So effectively the tick in front of A specifies left associativity.
The following table summarizes the introduced primitive constants: conceptconcrete syntaxabstract syntax \begin{array}{c||c|c} \text{concept} & \text{concrete syntax} & \text{abstract syntax} \\ \hline {\includegraphics[height=0.5em]{99E664F1-8B39-489B-AD2E-72F9A2316C0E}} & {\includegraphics[height=0.5em]{EBCD72E6-ED03-4497-8D42-8378DE40BB66}} & {\includegraphics[height=0.5em]{E1A08CC5-AAEE-468A-9D3F-136D58286195}} \\ {\includegraphics[height=0.5em]{ACCC8DCB-750D-4ECF-8659-5B1791DFEEFD}} & {\includegraphics[height=0.5em]{E45FA981-BBF4-4BDF-BC18-F041CCE67D64}} & {\includegraphics[height=0.5em]{8EE5CE7B-3D27-4461-A71D-50123F64A066}} \\ {\includegraphics[height=0.5em]{04D58D2C-FE64-4A9A-AEEF-7B091E259F61}} & {\includegraphics[height=0.5em]{8C039C9C-2A27-4159-8299-7597CB200453}} & {\includegraphics[height=0.5em]{28E19486-2682-444B-8F5B-1744A1FFB4C0}} \\ {\includegraphics[height=0.5em]{E0284385-9EAB-42FB-8948-D84E1A148B41}} & {\includegraphics[height=0.5em]{89CAA755-B814-4657-8FB3-835EA3D487DF}} & {\includegraphics[height=0.5em]{91FA5207-6504-44C9-8B67-EFB9F07573CF}} \\ {\includegraphics[height=0.5em]{5D2ECEA9-0512-4304-BC5A-2FE24AE46931}} & {\includegraphics[height=0.5em]{E6B2EBE2-7817-4ED8-9339-A4A189B83042}} & {\includegraphics[height=0.5em]{4FA836FD-3761-48CD-B3DA-2F9F4F19762E}} \\ {\includegraphics[height=0.5em]{B85C98F4-CA42-4A48-BAD7-1F92047B9644}} & {\includegraphics[height=0.5em]{AECF8373-CC04-47C8-B6FD-5F40A475943B}} & {\includegraphics[height=0.5em]{33AAA67E-49BC-42E2-876E-642B49BF4413}} \\ {\includegraphics[height=0.5em]{D867B758-D2E8-4F3C-BFC3-801300123E6F}} & {\includegraphics[height=0.5em]{14F35DF0-BEA7-4BE6-8B85-6D2E97058D21}} & {\includegraphics[height=0.5em]{E7EE50CD-1E6C-430E-A7BF-887A7D0F9919}} \\ \hline {\includegraphics[height=0.5em]{EB73FB5F-E85D-44F9-B5FE-DF752F411DD7}} & {\includegraphics[height=0.5em]{86D887C6-DCF8-4DE0-9A07-0B3E7D55D664}} & {\includegraphics[height=0.5em]{2D1D7256-4269-4391-B4B5-82D9DEFEDCF4}} \\ {\includegraphics[height=0.5em]{699835B7-299A-4D12-AF3B-753CF9A5D6E2}} & {\includegraphics[height=0.5em]{B8FEAA1B-CF73-44AB-B485-B1A3A9820554}} & {\includegraphics[height=0.5em]{5300406F-E4AC-4AF2-AD56-56A600447B6E}} \\ {\includegraphics[height=0.5em]{EFD098BC-7A77-42D4-8475-84457BFDD7F5}} & {\includegraphics[height=0.5em]{BDFD5FFF-84B9-46DE-B35C-F4427518D849}} & {\includegraphics[height=0.5em]{F496C4FB-9193-4208-8720-0F9EC3B687C6}} \\ \hline {\includegraphics[height=0.5em]{16EE2C3C-7EDF-4C4E-8771-8CFBB1763063}} & {\includegraphics[height=0.5em]{259198FC-5CD6-4527-81E8-2045C394CF5E}} & {\includegraphics[height=0.5em]{4860C48B-9662-4A56-B52E-70184DCECCFE}} \\ {\includegraphics[height=0.5em]{C14B0679-2B2B-4AAC-87E1-3CC967348E2A}} & {\includegraphics[height=0.5em]{9E9E1C5D-6E32-4D37-A17E-4F0F6A210494}} & {\includegraphics[height=0.5em]{E663CA89-AFA1-48CB-8FEE-6CE5C9B251C7}} \\ {\includegraphics[height=0.5em]{E4749CC0-B993-4E6E-96C7-62FD4C5054FE}} & {\includegraphics[height=0.5em]{B63E2A62-225E-4D76-8195-374A43E70411}} & {\includegraphics[height=0.5em]{B2E478E2-AD3B-461B-BA2E-9C4580BCA199}} \\ \hline {\includegraphics[height=0.5em]{3B6511E7-C292-4D06-A071-330A828E5C63}} & {\includegraphics[height=0.5em]{A7E4ADD7-E26F-451F-BDC1-4F0A6C01B576}} & {\includegraphics[height=0.5em]{21E22E7A-03D5-4154-B45B-A0B69B095B0B}} \\ {\includegraphics[height=0.5em]{FA66B003-31A3-488E-BE11-ED01D994777A}} & {\includegraphics[height=0.5em]{EAC8FB29-0A82-4924-8D8C-7C7D784C2816}} & {\includegraphics[height=0.5em]{F9AC6294-8D11-43A2-BB8C-BE1530BC9E20}} \\ {\includegraphics[height=0.5em]{907BB0E6-3A6D-4BB5-ADD5-4C68FCDE45A8}} & {\includegraphics[height=0.5em]{8654272C-DD83-4657-9DB7-1B8D57DF279C}} & {\includegraphics[height=0.5em]{4D13B433-DEE3-4BCA-802B-66181979EA69}} \\ \end{array}

Theorems

Practal Light has no theorems or rules of inference implemented yet. There will be basic axioms like
⊢ a = a
and
⊢ (a = b) : ℙ
and primitive inference rules like modus ponens, α\alpha-equivalence, β\beta-reduction, and instantiation of free variables.
Note that practical types are completely governed by theorems, i.e. a typing judgement is a normal theorem. That means that initially, type checking will possibly be harder compared to other ITP systems, as there is no built-in automation for proving typing judgements. But it also means that practical types have unparalleled flexibility. Furthermore, as the general support for automation in Practal gradually increases, this wave of automation will also lift type checking.

Definitions

Practal Light follows the LCF approach [4]. After initial primitive constants and axioms have been introduced, all other theory development occurs by consistency-preserving definitions only.
One basic definition mechanism has already been implemented in Practal Light: Given two terms lhs and rhs, where lhs describes the abstract syntax of the new constant to be defined, and rhs is the right hand side of the defining equality, a new constant is defined if certain side-conditions are met, such as the wellformedness of rhs, and that all free variables of rhs are among the free variables of lhs.
Using this definition mechanism, we can already define many important derived notions:
define("(not. P)", "P ⟶ ⊥", syntax: "¬`P", priority: LOGIC_PRIO + NOT_RPRIO)
define("(true.)", "¬ ⊥", syntax: "⊤")
define("(or. P Q)", "¬P ⟶ Q", syntax: "`P ∨ Q", priority: LOGIC_PRIO + OR_RPRIO)
define("(and. P Q)", "¬(¬P ∨ ¬Q)", syntax: "`P ∧ Q", priority: LOGIC_PRIO + AND_RPRIO)
define("(equiv. P Q)", "(P ⟶ Q) ∧ (Q ⟶ P)", syntax: "P ≡ Q", priority: REL_PRIO)
define("(neq. P Q)", "¬(P = Q)", syntax: "P ≠ Q", priority: REL_PRIO)
define("(ex x. P[x])", "¬(∀ x. ¬P[x])", syntax: "∃ x. `P", priority: BINDER_PRIO)
define("(all-in x. T P[x])", "∀ x. x : T ⟶ P[x]", syntax: "∀ x : T. `P", priority: BINDER_PRIO)
define("(ex-in x. T P[x])", "∃ x. x : T ∧ P[x]", syntax: "∃ x : T. `P", priority: BINDER_PRIO)
CONTROL_PRIO)
The above definitions are summarized in the following table: conceptconcrete syntaxlhsrhs \begin{array}{c||c|c|c} \text{concept} & \text{concrete syntax} & \text{lhs} & \text{rhs}\\ \hline {\includegraphics[height=0.5em]{A2D94491-71C2-4E2D-BF90-5FE5C5A9919E}} & {\includegraphics[height=0.5em]{4834B540-272C-49F0-9731-AE2CDBE3F0A8}} & {\includegraphics[height=0.5em]{8CC428B3-9963-488C-8CF5-14337312F0EB}} & {\includegraphics[height=0.5em]{6B3232A2-D28D-4756-8A53-541FDB480771}} \\ {\includegraphics[height=0.5em]{A7060E82-AF4D-4EC7-BCDD-B2F77A492467}} & {\includegraphics[height=0.5em]{6AA8932F-A648-40BF-A56E-3713366161BC}} & {\includegraphics[height=0.5em]{45BFA1E3-7431-4A71-B9C6-8598AD6043E3}} & {\includegraphics[height=0.5em]{A1733ED3-C7C7-4A8B-8E2F-36176B4767AE}} \\ {\includegraphics[height=0.5em]{D16309C7-3B57-4C26-B128-36CA292BF598}} & {\includegraphics[height=0.5em]{EF28DC43-9BE5-450F-B665-0DA04E87A457}} & {\includegraphics[height=0.5em]{325804B9-39E5-4B1E-9AE3-160B4D9D433A}} & {\includegraphics[height=0.5em]{CED3DD9F-BB63-4F0C-8720-D36B4611CD02}} \\ {\includegraphics[height=0.5em]{712567D6-5945-4472-BD3A-CA07E02E3A22}} & {\includegraphics[height=0.5em]{BD26F643-5EDE-4017-ADB9-F16E97336B27}} & {\includegraphics[height=0.5em]{FCDC70E6-F0CC-47BD-AE8D-CC50EDD254C2}} & {\includegraphics[height=0.5em]{766DBDD0-E8D7-41DE-8965-A3DAEA71E941}} \\ {\includegraphics[height=0.5em]{9868A034-C5E4-41F6-9017-622D6D38033B}} & {\includegraphics[height=0.5em]{32966C4A-935D-4BE4-A457-51660F427611}} & {\includegraphics[height=0.5em]{348E0C84-EC5E-43F3-975E-4237A24DBB68}} & {\includegraphics[height=0.5em]{B9CE783B-F988-4F52-9533-71CB48881718}} \\ {\includegraphics[height=0.5em]{42A0454C-BB31-4033-8EC4-E7146F406F31}} & {\includegraphics[height=0.5em]{2F2A7DF4-43F3-4EBB-8D05-BE9FA29EF9DB}} & {\includegraphics[height=0.5em]{AA5645D0-332F-48E0-88F1-70A231567BB2}} & {\includegraphics[height=0.5em]{2C395A44-9442-4D59-BA0F-4476D211DA32}} \\ {\includegraphics[height=0.5em]{22BD2B8A-6133-4F58-982F-5455E964184F}} & {\includegraphics[height=0.5em]{FB932EAC-CCDE-45FE-A2FC-2C382213B8DB}} & {\includegraphics[height=0.5em]{61F9A6B1-94F2-40EE-9F59-5A7E696852A0}} & {\includegraphics[height=0.5em]{4E67A8F6-6DC9-4E4C-858F-4A4250F78896}} \\ {\includegraphics[height=0.5em]{42E8F938-9A78-4A47-A19A-A64F798480AA}} & {\includegraphics[height=0.5em]{DB96758E-2EBD-43FF-BEB2-251AFE268535}} & {\includegraphics[height=0.5em]{AD94F892-608D-4666-B38C-CAC65284A44A}} & {\includegraphics[height=0.5em]{F5077D76-B8EA-4922-B5FD-C7C1DE13731B}} \\ {\includegraphics[height=0.5em]{A22A5118-CE75-488B-823E-63C747A0F1B7}} & {\includegraphics[height=0.5em]{CA935144-541E-4645-B675-296D90AA8C92}} & {\includegraphics[height=0.5em]{23477AD4-DD6A-4210-8D94-ADC3C552E83F}} & {\includegraphics[height=0.5em]{08D1381D-3A5E-4385-8E2D-A1D22A081632}} \\ \end{array}
In the following we write definitions in an abbreviated notation, for example:
all-in: ⊢ ∀ x : T. P[x] ≝ ∀ x. x : T ⟶ P[x]

Practical Types

The following is a blueprint for the development of practical types in Practal Light. For each type, the following questions need to be answered:

The Type of Propositions

Practal uses classical logic, and therefore the type of propositions consists of exactly two elements, , which denotes truth, and , which denotes falsity:
⊢ ∀ p : ℙ. p = ⊥ ∨ p = ⊤ 
⊢ ⊤ ≠ ⊥

The Empty Type

The empty type is the simplest possible type, as it has no members at all. It is characterized by
⊢ ∀ x : ∅. ⊥
We can actually define the empty type using predicate subtypes as
Empty: ⊢ ∅ ≝ { p : ℙ | ⊥ }

Types of Functions

Given two types A and B, the type of total functions with domain A and codomain B is denoted by A → B. Therefore the following axiom holds:
⊢ f : A → B ⟶ (∀ a : A. f x : B)
A function is only equal to other functions. Furthermore for a function f : A → B and g : C → D to be equal, they need to be defined on the same domain, i.e. A = C.
⊢ ∀ f : A → B. ∀ g : C → D. (f = g) = (A = C ∧ (∀ x : A. f x = g x))
It should therefore be possible to prove the following subtype relationship between function types for types A, B, C and D:
(A → B ⊆ C → D) = (A = C ∧ (A = ∅ ∨ B ⊆ D))
We will define the meaning of itself later .
We can define in our logic the property of being a function, in code:
define("(is-Fun. f)", "∃ U. ∃ V. f : U → V", syntax: "f : Fun", priority: REL_PRIO)
In our abbreviated notation this reads
is-Fun: ⊢ f : Fun ≝ ∃ U. ∃ V. f : U → V 
Note that Fun is not a type. It is just notation that allows us to pretend so in many situtations. We can extend notation for universal and existential quantification accordingly:
all-fun: ⊢ ∀ f : Fun. P[f] ≝ ∀ f. f : Fun ⟶ P[f]
ex-fun: ⊢ ∃ f : Fun. P[f] ≝ ∃ f. f : Fun ∧ P[f]

The Nil Type

Note that for any type A ≠ ∅ we have
(A → ∅) = ∅
On the other hand, for any type B, we have
(∅ → B) = (∅ → ∅)
We therefore define
Nil: ⊢ Nil ≝ ∅ → ∅
The type Nil has exactly one element, namely the unique function nil : Nil with empty domain:
nil: ⊢ nil ≝ λ x : ∅. ⊥
We use nil to deal with undefinedness. In particular, we postulate the following axiom:
⊢ f : A → B ⟶ ¬ (x : A) ⟶ f x = nil
In other words, if we apply a function to an argument outside of its domain, the result is nil. Because nil itself is a function with empty domain, we also have
⊢ nil x = nil
What does A B mean if A is not a function? The following axiom handles this case:
⊢ ¬(A : Fun) ⟶ A B = nil
Furthermore, we introduce the following notions that simplify working with undefinedness:
defined: ⊢ x↓ ≝ x ≠ nil
undefined: ⊢ x↑ ≝ x = nil
defined-eq: ⊢ x =↓ y ≝ (x↓ ∨ y↓) ∧ x = y
undefined-eq: ⊢ x =↑ y ≝ x↑ ∨ y↑ ∨ x = y  
The Hilbert choice operator obeys the following axioms:
⊢ (∃ x. P[x]) ⟶ P[ε x. P[x]]
⊢ ¬(∃ x. P[x]) ⟶ (ε x. P[x]) = nil

Predicate Subtypes

The expression
{ a : A | P[a] }
denotes the subtype of A consisting of exactly those elements a : A for which P[a] is true.
Consequently, we postulate the following axiom:
⊢ (a : { a : A | P[a] }) = (a : A ∧ P[a])
Of course, for any type A this implies
{ a : A | P[a] } ⊆ A

The Type of Natural Numbers

We also postulate the existence of a type which represents the natural numbers. We assume the Peano axioms, adapted to our setting:
⊢ 0 : ℕ
⊢ succ : ℕ → ℕ
⊢ succ n ≠ 0
⊢ succ(m) =↓ succ(n) ⟶ m = n
⊢ 0 : N ⟶ (∀ n : N. succ n : N) ⟶ ℕ ⊆ N

Types of Types

Given a type A, what type does A have? It would be convenient if there could be a single type Type of types, but using Russell’s paradox it is easy to see that this leads to inconsistency. Because let the type R be defined via
R = { T : Type | ¬(T : T) }
Assuming R : R, it follows that ¬(R : R). On the other hand, assuming ¬(R : R) implies R : R. Thus (R : R) = ¬(R : R), an inconsistency.
We take the same way out of this dilemma as Lean does: There is not just a single type Type of types, but an infinite family Type i of types of types such that
(Type 0 : Type 1) ∧ (Type 1 : Type 2) ∧ (Type 2 : Type 3) ∧ ...   
We furthermore require a nesting of types such that
(Type 0 ⊆ Type 1) ∧ (Type 1 ⊆ Type 2) ∧ (Type 2 ⊆ Type 3) ∧ ...   
We use 𝕋 as an abbreviation for Type 0:
Type-0: ⊢ 𝕋 ≝ Type 0  
Membership to Type i is governed by the following axioms:
⊢ ℙ : 𝕋
⊢ ℕ : 𝕋
⊢ A : Type i ⟶ B : Type i ⟶ A → B : Type i
⊢ A : Type i ⟶ P : A → ℙ ⟶ { a : A | P a } : Type i 
⊢ i : ℕ ⟶ Type i : Type (succ i)
⊢ i : ℕ ⟶ Type i ⊆ Type (succ i)
Let’s try to apply Russell’s paradox again, and form for example
S = { T : 𝕋 | ¬(T : T) }
From S : S we can still derive ¬(S : S). But the other direction doesn’t work anymore, because the assumption ¬ (S : S) is not enough to prove S : S. We also need to show S : 𝕋, but we can only prove S : Type 1. So there is no paradox here anymore, just a proof of ¬ (S : S).
Although there is no universal type of types, we can nevertheless introduce notation that emulates it in certain situations, just like we did for Fun:
is-Type: ⊢ T : Type ≝ ∃ i. T : Type i
all-type: ⊢ ∀ T : Type. P[T] ≝ ∀ T. T : Type ⟶ P[T]
ex-type: ⊢ ∃ T : Type. P[T] ≝ ∃ T. T : Type ∧ P[T]
We can now give the definition of :
sub-type: ⊢ U ⊆ V ≝ U : Type ∧ V : Type ∧ (∀ u : U. u : V)

Singleton Types

Given x : T we could define the singleton type { x } as
{ x } = { y : T | y = x }
But what if we don’t know which type x has? Could it be that x has no type? That doesn’t seem to make sense, so we introduce the following axiom:
⊢ ¬ (x : Type) ⟶ (∃ T : 𝕋. x : T)
This makes sure that x : { x } actually holds, where we define { x } by
Singleton: ⊢ { x } ≝ { y : (ε T. x : T) | x = y }

Union of Types

We want the expression
⋃ a : A. T[a]
to denote the type formed by the union of T[a] over all a : A.
But this is a powerful operation, and we need to be careful for it not to lead to inconsistency. For example, if we allow
⋃ i : ℕ. Type i
then we have effectively constructed our type of all types, which we know leads to inconsistency.
We try to tame the union operator by defining it only when all T[a] are members of Type i for some fixed i. This leads to the following axioms:
⊢ (∀ a : A. T[a] : Type i) ⟶ (x : ⋃ a : A. T[a]) = (∃ a : A. x : T[a])
⊢ (∀ a : A. T[a] : Type i) ⟶ (⋃ a : A. T[a]) : Type i
⊢ ¬(∃ i : ℕ. ∀ a : A. T[a] : Type i) ⟶ (⋃ a : A. T[a]) = nil
This still allows us to construct huge types like
⋃ T : 𝕋. T

The Intersection Type

We define the intersection of types by
Intersection: ⊢ ⋂ a : A. T[a] ≝ { x : ⋃ a : A. T[a] | ∀ a : A. x : T[a] }

The Binary Union and Intersection Types

We define A ∪ B and A ∩ B via
Binary-Union: ⊢ A ∪ B ≝ ⋃ p : ℙ. (if p then A else B) 
Binary-Intersection: ⊢ A ∩ B ≝ ⋂ p : ℙ. (if p then A else B) 
The if-then-else construct is defined by
if-then-else: ⊢ if p then A else B ≝ ε x. (p ⟶ x = A) ∧ (¬ p ⟶ x = B)

Function Abstraction

The lambda expression
λ x : T. B[x]
is only defined if its type
T → ⋃ x : T. {B[x]}
is defined:
⊢ T → (⋃ x : T. {B[x]}) ↓ ⟶ (λ x : T. B[x]) : T → (⋃ x : T. {B[x]})
⊢ T → (⋃ x : T. {B[x]}) ↑ ⟶ (λ x : T. B[x]) = nil
Application of a lambda expression to an argument is defined accordingly:
⊢ T → (⋃ x : T. {B[x]}) ↓ ⟶ x : T ⟶ (λ x : T. B[x]) x = B[x]

The Type of Pairs

There is no mechanism for defining new types implemented yet in Practal Light. Let us assume it would allow a type definition of pairs somewhat like this:
typedef Pair: A ⨯ B ⇄ { f : ℙ → A ∪ B | f ⊥ : A ∧ f ⊤ : B }
A member e of A ⨯ B is written (a, b) if it is represented by an f such that f ⊥ = a and f ⊤ = b. In this case we also define fst e = a, and snd e = b.
Why do we need a new type definition mechanism? Could we not just define it as:
Pair: ⊢ A ⨯ B ≝ { f : ℙ → A ∪ B | f ⊥ : A ∧ f ⊤ : B }
That would certainly be possible, but then A ⨯ B would consist of functions. We want pairs not to be equal to anything than other pairs, and thus the new mechanism is needed.

Dependent Functions and Pairs

The dependent function type [a : A] → B[a] is defined as
D-Fun: ⊢ [a : A] → B[a] ≝ { f : A → (⋃ a : A. B[a]) | ∀ a : A. f a : B[a] }
Dependent pair types are defined likewise:
D-Pair: ⊢ [a : A] ⨯ B[a] ≝ { p : A ⨯ (⋃ a : A. B[a]) | snd p : B[fst p] }

Cleaning Up

There have been a few assumptions left unsaid and implicit, and it is now time to spell them out explicitly.
Propositions, natural numbers, functions, pairs and types are disjoint:
⊢ ℙ ∩ ℕ = ∅
⊢ ℙ ∩ (A → B) =↑ ∅
⊢ ℙ ∩ (A ⨯ B) =↑ ∅
⊢ ℙ ∩ Type i =↑ ∅
⊢ ℕ ∩ (A → B) =↑ ∅
⊢ ℕ ∩ (A ⨯ B) =↑ ∅
⊢ ℕ ∩ Type i =↑ ∅
⊢ (A → B) ∩ (A ⨯ B) =↑ ∅
⊢ (A → B) ∩ Type i =↑ ∅
⊢ (A ⨯ B) ∩ Type i =↑ ∅
The =↑ operator is used instead of just = because operands might be undefined:
⊢ ¬ (A : Type) ∨ ¬ (B : Type) ⟶ (A → B) = nil 
⊢ ¬ (A : Type) ∨ ¬ (B : Type) ⟶ (A ⨯ B) = nil 
⊢ ¬ (i : ℕ) ⟶ Type i = nil
Similarly, we need to be clear for all remaining primitive constants when they are defined or otherwise undefined. This will then ripple through derived notions, often without any further need to specify definedness / undefinedness.
⊢ ¬ (T : Type) ⟶ (x : T) = ⊥
⊢ ¬ (T : Type) ⟶ { x : T | P[x] } = nil
Logical operators are defined on ℙ ∪ Nil, and the nil value is taken to mean false:
¬ (P : ℙ ∪ Nil) ⊢ (¬ P)↑  
¬ (P : ℙ ∪ Nil) ⊢ (P ⟶ Q)↑  
¬ (Q : ℙ ∪ Nil) ⊢ (P ⟶ Q)↑  
⊢ ¬ nil
⊢ (¬ ⊤) = ⊥
P : ℙ ∪ Nil ⊢ nil ⟶ A 
P : ℙ ∪ Nil ⊢ ⊥ ⟶ A
⊢ ⊤ ⟶ ⊤
⊢ (⊤ ⟶ ⊥) = ⊥
⊢ (⊤ ⟶ nil) = ⊥
Universal quantification ∀ x. P[x] is defined as follows:

Conclusion

What just happened here? I am actually not quite sure. I really see only two possibilities:
Maybe you have an opinion which one of the two it is, or want to offer a third option? If you have any comments, questions, and/or want to share your ideas for Practal, please visit Practal’s discussion forum!

References

[1]Steven Obua. (2021). Practal — Practical Logic: A Bicycle for Your Mathematical Mind, https://doi.org/10.47757/practal.1.
[2]Z. Luo, S. Soloviev, T. Xue. (2013). Coercive Subtyping: Theory and Implementation, https://doi.org/10.1016/j.ic.2012.10.020.
[3]Steven Obua. (2021). A New Semantics for Local Lexing, https://doi.org/10.47757/obua.ll.1.
[4]Lawrence C. Paulson, Tobias Nipkow, Makarius Wenzel. (2019). From LCF to Isabelle/HOL, https://doi.org/10.1007/s00165-019-00492-1.