In dit hoofdstuk wordt de werking van een programma behandeld dat een expressie ontvangt, deze analyseert, type-checking uitvoert, en uiteindelijk de semantiek van de expressie afleidt en uitvoert. Het gebruik van een formele syntaxis en type-systeem speelt een cruciale rol in dit proces. Het programma begint met het parsen van een invoerexpressie en het bouwen van een abstracte syntaxisboom. Vervolgens wordt een type-checking uitgevoerd, waarna de expressie wordt vertaald naar een semantisch gerelateerde operatie. Ten slotte wordt deze operatie geëvalueerd en de uitvoer getoond.

Het proces van parsing wordt als volgt uitgevoerd: een gegenereerde parser leest een tekstinvoer en converteert deze naar een expressie die vervolgens wordt geprint. Deze expressie wordt daarna onderworpen aan type-checking, waarbij de expressie wordt gemarkeerd met type-informatie. Dit type-informatie wordt ook geprint, waarna de semantiek van de expressie wordt bepaald door de toewijzing van een operatie, die vervolgens wordt geëvalueerd en het resultaat wordt getoond. Dit proces wordt stap voor stap behandeld in de code van het programma, waarbij er expliciet aandacht is voor mogelijke fouten zoals parsing- of type-checkingfouten, die allemaal in een exceptieblok worden opgevangen.

Na het compileren van de Java-code kan het programma gestart worden en de volgende invoer worden gegeven:

cpp
((1+1)=if (1=1) then (1+1) else 1)

De uitvoer die het programma genereert is als volgt:

ruby
((1+1)=(if (1=1) then (1+1) else 1))
((1:aexp+1:aexp):aexp=(if (1:aexp=1:aexp):bexp then (1:aexp+1:aexp):aexp else 1:aexp):aexp):bexp true

In deze uitvoer wordt eerst de tekstuele representatie van de abstracte syntaxisboom getoond, die is afgeleid van het parsen van de invoer. De daaropvolgende regels tonen de representatie van de abstracte syntaxisboom na het type-checking, waarbij alle subexpressies zijn geannoteerd met type-informatie. De laatste regel toont de waarde van de expressie, zoals bepaald door de semantiek ervan.

De sleutel tot dit proces ligt in de nauwkeurige afstemming van syntaxis, type en semantiek. Elk van deze elementen is van cruciaal belang voor het correct functioneren van een programma en de zekerheid dat het verwachte resultaat wordt verkregen.

De volgende stap in de ontwikkeling van dit systeem is het begrijpen van de fundamenten van de logica die we gebruiken om de syntaxis en semantiek van een programma te formaliseren. Hierbij wordt eerste-orde logica (predicaatlogica) geïntroduceerd als de basis voor rationele en formele redenaties in de informatica en andere wetenschappelijke disciplines. In de volgende secties van de tekst wordt eerste-orde logica als een formele taal gepresenteerd, die gebaseerd is op een abstracte syntaxis, waaruit de intuïtieve betekenis van verschillende zinnen wordt afgeleid.

Eerste-orde logica omvat termen, reeksen van termen, formules, variabelen, constante symbolen, functiewoordenschat en predicaatwoorden, die volgens een specifieke grammatica worden gevormd. Deze grammatica maakt het mogelijk om de formele structuur van uitdrukkingen die als logisch waar of onwaar kunnen worden geëvalueerd, te begrijpen.

Daarnaast worden termen en formules gecombineerd door logische operatoren zoals negatie (¬), en (∧), of (∨), implicatie (⇒), en equivalentie (⇔). Deze operatoren hebben een verschillende bindkracht, die bepaalt welke delen van een formule met elkaar gecombineerd worden. Er worden ook kwantificatoren zoals ∀ (voor alle) en ∃ (er bestaat) geïntroduceerd, die variabelen binden binnen formules, en de let ... in ... constructie, die termen introduceert.

In het systeem van eerste-orde logica worden niet alle zinnen die volgens de grammatica kunnen worden gevormd automatisch als goed gevormd beschouwd. Er is een extra regel nodig die zorgt voor de validiteit van de expressies. Dit wordt vaak gecontroleerd door een type-systeem dat de juiste toepassing van functies en predicaten op een bepaald aantal argumenten waarborgt. Een dergelijke controle kan alleen worden uitgevoerd als de ariteit van de gebruikte functies en predicaten consistent is met de grammaticale regels van de taal.

