Programmeringsspråk och deras semantiska definitioner är ofta komplexa, men de följer en strikt logik som kan beskrivas i termer av värdeförhållanden och miljöer. I denna kapitel undersöks hur deklarationer, program och procedurer relaterar till varandra genom den denotationella semantiken, där värden och adresser hanteras systematiskt. Denna beskrivning ger en inblick i hur program körs genom att relatera parametrarnas initiala värden till deras slutliga värden och hur olika typer av deklarationer påverkar programmets körning.

En programsekvens kan beskrivas som en relation mellan de initiala värdena på parametrarna och deras slutliga värden efter att programmet har körts. För att förstå detta, betraktas programmet genom en formell struktur där varje sekvens av deklarationer och tillhörande instruktioner bidrar till skapandet av en miljö som styr hur värden lagras och hanteras i programmet. Programmet definieras genom en relation mellan den initiala miljön, de värden som tilldelas variabler och hur dessa värden förändras under programmets körning.

Första steget är att definiera deklarationerna i programmet, där varje deklaration påverkar miljön och stacken. En deklaration av en variabel eller en procedur läggs till i den globala miljön. När programmet körs allokeras adressen för varje parameter och variabel på stacken, och dessa adresser används senare för att lagra eller referera till variablerna. Därmed är deklarationen inte bara en formell uppgift att identifiera variabler eller procedurer utan en process som förändrar programmets minnesstruktur genom tilldelning av adresser.

En viktig aspekt är att variabler som deklareras på global nivå i programmet är bundna till specifika adresser i minnet. Dessa adresser spelar en central roll när värden ska lagras och hämtas, både för variabler och för procedurens parametrar. Adresserna används som referenser i hela programmet och är avgörande för hur variabler kopplas till värden. Procedurer, å andra sidan, definieras med specifika parametrar där vissa parametrar kan vara referenstyper, vilket innebär att de direkt pekar på minnesadresser och därmed möjliggör en dynamisk förändring av minnesinnehållet under körning.

Vidare är procedurens semantik viktig för att förstå hur programmets tillstånd förändras när en procedur anropas. En procedur kan ta emot värde- eller referensparametrar och för varje parameter skapas en specifik adresssekvens som styr hur argumenten från anropet tilldelas respektive parameter. Genom att använda en funktionell semantisk definition av proceduren kan vi beskriva förhållandet mellan programmets initiala tillstånd (minne och stack) och dess slutliga tillstånd. Det är här den denotationella semantiken verkligen får sin betydelse: den definierar en exakt relation mellan de data som lagras i systemet och de instruktioner som utförs.

En mer detaljerad förklaring visar hur värden kopplas till specifika adresssekvenser i minnet. För varje procedur som anropas allokeras nya adresser på stacken för att lagra argumenten som skickas till proceduren. Dessa argument är antingen värdeparametrar eller referensparametrar. För värdeparametrar kopieras värdena från de aktuella variablerna till de nya adresserna, medan referensparametrarna pekar direkt på de ursprungliga minnesadresserna. Detta gör det möjligt för proceduren att direkt manipulera de ursprungliga variablerna i programmets tillstånd.

När en procedur har exekverats, kan de nya värdena av dess parametrar extraheras och överföras tillbaka till huvudprogrammet. Denna process innebär att minnesadresserna som används under procedurens exekvering måste hanteras noggrant för att säkerställa att rätt värden återgår till huvudprogrammet. Detta är en central aspekt av hur programmeringsspråk hanterar funktioner och procedurer, och det är genom att använda sådana adresser som programmeraren säkerställer att dataflödet mellan olika delar av programmet bibehålls korrekt.

Vidare är det viktigt att förstå hur kommandon och tilldelningar fungerar i relation till miljöer och minneshantering. När en tilldelning av en variabel sker, används en specifik adress från miljön för att lagra värdet som tilldelas variabeln. Denna process förutsätter att värdet för variabeln korrekt hämtas från minnet, och att tilldelningen till den specifika adressen uppdaterar programmets tillstånd. Om programmet till exempel innehåller en tilldelning av en variabel, måste den korrekta adressen för variabeln användas för att uppdatera dess värde. Likaså, när en procedur anropas, tilldelas adresser för både värde- och referensparametrar, vilket gör att argumenten på rätt sätt kan överföras och lagras under procedurens körning.

När det gäller de tekniska aspekterna av programvarudesign och semantik är det avgörande att ha en förståelse för hur adresser, miljöer och stackhantering samverkar för att definiera programmets exekvering. Varje deklaration i programmet påverkar det globala minnet och stacken, vilket betyder att varje steg i programmets exekvering, från deklarationer till proceduranrop och tilldelningar, är beroende av exakt hantering av dessa dataflöden.