Naast de formele structuur van de taal is het ook van belang te begrijpen dat de betekenis van een expressie vaak afhankelijk is van de context waarin deze wordt gebruikt. Dit komt doordat de ariteit van functies en predicaten implicaties heeft voor de manier waarop ze worden toegepast en geïnterpreteerd in de logica.

Voor een beter begrip van hoe de logica werkt binnen een programma is het noodzakelijk de syntactische structuur van formules te visualiseren. Dit helpt om de abstracte representatie van logische formules duidelijker te maken en te zien hoe de verschillende elementen met elkaar interageren.

Bijvoorbeeld, de formule ∀𝑥. 𝑝1 (𝑥) ∨ 𝑝2 (𝑥) ⇒ ∃𝑦. 𝑞(𝑦) ∧ 𝑟(𝑓(𝑥, 𝑎), 𝑦) kan visueel worden gerepresenteerd in een abstracte syntaxisboom. Hier wordt duidelijk hoe de kwantificatoren ∀ en ∃ de variabelen binden en hoe de operatoren de formules met elkaar verbinden. Het is belangrijk om te begrijpen hoe de verschillende operatoren en kwantificatoren elkaar beïnvloeden, om de betekenis van een formule volledig te doorgronden.

Hoe invloed hebben de volgorde van kwantifiers en de betekenis van logische formules in de formele logica?

In de formele logica wordt de volgorde van kwantifiers in formules vaak als essentieel beschouwd voor het vaststellen van de betekenis en de waarheidswaarde van uitspraken. Dit kan op een zeer subtiele manier invloed hebben op de interpretatie van de logische structuur van een uitspraak, wat duidelijk wordt bij het vergelijken van twee schijnbaar vergelijkbare formules.

Bijvoorbeeld, in de formule ∀𝑥. ∃𝑦. loves(𝑥, 𝑦), wat betekent "iedereen houdt van iemand", is de interpretatie dat voor iedere persoon 𝑥 er ten minste één persoon 𝑦 is van wie 𝑥 houdt. De volgorde van de kwantifiers geeft aan dat de keuze van 𝑦 afhankelijk is van 𝑥: voor elke 𝑥 kan er een andere 𝑦 zijn.

Echter, de formule ∃𝑦. ∀𝑥. loves(𝑥, 𝑦), wat betekent "iemand wordt door iedereen geliefd", heeft een andere betekenis. Hier is er een specifiek persoon 𝑦 voor wie geldt dat iedereen van 𝑦 houdt. Deze subtiele verandering in de volgorde van de kwantifiers heeft grote gevolgen voor de betekenis van de uitspraak.

Dit voorbeeld toont aan hoe kwantifiers, zoals "voor alle" (∀) en "er bestaat" (∃), de manier beïnvloeden waarop relaties en waarheidswaarden worden gemodelleerd. Een andere manier om deze verandering te illustreren is door een matrix te gebruiken, waar de relaties tussen de personen worden weergegeven. In de eerste matrix, die de uitspraak "iedereen houdt van iemand" weerspiegelt, zien we dat in elke rij 𝑥 een "schaduw" verschijnt in de kolom van een 𝑦, wat aangeeft dat 𝑥 van iemand houdt. In de tweede matrix, die de uitspraak "iemand wordt door iedereen geliefd" weerspiegelt, is er slechts één kolom waarin iedereen een "schaduw" vertoont, wat aangeeft dat één persoon geliefd is door iedereen.

Een ander belangrijk aspect van formele logica is het gebruik van bindende variabelen. Variabelen die door kwantifiers worden gebonden, zoals 𝑥, 𝑦, 𝑢, 𝑣, dienen enkel als tijdelijke aanduidingen. De naam van de variabele heeft geen invloed op de betekenis van de formule; ze kunnen dus zonder gevolgen worden hernoemd. Dit wordt duidelijk in de formule die oorspronkelijk wordt gepresenteerd als ∀𝑢∀𝑣∃𝑤. isMother(𝑤, 𝑢) ∧ isMother(𝑤, 𝑣) ⇒ if isBrother(𝑣, 𝑢) then isMale(𝑣) else isFemale(𝑣), wat dezelfde betekenis heeft als de originele formule, ondanks dat de variabelen zijn veranderd.

Deze flexibiliteit in het benoemen van variabelen is belangrijk voor het begrijpen van de formules in formele logica, omdat het de ruimte biedt voor vereenvoudiging of herstructurering van formules zonder dat de onderliggende betekenis verandert. Het is een cruciaal concept, vooral wanneer formules complexer worden en verschillende variabelen en functies bevatten.

Een ander belangrijk punt is dat een term of formule alleen als "goed gevormd" wordt beschouwd als deze voldoet aan de ariteit van de gebruikte functies en predikaten. De ariteit is een maat voor het aantal argumenten dat een functie of predicaat vereist. Dit zorgt ervoor dat formules logisch consistent zijn en binnen de regels van de logica blijven.

Als we verder gaan met de verkenning van de proposionele logica, zien we dat deze een beperkt maar essentieel deel uitmaakt van de formele logica. Proposionele logica is niet in staat om uitspraken te doen over de waarden van domeinen, maar focust zich enkel op hoe de waarheidswaarden van samengestelde formules worden bepaald op basis van de waarheidswaarden van hun basisformules. Een voorbeeld van een eenvoudig proposioneel schema is A ∧ B, waarbij de waarheidswaarde wordt bepaald door de waarheidswaarden van A en B. Door te kijken naar alle mogelijke combinaties van de waarheidswaarden van A en B, kunnen we de waarheidstafel voor dit schema afleiden.

Een ander belangrijk concept binnen proposionele logica is de tautologie, een formule die altijd waar is, ongeacht de waarheidswaarden van de betrokken proposities. Bijvoorbeeld, de formule ¬(A ∧ B) ⇔ (¬A) ∨ (¬B) is een tautologie, omdat voor elke mogelijke combinatie van A en B de waarde van de linkerkant gelijk is aan de waarde van de rechterkant.

De formuleringen en de structuren die hier worden gepresenteerd, zijn niet slechts abstracte theorieën. Ze weerspiegelen fundamentele concepten die de basis vormen voor veel toepassingen in de formele logica, kunstmatige intelligentie en wiskundige logica. Deze logische structuren helpen ons de complexiteit van relaties en waarheidswaarden te begrijpen, en spelen een cruciale rol in het ontwikkelen van systemen die gebaseerd zijn op formele redenering en besluitvorming.

Hoe kan men de normale vormen van logische schema's construeren?

In de propositional logic is het mogelijk om elke logische uitspraak om te zetten in een disjunctieve normale vorm (DNF) of een conjunctieve normale vorm (CNF). Deze vormen hebben hun eigen toepassingen in logische bewijsvoering, optimalisatie van circuits, en meer. Het bestaan van deze vormen kan eenvoudig worden afgeleid uit waarheids-tabellen, die een fundamentele rol spelen bij de transformatie van logische formules naar hun respectieve normale vormen.

De disjunctieve normale vorm (DNF) van een formule is een disjunctie (of), die bestaat uit conjuncties (en) van letterlijk formules. Elke rijen in de waarheids-tabel van een propositional schema die als waar worden beoordeeld, draagt bij aan de opbouw van de DNF. Als bijvoorbeeld een rij de waarde “waar” heeft, worden de proposities in die rij met hun normale of genegatieve vorm gecombineerd in een conjunctie. Vervolgens worden deze conjuncties samengevoegd door disjunctie. Als voorbeeld kunnen we de volgende formule overwegen:

A(AB)A \land (A \lor B)

De waarheids-tabel voor deze formule is als volgt:

ABA ∨ BA ∧ (A ∨ B)
falsefalsefalsefalse
falsetruetruefalse
truefalsetruetrue
truetruetruetrue

We nemen de laatste twee rijen van de tabel, die de waarde “waar” opleveren, en construeren de disjunctieve normale vorm:

(A¬B)(AB)(A \land \neg B) \lor (A \land B)

Deze DNF is een disjunctie van twee conjuncties, wat de betekenis van de oorspronkelijke formule weerspiegelt.

Evenzo kan de conjunctieve normale vorm (CNF) van een formule worden afgeleid door de negatie van de formule te overwegen. We kunnen de DNF van de negatie nemen en deze omzetten in conjunctieve vorm door gebruik te maken van de De Morgan-regels. Dit is een belangrijk concept omdat het aantoont dat we de conjunctieve vorm kunnen verkrijgen door de disjunctieve normale vorm van de negatie van een formule te nemen.