Denna syn på programmet som en sekvens av tillståndsövergångar, där varje steg noggrant styrs av adresser och deklarationer, ger oss ett kraftfullt verktyg för att förstå och förutsäga programmets beteende i olika situationer. En detaljerad analys av sådana semantiska relationer ger insikter i både programmeringsspråkets struktur och dess faktiska körning, vilket är ovärderligt för såväl utvecklare som teoretiker inom området programmeringsteori.

Hur fungerar Σ-homomorfismer i abstrakta datatyper och algebraer?

I teorin om abstrakta datatyper, och specifikt i användningen av Σ-algebraer, definieras en algebra som ett sätt att ge semantik åt de olika syntaktiska enheterna som deklareras inom en given signatur Σ. Detta innebär att varje element som deklareras i signaturen får en tillhörande tolkning (semantik) som är kompatibel med de sortdeklarationer som definieras i samma signatur.

En Σ-algebra associerar alltså varje element i signaturen med en mängd eller funktion, och de olika algebrorna kan vara både förenliga och olika beroende på hur de tolkar funktioner och operationer. Men även om två algebror är olika kan de fortfarande vara "kompatibla" i vissa avseenden, vilket kan visas genom att definiera en så kallad Σ-homomorfism mellan dem.

En Σ-homomorfism är en strukturbevarande funktion som "översätter" mellan två algebror och bevarar de tolkningar och operationer som är definierade inom varje algebra. Mer specifikt, om 𝐴 och 𝐵 är två Σ-algebraer, så innebär en homomorfism ℎ : 𝐴 →Σ 𝐵 att det finns en familj av funktioner ℎ𝑆 som för varje sort 𝑆 ∈ Σ.s, översätter element från 𝐴(𝑆) till motsvarande element i 𝐵(𝑆), och att denna översättning bevarar både konstanter, funktioner och predikat, enligt definierade villkor.

Homomorfismen ska alltså bevara semantiken för alla operationer i signaturen. Det innebär att om två olika algebror till exempel tillämpar en funktion på samma argument, så ska resultatet efter översättningen också vara förenligt med varandra. Homomorfismen definierar en ordningsrelation där den ena algebran kan ses som att den har "inte mindre struktur" än den andra.

Exempel 6.3 illustrerar detta med en enkel signatur Σ där vi definierar tre olika Σ-algebraer 𝐴, 𝐵 och 𝐶, var och en med olika tolkningar för de funktioner och konstanter som ingår i signaturen. Även om dessa algebror är olika kan vi se att de är kompatibla genom definierade homomorfismer mellan dem.

För att exemplifiera detta i ett konkret sammanhang, överväg en signatur Σ som består av en enda sort, Bool, tillsammans med konstanterna true och false, men utan några funktioner eller predikat. Vi kan definiera tre olika Σ-algebraer 𝐴, 𝐵 och 𝐶, där de definierade tolkningarna för konstanterna är olika i varje algebra, men vi kan fortfarande konstruera homomorfismer mellan dem.

För varje sådan algebra och varje homomorfism är det viktigt att förstå att homomorfismen inte bara översätter värden från en algebra till en annan. Den bevarar också den struktur som definieras av tolkningarna och funktionerna. Det innebär att om två element i en algebra ges samma tolkning av en funktion, så måste de motsvarande elementen i den andra algebran också tolkas på ett kompatibelt sätt.

Exempel 6.4 tar upp en mer detaljerad situation där vi har en mer komplex signatur Σ′ som innehåller en funktion "not" för booleska värden. När vi definierar nya homomorfismer för denna utvidgade signatur, kan vi fortfarande konstruera översättningar mellan algebrorna 𝐴, 𝐵 och 𝐶, vilket visar att även om tolkningarna förändras, bevaras den övergripande strukturen i systemet.

I själva verket visar detta på en grundläggande idé i algebraisk teori, nämligen att homomorfismer tillåter oss att jämföra algebror och deras strukturer på ett ordnat sätt. Om en homomorfism existerar mellan två algebror, betyder det att den ena algebran är "mindre strukturerad" än den andra i den meningen att den har en förenklad eller reducerad tolkning av de samma operationer.

För att bygga vidare på denna förståelse är det viktigt att överväga att en Σ-homomorfism inte bara fungerar som en enkel översättning mellan två algebror utan också som ett sätt att definiera en hierarki eller ordning av algebror. En algebra som kan överföras till en annan genom en homomorfism, visar på en grad av kompatibilitet mellan de två, vilket möjliggör användning av den ena algebran som en modell för den andra i olika kontexter.

Hur man resonerar om specifikationer för abstrakta datatyper

Vi börjar med att adressera den första frågan ∀𝐴 ∈ M. [𝐹]𝐴 = true för de olika typerna av "grundläggande" specifikationer (lös, (ko)genererad och (ko)fri), och fortsätter med de "strukturerade" specifikationerna sammansatta av olika typer av konstruktioner introducerade i avsnitt 6.7.

Lösa Datatyper