Bijvoorbeeld, de formule A(AB)A \land (A \lor B) heeft de volgende negatie:

¬(A(AB))=¬A¬(AB)\neg (A \land (A \lor B)) = \neg A \lor \neg (A \lor B)

Door De Morgan’s wetten toe te passen, krijgen we:

(¬A¬A)(¬A¬B)(\neg A \lor \neg A) \land (\neg A \lor \neg B)

De conjunctieve normale vorm van de originele formule is dus:

(¬A¬B)(¬AA)(\neg A \lor \neg B) \land (\neg A \lor A)

Dit proces laat zien dat zowel de conjunctieve als disjunctieve normale vormen van een formule uit de waarheids-tabel kunnen worden afgeleid, afhankelijk van of we de waarheidswaarden van de “waar” of de “onwaar” rijen gebruiken.

Naast de constructie van normale vormen, is het belangrijk te begrijpen dat variabelen binnen een formule ofwel “gebonden” of “vrij” kunnen zijn. Gebonden variabelen zijn diegene die onder invloed staan van een kwantor, zoals ∀ (voor alle) of ∃ (er bestaat). Vrije variabelen zijn de variabelen die geen such kwantor bezitten en waarvan de waarheidswaarde afhangt van de toewijzing die wordt gegeven door de omgeving. In de context van eerste-orde logica spelen deze concepten een cruciale rol bij de interpretatie van de formule.

In een formule zoals:

xN.yx\forall x \in \mathbb{N}. y \leq x

is xx een gebonden variabele, terwijl yy een vrije variabele is, omdat de waarheid van de formule afhankelijk is van de waarde van yy, maar niet van xx, die al door de kwantor ∀ is gebonden.

Een essentieel inzicht hierbij is dat de naam van gebonden variabelen veranderd kan worden zonder de betekenis van de formule te veranderen. Vrije variabelen daarentegen moeten dezelfde naam behouden, omdat ze zichtbaar zijn vanuit de buitenwereld. Het begrijpen van deze relaties is essentieel voor het effectief werken met formules in logica en het begrijpen van de effecten van kwantoren.

Wat verder belangrijk is om op te merken, is dat formules in de formele logica altijd moeten worden geïnterpreteerd over een specifiek domein, een verzameling van waarden die aan de vrije variabelen van de formule worden toegewezen. De waarheidswaarde van een formule hangt dus af van dit domein en van de specifieke interpretaties van de constante symbolen, functie symbolen, en predicaat symbolen die in de formule voorkomen.

Bijvoorbeeld, de formule:

y.xy\forall y. x \leq y

kan verschillend worden geëvalueerd afhankelijk van het domein waarin het wordt geïnterpreteerd. In het domein van de natuurlijke getallen kan de waarheidswaarde van de formule variëren afhankelijk van de waarde van xx, terwijl het in andere domeinen zoals de verzameling van verzamelingen of de gehele getallen, een geheel andere waarheidswaarde kan opleveren.

Het begrijpen van de formalisme in proposities en de transformatiemethoden naar normale vormen is cruciaal voor het werken met formules in logica en het verder ontwikkelen van geavanceerde logische systemen, bijvoorbeeld in kunstmatige intelligentie en formele verificatie.

Hoe wordt een expressie vertaald naar een machineprogramma?

In de context van het vertalen van expressies naar machineprogramma’s, wordt elke expressie geconverteerd naar een reeks machine-instructies die de uitdrukking stap voor stap uitvoeren. Dit proces vereist een diepgaand begrip van zowel de abstracte syntaxis van de expressie als de specifieke machine-instructies die nodig zijn om de berekeningen uit te voeren. De vertaling is gebaseerd op het idee dat elke expressie, ongeacht of deze een getal of een booleaanse waarde oplevert, wordt omgezet in een reeks machine-instructies die het gewenste resultaat produceren. De vertaalde instructies volgen een logisch pad dat is ontworpen om de oorspronkelijke expressie nauwkeurig te repliceren.

Bij het vertalen van een expressie in de programmeertaal naar machine-instructies, is het belangrijk om te begrijpen hoe de abstracte syntaxis en de semantiek van de expressie elkaar beïnvloeden. Elke expressie kan worden weergegeven als een machineprogramma, dat bestaat uit een reeks overgangsregels die de uitvoering van de expressie bepalen. Deze regels zorgen ervoor dat de machine de juiste waarde berekent door de juiste operaties in de juiste volgorde uit te voeren.