Om en abstrakt datatyp M betecknas av den lösa tolkningen av en deklaration 𝐷 med presentation ⟨Σ,Φ⟩, beskriver mängden Φ = {𝐹1, . . . , 𝐹𝑛} redan all kunskap som finns om M. För att visa att M uppfyller en formel 𝐹, räcker det att visa att 𝐹1, . . . , 𝐹𝑛 ⊢ 𝐹, det vill säga att 𝐹 är en logisk konsekvens av Φ.

Exempel 6.29. Givet specifikationen för typen Bool ≔ True | False | Not(Bool) och axiom ∀b:Bool. True ≠ False ∧ (b = True ∨ b = False) ∧ Not(b) ≠ b, för att visa att Not(True) = False måste vi bevisa att ∀𝑏 ∈ Bool. True ≠ False ∧ (𝑏 = True ∨ 𝑏 = False) ∧ (Not(𝑏) ≠ 𝑏) implicerar Not(True) = False. Detta kan enkelt visas: från axiom vet vi att Not(True) = True ∨ Not(True) = False, men också att Not(True) ≠ True, vilket implicerar vårt mål.

Genererade Datatyper

I fallet med en specifikation som är genererad 𝐷, kan vi anta att varje sort som introduceras i 𝐷 har värden som kan betecknas av tillämpningen av någon funktion. Detta ger upphov till ett ytterligare axiom ∀𝑥 ∈ 𝑆. ∃... 𝑥 = 𝑓1 (... ) ∨ ... ∨ 𝑥 = 𝑓𝑛 (...), där 𝑓1, . . . , 𝑓𝑛 betecknar alla konstruktorer för S, det vill säga alla funktioner med domän 𝑆 introducerade av 𝐷.

Ytterligare, vi kan anta att varje värde i 𝑆 kan betecknas av någon term. Detta ger upphov till bevisprincipen för (strukturell) induktion på 𝑆. Fs ⊢ ∀𝑥1, 𝑥2, . . . ∈ 𝑆. 𝐹[𝑥1/𝑥] ∧ 𝐹[𝑥2/𝑥] ∧ ... ⇒ 𝐹[𝑓1(..., 𝑥𝑖 , ...)/𝑥] ∧ ... ∧ 𝐹[𝑓𝑛(..., 𝑥𝑗 , ...)/𝑥].

Exempel 6.30. Ta specifikationen för den genererade typen Int ≔ 0 | 1 | Plus(Int, Int) | Minus(Int, Int), som introducerar sorten Int med konstruktorerna 0, 1, Plus och Minus. För att visa att egenskapen ∀𝑥 ∈ Int. 𝐹 gäller, räcker det att bevisa ∀𝑥1, 𝑥2 ∈ Int. 𝐹[𝑥1/𝑥] ∧ 𝐹[𝑥2/𝑥] ⇒ 𝐹[0/𝑥] ∧ 𝐹[1/𝑥] ∧ 𝐹[Plus(𝑥1, 𝑥2)/𝑥] ∧ 𝐹[Minus(𝑥1, 𝑥2)/𝑥].

Fria Datatyper

För en specifikation som är fri 𝐷 med en uppsättning Φ av Horn-Clauser som axiomer, kan vi anta att alla axiomer och inferensregler som härleds från en specifikation genererad 𝐷 också är tillämpliga. För att bevisa att den specifika datatypen M uppfyller en formel 𝐹 kan vi, som i fallet med en genererad 𝐷-specifikation, försöka visa att 𝐹 är en logisk konsekvens av formlerna i Φ med hjälp av bevisprincipen för induktion på konstruktörerna för de specifika sorterna.

Om detta inte räcker, kan vi också använda kunskapen om att M är monomorf, det vill säga att den består av en enda isomorfklass som innehåller kvottermalgebran TermΦ som den kanoniska representanten; det räcker därmed att visa att [𝐹]TermΦ Σ = true. Eftersom arbete på denna "semantiska" nivå är mer invecklat, föredrar vi att hålla oss på "logik" nivå genom att lägga till ytterligare kunskap som kan användas i beviset.

Exempel 6.32. Specifikationen för den fria typen Nat ≔ 0 | +1(Nat) inducerar axiom ∀𝑥, 𝑦 ∈ Nat. (+1(𝑥) = +1(𝑦) ⇒ 𝑥 = 𝑦) ∧ 0 ≠ +1(𝑥) för varje modell med bäraren Nat för sorten Nat och tolkningarna 0 och +1 för konstruktorerna 0 och +1, respektive. Från detta kan vi till exempel härleda att +1(0) ≠ +1(+1(0)).

Fria typdeklarationer utan axiomer representerar termalgebror, och detta har viktiga konsekvenser eftersom det gör det möjligt att införa nya operationer genom "rekursiva definitioner" som tillämpar "mönsterigenkänning" mot konstruktorterm. Detta kommer att behandlas vidare när vi diskuterar specifikationernas konsistens.