Een typisch vertaalproces kan worden beschreven aan de hand van een aantal voorbeelden. Wanneer een eenvoudige expressie zoals a + 1 moet worden vertaald naar een machineprogramma, zou de vertaling eruit zien als een reeks van drie machine-instructies: eerst wordt de waarde van a geladen, dan wordt de constante waarde 1 toegevoegd, en ten slotte wordt het resultaat opgeslagen. In de machine is de uitvoering van deze instructies te volgen aan de hand van de status van de programma-counter en de stack. Elke stap wordt nauwkeurig uitgevoerd, waarbij de waarden van variabelen zoals a en de resulterende getallen op de stack worden aangepast.

Een meer complexe expressie, zoals (a + 1) * b, vereist extra stappen, maar de vertaling volgt dezelfde logica: eerst wordt de expressie a + 1 vertaald naar een reeks van twee instructies voor het optellen van de waarden, waarna de expressie b wordt geladen en de vermenigvuldiging wordt uitgevoerd. Dit proces resulteert in een nieuwe configuratie van de machine, waarin de uiteindelijke waarde van de expressie bovenaan de stack staat.

Het vertaalproces is gebaseerd op een aantal kernconcepten. Allereerst wordt een atomische expressie, zoals een constante of een variabele, omgezet in een enkele machine-instructie die de waarde op de stack plaatst. Voor een unitaire expressie, zoals een negatieve waarde of een booleaanse operatie, wordt de vertaling uitgebreid met een instructie die de bovenste waarde van de stack manipuleert. Bij binaire expressies, zoals optellen of vermenigvuldigen, worden de twee operanden van de stack gehaald en uitgevoerd op basis van de gedefinieerde operator.

In de vertaalde machine-instructies moeten de argumenten in de omgekeerde volgorde worden gepopt. Dit betekent dat de tweede operand van een binaire operatie eerst uit de stack wordt gehaald, gevolgd door de eerste operand, waarna de bewerking wordt uitgevoerd en het resultaat terug op de stack wordt geplaatst. Dit proces is essentieel om te begrijpen, aangezien het de manier weerspiegelt waarop de machine werkt, waarbij de laatste ingevoerde waarde als eerste wordt uitgehaald. Dit principe is vergelijkbaar met de manier waarop de zogenaamde 'Reverse Polish Notation' (RPN) werkt, waar de operator na de operanden wordt geplaatst. Dit systeem werd vroeger veel gebruikt in rekenmachines.

Bij de vertaling van booleaanse expressies, zoals gelijkheid (a = b), negatie (!B) of logische AND (B1 && B2), wordt een vergelijkbare structuur gevolgd. De vertaling van een booleaanse expressie bestaat uit instructies die de waarden op de stack vergelijken of manipuleren om het resultaat te bepalen. Als bijvoorbeeld een booleaanse expressie a = b wordt vertaald, wordt eerst de waarde van a en b geladen, waarna een gelijkheidscontrole wordt uitgevoerd die true of false op de stack plaatst, afhankelijk van het resultaat.

In de praktijk wordt bij de vertaling van complexe programma’s een systematisch proces gevolgd, waarbij de abstracte semantiek van de expressie wordt omgezet in een formeel uitvoerbare reeks instructies. Dit biedt een heldere en gestructureerde manier om programmeercode om te zetten in machine-instructies die de beoogde uitvoer correct en efficiënt kunnen produceren.

Voor de lezer is het van belang te begrijpen dat de transformatie van een expressie naar een machineprogramma niet slechts een eenvoudige omzetting is, maar eerder een herstructurering van de oorspronkelijke logica op een manier die de machine kan uitvoeren. Het betekent dat je, door het vertaalproces te begrijpen, niet alleen de technische werking van de machine kunt volgen, maar ook hoe de programmeertaal en de bijbehorende semantiek de uitvoering sturen.

Naast de vertaling van expressies naar machineprogramma's, is het belangrijk om de rol van de stack te begrijpen. De stack wordt gebruikt om tussenresultaten tijdelijk op te slaan en maakt het mogelijk om de waarden van variabelen dynamisch te manipuleren zonder de oorspronkelijke staat van de variabelen in het geheugen permanent te wijzigen. Dit zorgt voor een efficiënte en flexibele manier van berekeningen uitvoeren.