[{"content":"I supervise some CST courses at Cambridge, for example:\nPart IA Operating Systems\nI am happy to supervise Part II/III projects, either suggested by me (to be added), or if you have a proposal that aligns with both of our interest, feel free to reach out to me (contact details on the homepage). In general I am interested in systems projects such as distributed algorithms, database systems or operating systems. To get an idea of the past work that I have done, have a read of the hypermnesia or erlog projects I have done.\n","permalink":"https://incipit0.github.io/pweb/teaching/","summary":"\u003cp\u003eI supervise some CST courses at Cambridge, for example:\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://www.cl.cam.ac.uk/teaching/current/OpSystems/\"\u003ePart IA Operating Systems\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003eI am happy to supervise Part II/III projects, either suggested by me (to be added),\nor if you have a proposal that aligns with both of our interest, feel free to\nreach out to me (contact details on the homepage). In general I am interested in\nsystems projects such as distributed algorithms, database systems or operating\nsystems. To get an idea of the past work that I have done, have a read of\nthe \u003ca href=\"../posts/hypermnesia\"\u003ehypermnesia\u003c/a\u003e or \u003ca href=\"../posts/erlog.md\"\u003eerlog\u003c/a\u003e projects\nI have done.\u003c/p\u003e","title":"Teaching"},{"content":"This blogpost is the second part of a trilogy of formal verification on computer systems. Here we will take a quick tour of what formal verification means, with a particular focus on verifying systems software and examples of verifying some file system. If you have not read the previous post on system verification, feel free to jump there first for an introduction!\nCase studies CompCert CompCert1 is probably one of the first and most famous verified compilers (and perhaps also computer systems). It is itself a significant piece of work which took around 15 years. It is a rather important corner stone, however, that is achieved by the verification community, with tools/frameworks available in 2009, it is already possible to formally verify a complex system such as a compiler.\nThe verification is done through the notion of semantic preservation, which is amenable to compiler passes. A compiler will run a number of passes on the code, transforming the code into structures that is desirable for a particular target (often the binary executable, but could be something else). The paper formally defines the semantic preservation as\n$$ S\\ \\mathrm{safe} \\implies (\\forall B, C \\Downarrow B \\implies S \\Downarrow B) $$\nwhere a safe source (S) excludes non-determinism from the source such as undefined behaviours where the compiler can do whatever it wants to. This is saying: given that the source input program is good, the produced code C should only contain behaviours that is allowed by the S.\nA verified compiler is then defined as:\n$$ \\forall S, C, \\mathit{Comp}(S)=\\mathtt{OK}(C)\\implies S\\approx C $$\nwhich says that our verified program can produce nothing, but it never produces something that is incorrect.\nAnd the rest of the steps would be to define formal semantics for S and C, and define a simulation relation (as in the abstraction relation) to prove that a step in the higher level code is locked by a step in a level below.\nCompCert does have some limitations, though: for example, some parts of it is not verified (lexer, parser), and indeed there has been bugs found in the unverified part of CompCert. But otherwise I think it is very impressive work, especially considering that the performance of its generated code is competitive with gcc with with optimisation turned on.\nFSCQ FSCQ2 is a verified file system that incorporates crashes. The main novelty in this paper is that the author extended the usual Hoare triple \\(\\{P\\}C\\{Q\\}\\) with a new crash condition that describes the state of the system just before the crash happens. This allows them to reason about the the behaviour of the system when it crashes, which is arguably quite important for file systems, since crash safety is one of the important features of a file system, and crash recovery is implemented in tools such as fsck, therefore it is important to understand the formal behaviour of such a system. A simple example from the paper illustrates this well:\n$$ \\begin{align} \\mathrm{SPEC}:\u0026amp;\\ \\mathtt{disk\\_write}(a, v) \\newline \\mathrm{PRE}:\u0026amp;\\ \\mathbf{disk}:a\\to \\langle v_0，vs\\rangle * \\mathit{other\\_blocks} \\end{align} $$\nTo cope with repeated crashes i.e. crashes that happen while recovery, the spec of the recovery procedure needs to be idempotent, which means that the crash condition of the recover function implies its precondition, hence we can always go from a crashed state of the recover procedure back to its starting state again.\nx86-TSO The x86-TSO is also an interesting work which formalises the shared memory concurrency model for the x86 architecture. The motivating example given in the paper is fairly illuminating:\nProc 0 Proc 1 Mov x \u0026lt;- 1 Mov y \u0026lt;- 1 Mov Eax \u0026lt;- y Mov Ebx \u0026lt;- x Allowed final state: Proc 0: Eax=0 /\\ Proc 1: Ebx = 0 What this is saying is that processes are writing to the shared memory, but no one is observing each other\u0026rsquo;s write. Apparently this is allowed in the x86 (and also AArch) memory model, but developers often have to refer to the x86 manual (perhaps thousands of pages) to find out if this is indeed allowed by design.\nIf we had a mathematically precise model for these ISAs, then it would be more rigorous for developers and easier to verify whether something is allowed by the model. This is where this paper developed x86-TSO model which formulates this memory model in HOL4.\nArmada Armada is a verification language/tool/framework that allows programmers to write concurrent code with their choice of sync primitives and offers them the flexibility. It then allows programmers to write a series of more abstract spec about their program, and prove the refinement. Armada also has mechnically verified proof strategies, making it more trustworthy.\nThe way I think about Armada is another layer built on top of Dafny to make verification simpler. It uses several techniques such as movers to combine blocks, refinement, explicitly tracking non-determinism through encapsulation, etc. These techniques help make programmers\u0026rsquo; (who are trying to implement/verify a program) life easier.\nCertiKOS This is about a proof of the security property of a system. The systems mentioned above are usually about the functional correctness of a system, specifying security of a system presents a different challenge. Firstly it is attractive to do so since if security often consists of corner cases of code where an attack can be launched, i.e. they are not in the normal good execution flow. It tends to be hard for developer to manually rule out all such possible cases. Formal verification can be helpful as it allows developer to do so-called \u0026ldquo;structured exploration\u0026rdquo;3, and systematically rule out possible attacks, subject to the proof assumptions.\nThe key idea introduced in the CertiKOS4 paper is an observation function: O(principal, state) defines the set of objects/observations that can be made by the principal. Two states are then indistinguishable when their observations are the same. If we can then prove inductively that the state indistinguishability is preserved by the execution, non-interference can be proved, which means that two traces are different entirely based on the user input/observed data. The opposite of that would be one process\u0026rsquo;s execution result is interfered by another one due to, for example, some shared resources being modified. The meltdown attack can be approximately described as an interference: the (timing) result we get by launching the meltdown attack is not based on the private data we have, but some other kernel memory data, for example. This kind of interference by other process (in this case the kernel, and the CPU) breaks down the isolation between processes and can cause security problems.\nIndustrial scale static analysis I want conclude the case study with an example of a real-world deployment of static analysers at scale in industry. We study two articles from Google 5 and Facebook/Meta 6, where static analysis tools are deployed in (part of) their large codebase and made impact on their product.\nGoogle developed several static analysers such as FindBugs 7 for Java program analysis, Tricoder and JavaFlume. Most of these tools perform intra-procedural analysis, as Google argues that \u0026ldquo;Google does not have infrastructure support to run interprocedural or whole-program analysis at Google scale\u0026rdquo;. We see that in industry companies often make tradeoffs between full-fledged product and pragmatism: what is the sweet spot between these two that can be developed? This is often in contrast to academia research where people wish to pursue a vision/develop a model that is as complete as possible (arguably academia often makes tradeoffs as well, due to resource constraints, but probably less so than industry). The 20/80 rule can often be seen in many places there.\nWhile Google focuses on getting the \u0026ldquo;effective false positives\u0026rdquo; (which they define based on developer action) as low as possible, which often necessarily mean that false negatives would increase as well. Facebook is coming from a different perspective, where they do care about false negatives and hence has developed tools like Infer 8 to carry out inter-procedural analysis. Facebooks puts an emphasis on different types of bugs as well: for example, a crash on a busy server is probably more severe than a memory leak on a rarely hit code path, and they set different tolerance level for false positives according to the nature of the bug. One extreme of such would be security bugs, which they set a relatively high tolerance bar.\nThere are several common themes among the techniques used by these two companies though:\nBoth of them focus on developer happiness in such tools, by using techniques such as \u0026ldquo;diff time\u0026rdquo; analysis, which just means that potential issues reported by the static analyser will be fired at code review time. Developers are often reluctant to fix bugs identified in batch analysis, since these bugs are often in code that is already in master, perhaps written a long time ago, and most enginners are now working on a different problem. Dedicating time to fix these bugs often mean that they had to context switch out of their current problem, which tend to quite an expensive operation, and also mentally unpleasant.\nThe previous point also implies that these tools need to be intergated with the developer workflow: from IDE to CI/CD, the earlier the analyser can issue its finding, the cheaper it tends to be in terms of fixing it. Although here another tradeoff would be that developer might not want to be bothered by static analyser when they are delibrately taking a shortcut in their code to test a certain feature, so review time analysis seems to be a sweet spot.\nCompanies like Google and Facebook has a enormous codebase, even building the codebase itself can take a long time (see my S-REPLS14 article on build systems developed by Meta), let along performing complex analysis. Google has taken the approach to focus on simple yet effective analysis, while Facebook develops incremental analysis and compositionality model to help speed up their analysis.\nSummary system abstraction level theorem prover/tools novelty Static analysis code analysis code structure FindBugs/Infer deployed in industry at scale CompCert compiler after parsing, before linking Coq One of the first to verify a complex system FSCQ file system spec to impl. of the FS Coq Crash Hoare/Separation logic x86-TSO x86 memory model multicore CPU memory model HOL4 useful memory model for ISA Armada language for verification Impl + progressive abstr. Built with Dafny verification language with flexibility, automation and sound semantic extensibility IronFleet RSM and KV store Spec \u0026amp; Impl TLA \u0026amp; Dafny breakthrough in distributed system verification CertiKOS kernel security e2e security Coq formalisation of security properties: non-interference Does formally verified systems still have bugs? A good empirical study in this area 9 points out that most of the time verified system themselves are quite stable but systems we use inevitably consist of many layers and it is often the tooling, shim, frameworks around the these verified systems can go wrong.\nReferring to the diagram in my previous post, I have added colour to indicate different levels of verification. Verifying the whole system stack can often be too expensive to be done, therefore most systems will focus on one part of the stack. Even in a system such as IronFleet, it is just the protocol and implementation that have been verified. The rest of the system is either partially verified or not verified. For example, the specification of the system is often not verifiable (this is like asking if the type signature of a function is correct, but it is up to the programmer to decide what type signature they want a function to have), hence bugs such as incorrect assumptions about the system (infinite stacks, UDP vs TCP etc) can often cause bugs in the system. The shim layer that often acts as a wrapper around some of the OS functionalities (for example some language thin libraries that wrap around OS system calls to ease the life of developers) might do incorrect translations, handling of errors.\nAlthough there are still bugs in these verified systems, the good news is that those verified parts (green ones) are indeed bug-free, unlike many unverified systems. This does tell us though that verification is not panacea, but merely an integral part. Practically speaking, we still want to combine testing strategies (fuzzing, property-based, integration, regression) with formal verification to act, as if, sanity checks for these formally verified systems.\nHow applicable are these systems? So far these verified systems often still live in their research areas and are seldom deployed in production. Moreover, there are still quite a lot of beasts that we need to overcome in order to make these techniques more usable in general realm of software development. In this section I will be focusing on two major, in my opinion, obstacles yet to be overcome for formal verification to be widely used.\nDevelopment Perhaps what is fairly obvious is that specifying the system consists of certain overhead, let alone proving them. Needless to say, this would depend on the actual theorem prover being used. Coq, for example, can often require 10x10 proof to code ratio due to its powerful proof system (which means relatively less automation). Other tools might be better, anecdotally, Dafny has a much lower proof to code ratio, which is around 1:10, thanks to its powerful automation. This comes with the cost that Dafny is a much more complex system that depends on external libraries and solvers to do the job for it. Bugs/crashes in these external tools might introduce bugs in a formally verified system. There are lots of other theorem provers available to choose from, I am planning to write a more detailed survey of these soon, so stay tuned!\nAnother overhead in using these formal verifications is that they require extra learning effort. Again, this would depend on the specific tools being used, but my personal experience with Coq, even coming from a functional programming background, is that it still takes quite some effort to get up to speed and do proofs efficiently, let alone mastering it. Coq has lots of advanced automation features, such as its Ltac tactical language that allows one to develop \u0026ldquo;higher order\u0026rdquo; tactics. This indeed makes proofs more automatable and robust, can take some effort to learn. The formal verification community is not quite as mature as the programming language community so its documentation/tutorials might not be as approachable. Moreover, learning Coq, as pointed out here 11, is learning specification and proof at the same time. The good news is that both of them are often interconnected, and an incorrect specification leads to inability to prove and makes one think about the spec again. The disadvantage is that learning two things at the same time is not easy, oftentimes knowing how to write a spec will get you quite far in ensuring the correctness of the system. That is why there are tools that tries to automate the proof process and let developers focus on writing a good spec.\nDeployment Formally verified systems are often not as fast as their unverified counterparts. Lots of optimisations involving mutable data structures, impure code, which are fairly tricky to prove. And many of the production systems is a combination of years of optimisation and fine tuning, whereas formally verified systems are often developed from scratch (it tends to be tricky to verify an existing codebase, since their code is not written with verification in mind), and the main focus is often not getting it as fast as possible. Is it worth using a formally verified system to get those extra assurance in exchange of some performance downgrades? Depending on your context, but for software (excluding safe-critical ones), maybe not. Since software are relatively easy to fix up, and most of them can be solved by restarting the system anyway, as long as they are \u0026ldquo;good enough\u0026rdquo;, users won\u0026rsquo;t care if their software is formally verified.\nAnother question to ask is how maintainable are these formally verified systems? Ideally we might want to say that our system is 100% correct so there is no need to maintain (or at least fix bugs) in the first place! But in reality, specs can go wrong, dependencies might get updated, and there may be new feature requests. Note that updating the source code of a formally verified system could require updating the proof as well. One line of change might render the whole proof incorrect (this would depend on the actual tool used, once again). So do companies want to pay that price for that extra correctness?\nReflecting on static analyser section, in order to get more deployment of these systems, we should consider: 1. how can these verification techniques be integrated into developer\u0026rsquo;s day-to-day workflow; 2. how can we lower the bar of learning to use tools so that developers can get started quickly; 3. how can we make sure that such systems scale well for large corporations; 4. is the cost-benefit ration of developing/deploying these tools striking the right balance for the company to invest in them. Addressing these open questions is a key part of getting more wide spread usage of these tools.\nConclusions Recent years have seen lots of development in formal verifications, especially in verifying a relatively large piece of software system. Although the real-world deployment of these systems are still not prevalent, the community has been growing larger and larger with more formally verified systems, as well as tools/frameworks for doing such verification.\nThere are still steps need to be taken to get us towards wider deployment of these techniques, including learning curves, proof overhead, etc. And to end on a positive note, formal verification has found its usage in safety-critical systems 12 and will probably remain being used in such areas.\nReferences Xavier Leroy. 2009. Formal verification of a realistic compiler. Commun. ACM 52, 7 (July 2009), 107–115. https://doi.org/10.1145/1538788.1538814\u0026#160;\u0026#x21a9;\u0026#xfe0e;\nHaogang Chen, Daniel Ziegler, Tej Chajed, Adam Chlipala, M. Frans Kaashoek, and Nickolai Zeldovich. 2015. Using Crash Hoare logic for certifying the FSCQ file system. In Proceedings of the 25th Symposium on Operating Systems Principles (SOSP \u0026lsquo;15). Association for Computing Machinery, New York, NY, USA, 18–37. https://doi.org/10.1145/2815400.2815402\u0026#160;\u0026#x21a9;\u0026#xfe0e;\nT. Murray and P. van Oorschot, \u0026ldquo;BP: Formal Proofs, the Fine Print and Side Effects,\u0026rdquo; 2018 IEEE Cybersecurity Development (SecDev), Cambridge, MA, USA, 2018, pp. 1-10, doi: 10.1109/SecDev.2018.00009. keywords: {Security;Computational modeling;Cognition;Kernel;Arrays;Conferences;formal verification;computer security;software engineering},\u0026#160;\u0026#x21a9;\u0026#xfe0e;\nDavid Costanzo, Zhong Shao, and Ronghui Gu. 2016. End-to-end verification of information-flow security for C and assembly programs. SIGPLAN Not. 51, 6 (June 2016), 648–664. https://doi.org/10.1145/2980983.2908100\u0026#160;\u0026#x21a9;\u0026#xfe0e;\nCaitlin Sadowski, Edward Aftandilian, Alex Eagle, Liam Miller-Cushon, and Ciera Jaspan. 2018. Lessons from building static analysis tools at Google. Commun. ACM 61, 4 (April 2018), 58–66. https://doi.org/10.1145/3188720\u0026#160;\u0026#x21a9;\u0026#xfe0e;\nDino Distefano, Manuel Fähndrich, Francesco Logozzo, and Peter W. O\u0026rsquo;Hearn. 2019. Scaling static analyses at Facebook. Commun. ACM 62, 8 (August 2019), 62–70. https://doi.org/10.1145/3338112\u0026#160;\u0026#x21a9;\u0026#xfe0e;\nhttps://github.com/findbugsproject/findbugs\u0026#160;\u0026#x21a9;\u0026#xfe0e;\nhttps://fbinfer.com/\u0026#160;\u0026#x21a9;\u0026#xfe0e;\nPedro Fonseca, Kaiyuan Zhang, Xi Wang, and Arvind Krishnamurthy. 2017. An Empirical Study on the Correctness of Formally Verified Distributed Systems. In Proceedings of the Twelfth European Conference on Computer Systems (EuroSys \u0026lsquo;17). Association for Computing Machinery, New York, NY, USA, 328–343. https://doi.org/10.1145/3064176.3064183\u0026#160;\u0026#x21a9;\u0026#xfe0e;\nUpamanyu Sharma, Ralf Jung, Joseph Tassarotti, Frans Kaashoek, and Nickolai Zeldovich. 2023. Grove: a Separation-Logic Library for Verifying Distributed Systems. In Proceedings of the 29th Symposium on Operating Systems Principles (SOSP \u0026lsquo;23). Association for Computing Machinery, New York, NY, USA, 113–129. https://doi.org/10.1145/3600006.3613172\u0026#160;\u0026#x21a9;\u0026#xfe0e;\nhttps://news.ycombinator.com/item?id=15781508\u0026#160;\u0026#x21a9;\u0026#xfe0e;\nCousot, P., Cousot, R., Feret, J., Mauborgne, L., Miné, A., Monniaux, D., and Rival, X. The ASTRÉE analyzer. In Proceedings of the European Symposium on Programming (Edinburgh, Scotland, Apr. 2–10). Springer, Berlin, Heidelberg, 2005.\u0026#160;\u0026#x21a9;\u0026#xfe0e;\n","permalink":"https://incipit0.github.io/pweb/posts/proof-system-study/","summary":"\u003cp\u003eThis blogpost is the second part of a trilogy of formal verification on computer\nsystems. Here we will take a quick tour of what formal verification means, with\na particular focus on verifying systems software and examples of verifying some\nfile system. If you have not read the previous post on\n\u003ca href=\"https://incipit0.github.io/pweb/posts/system-proof-101/\"\u003esystem verification\u003c/a\u003e, feel free to jump\nthere first for an introduction!\u003c/p\u003e\n\u003ch2 id=\"case-studies\"\u003eCase studies\u003c/h2\u003e\n\u003ch3 id=\"compcert\"\u003eCompCert\u003c/h3\u003e\n\u003cp\u003eCompCert\u003csup id=\"fnref:1\"\u003e\u003ca href=\"#fn:1\" class=\"footnote-ref\" role=\"doc-noteref\"\u003e1\u003c/a\u003e\u003c/sup\u003e is probably one of the first and most famous verified compilers (and\nperhaps also computer systems). It is itself a significant piece of work which\ntook around 15 years. It is a rather important corner stone, however, that is achieved\nby the verification community, with tools/frameworks available in 2009, it is already\npossible to formally verify a complex system such as a compiler.\u003c/p\u003e","title":"Proof System Case Study"},{"content":"This blogpost is the first part of a trilogy of formal verification on computer systems. Here we will take a quick tour of what formal verification means, with a particular focus on verifying systems software and examples of verifying some file system.\nWhat do you mean by formally verified systems? Sometimes we hear people say we have built this formally verified system that is provably correct, and does all sorts of amazing things without bugs. But what we mean by having a system formally verified? At a high level, a formally verified system often has a formal specification which mathematically stipulates what the system ought (and ought not) to do, followed by a proof that such a concrete realisation of the system meets such a spec.\nOne question that has I think this comes down to which layer of the abstraction is verified. Modern systems will inevitably consist of many layers of abstractions, from the application itself down to the implementation and even to the hardware. There is not much work (as far as I am aware) that can do full stack verification, from the application to the hardware, for example. One could develop a system and by having a verified protocol: say I want to develop (yet another) system that can provide distributed consensus, and I grab an off-the-shelf verified protocol (like Paxos) and implement it, and claim that I have got a formally verified system. Well the Paxos protocol might be formally verified, but that is far from saying that your consensus system is verified: Who knows if you are actually implementing the Paxos protocol, maybe you are writing Raft? And how do I know there is no bug in your system? Why should I believe that the runtime of whatever language you are writing in is correct, how do I know if the OS is correct, how about the hardware? The latter components, such as OS and hardware, are certainly not the weak parts of the chain, but still, there is proof that they have been formally proved to be correct. What I am trying to reach here is that computer systems are complex and what verification can often (and realistically) do is to verify one (small) part of the stack (often the protocol and implementation layer).\nSpecification So far we have been talking quite abstractly about how we wish to prove the correctness of a system. In the next two sections I will be using a couple of concrete examples1 to illustrate the proof process.\nSuppose we wish to specify a replicated disk protocol, and provide an implementation that meets such a protocol. A replicated disk protocol is commonly used for fault tolerance purposes, sometimes referred to as RAID, where multiple physical disks are connected to provide one logical disk. Under the hood, when one of the disk fails, it can then transparently switch to other failover disks to continuously provide the disk service. The user does not need to worry about this failover/switching process, all user gets to see is that their disk is continuously working, despite the actual failure that has happened.\nLet\u0026rsquo;s try to specify what we expect a normal disk would do (no fancy failover whatsoever). One might imagine it provides two basic functionalities:\nAxiom read: addr -\u0026gt; proc block Axiom write: addr -\u0026gt; block -\u0026gt; proc unit The proc type above is just some kind of monad constructor that lifts the return value to the monadic world.\nThe next step would be to specify what these operations are expected to do. We can do that in a (crash) Hoare style logic, referring to the pre/post-condition of our read operation. For example:\nDefinition read_spec (a: addr) := fun (_: unit) state =\u0026gt; {| pre := True; post := fun r state\u0026#39; =\u0026gt; state\u0026#39; = state /\\ diskGet state a =?= r; recovered := ... |}. Definition write_spec (a : addr) (v : block) := fun (_ : unit) state =\u0026gt; {| pre := True; post := fun r state\u0026#39; =\u0026gt; r = tt /\\ state\u0026#39; = diskUpd state a v; recovered := fun _ state\u0026#39; =\u0026gt; ... |}. There are lots of details here to be explained (I will omit the recovered condition for this post, to simplify the explanation), but we can focus on the important part, which is the pre and post conditions of these specs. For read, it says, whatever it is initially, after the read, the state (that is, the disk blocks) stays the same, and the return value r is equal to the actual value stored in the disk state state at addr a. Contrast this with the simplest Hoare logic triples:\n{X = 1} {X + 1 = 2} X := X + 1 {X = 2} the state variable above refers to the stack in the usual Hoare logic, and the disk* operations are just helper functions defined on the state to talk about certain properties of the state.\nNow we have specified what it means for a disk to function correctly in terms of read and write, we can start specifying our replicated disk. We assume there is a two disk library that has already been specified and verified for us, on top of which we will specify the replicated disk. The spec might look something like this:\nTheorem read_int_ok : forall a, proc_spec (fun d state =\u0026gt; {| pre := two_disks_are state (eq d) (eq d); post := fun r state\u0026#39; =\u0026gt; two_disks_are state\u0026#39; (eq d) (eq d) /\\ diskGet d a =?= r; recovered := ... |}) (read a) td.recover td.abstr. Theorem write_int_ok : forall a b, proc_spec (fun d state =\u0026gt; {| pre := two_disks_are state (eq d) (eq d); post := fun r state\u0026#39; =\u0026gt; r = tt /\\ two_disks_are state\u0026#39; (eq (diskUpd d a b)) (eq (diskUpd d a b)); recovered := ... |}) (write a b) td.recover td.abstr. Proof. where now the state becomes a pair of disks, which comes from the two disk library. And our pre/post conditions refer to both of these disks, which the two_disks_are function helps us to express. In particular, note that the write spec says that after a (successful) write operation, the state of both disks would be the same, i.e. both updated.\nSo far I believe the specs are all pretty straightforward and how one would expect a duplicated disk would behave. The important thing to highlight (and will be mentioned later on in the Abstraction section as well) is that there are different layers: there is the top level single disk spec, the intermediate two disk disk spec, and the replicated disk spec which sits above the two disk spec and below the single disk spec, connecting these two together by implementing the two sing disk API, using the two disk API. This is indeed the power of abstraction, which prevails many aspects of software development, and naturally manifests in formal verification as well.\nImplementation The actual implementation of a replicated disk should be quite familiar to many of the software engineers/computer scientists. The general strategy is to do the operation on both disks, and return the appropriate value. I will include the code here for completeness, but the code itself should be quite straightforward.\nDefinition read (a:addr) : proc block := mblk \u0026lt;- td.read d0 a; match mblk with | Working blk =\u0026gt; Ret blk | Failed =\u0026gt; mblk \u0026lt;- td.read d1 a; match mblk with | Working blk =\u0026gt; Ret blk | Failed =\u0026gt; Ret block0 end end. Definition write (a:addr) (b:block) : proc unit := _ \u0026lt;- td.write d0 a b; _ \u0026lt;- td.write d1 a b; Ret tt. Abstractions In this section we take a quick detour on the actual example and talk about abstraction abstractly :) before we return to our replicated disk example.\nAbstractions, abstractly Abstraction is at the core of many areas of Computer Science. Whether we are building software or hardware systems, we rely on layers of abstractions to help us produce good design and focus on the property we care about with the right level of detail. This is no different to formal verification. In fact, a specification itself can be thought of as an abstraction of the underlying system. We specify in precise, rigorous mathematical language how we want our system to behave and then we prove that our actual design/implementation meets such a specification.\nThe diagram1 below summarises this in a very concise manner\nWe write down our spec in a mathematical language, examples would be in a Hoare logic style, and then as we execute our code step by step, the state of our world gets changed (think about the state of an abstract state machine, Turing machine, register machine, etc). If initially we are in state \\(w\\), and as we execute our code, we transitioned to state \\(w\u0026rsquo;\\). Now if our abstraction relation holds when we were in \\(w\\), then we need to show that there will be a new spec \\(s\u0026rsquo;\\) that satisfies the abstraction relation in \\(w\u0026rsquo;\\) as well, and this new spec is not a random spec, but one where there is a valid transition from the old spec \\(s\\). For example, if we are writing a our spec using Hoare logic, then the allowed transition would be from the precondition to the postcondition. In a TLA-style model checking spec, this would be encoded as part of the state transition relation. In the diagram above, the solid lines indicate our assumptions, and dashed lines are what we need to prove. Mathematically:\n$$ w \\to w\u0026rsquo; \\land \\mathrm{abstr}(s, w) \\implies \\exists s\u0026rsquo;, \\mathrm{abstr}(s\u0026rsquo;, w\u0026rsquo;) \\land s \\to s' $$\nIf we do this for every step of the program, and compose them together, we get a proof that: if our program is in a initial state allowed by the spec, it will be in a final state allowed by the spec, and we have proved that our program satisfies/simulates/refines the spec! Intuitively, this means we have found a concrete instantiation (i.e. the implementation) of our spec and proves it is indeed a valid instantiation.\nNote that not only can we compose this diagram horizontally to get a proof from each instruction of the program, we can also stack it vertically to get a prove about multiple layers of the system. This is indeed what IronFleet2 does, where they used TLA-style state-machine refinement to verify that a Paxos-based consensus protocol satisfies the high level spec (presumably reaches consensus), and then use Floyd-Hoare-style logic to prove that their implementation of the system satisfies consensus protocol. In the end, we get a system implementation (with actual binaries) that meets the mathematical spec (they will reach consensus).\nThis idea of abstraction is reminiscent of the abstraction/encapsulation we see in Object-oriented programming (and indeed in many other areas of Computer Science). The similarity lies in that in OOP, programmers can just look at the interface of a class and know how to use them, without worrying about how they are implemented. And with formally specified code, we can check the spec of the code, and try to see whether the spec is correct, which is usually much easier than looking at the code and trying to find bugs. In other words, the spec is doing a similar thing as the interface: to provide something that can be quickly inspected by the programmer, knowing that the developer of the library/implementation has done the hard work to make sure that the implementation has indeed satisfied the spec. The spec can also be thought of as an enhancement of the documentation/type signature provided by many of the libraries. Developers often need to read the doc/type signature to understand how to use the API, the difference is that the documentation is in natural language and provides no guarantee whatsoever that the implementation does actually folllow the doc. Type signature, on the other hand, does provide these guarantees by the compiler that at least the implementation takes the right arguments and returns the value of the right type. Type systems, however, are often not (yet) expressive enough to convey all the details in the doc though, and that is why developers need to write prose to augment the type signature. The spec is almost like a more expressive type system that allows one to express the intention of the API in a mathematical language and have them machine-checked, just like the type checking. In fact, the trend we see in programming language type system design is that they are more and more expressive: from weak types to strong types to borrow checker, dependent types, linear types, graded types, etc. We shall discuss more on the this trend in the real world applicability section.\nAbstractions in a replicated disk (Un)fortunately the abstraction relation which connects the single disk state and the replicated dual disk state is fairly straightforward:\nDefinition rd_abstraction (state: TwoDiskBaseAPI.State) (d:OneDiskAPI.State) : Prop := two_disks_are state (eq d) (eq d). which basically says that a single disk d and dual disk (d1, d2) is related if d = d1 /\\ d = d2, which is again, what one would expect from a replicated disk.\nAbstractions in a file system log disk To illustrate the power of abstraction, here is another example where we are trying to implement a write-ahead log. We will implement on top of the single disk api. The basic strategy is to have one block in our disk to record the size of our current log, and anything beyond the size stored in that block is considered invalid. With the assumption that each individual disk write is atomic, we can achieve atomic writes to the entire block with this implementation (by writing the actual data first and does not commit until we atomically write to the first block).\nInductive log_abstraction (rd_disk: OneDiskAPI.State) (log_state: LogAPI.State) : Prop := | LogAbstraction: forall (Hblock: forall b : block, diskGet rd_disk 0 =?= b -\u0026gt; (block_to_addr b) + 1 \u0026lt;= diskSize rd_disk /\\ diskGets rd_disk 1 (block_to_addr b) =??= log_state), log_abstraction rd_disk log_state. This is Coq\u0026rsquo;s inductive definition, which says that two states OneDiskAPI.state and LogAPI.state is related if we can find a type constructor. And there is only one type constructor, namely LogAbstraction, which says that for all the hypothesis Hblock, the abstraction relation holds, i.e. if there is a hypothesis that looks like Hblock, and it is proved to be true, we can conclude that log_abstraction holds.\nThe hypothesis itself, in this case, just says, the first block is a valid block, and the rest of the block up to the size stored in the first block, is the same in the log and the one disk.\nThere are lots of details left out in the abstraction relation (and in the actual proof, this is where the devil is in). But at least intuitvely, this abstraction relation is, again, self-explanatory and as expected.\nAnd this is it in terms of specifying, implementing and abstracing our replicated disk protocol. Now the only thing left to do is (just?!) to prove that these things are actually true, which I will not bore you with. At a high level, a user of this replicated disk library would just need to check the spec of its api and ignore the rest (such as implementations, abstractions, proofs) but still have the confidence in the correctness of the system (as long as the author of this library is not doing completely crazy things such as defining the abstraction relation to True).\nReference Adapted from the 6.826 course, thanks to the course staff for providing guidance and the proof infrastructure!\u0026#160;\u0026#x21a9;\u0026#xfe0e;\u0026#160;\u0026#x21a9;\u0026#xfe0e;\nHawblitzel, C. et al. (2015) ‘IronFleet: proving practical distributed systems correct’, in Proceedings of the 25th Symposium on Operating Systems Principles. SOSP ’15: ACM SIGOPS 25th Symposium on Operating Systems Principles, Monterey California: ACM, pp. 1–17. Available at: https://doi.org/10.1145/2815400.2815428.\u0026#160;\u0026#x21a9;\u0026#xfe0e;\n","permalink":"https://incipit0.github.io/pweb/posts/system-proof-101/","summary":"\u003cp\u003eThis blogpost is the first part of a trilogy of formal verification on computer\nsystems. Here we will take a quick tour of what formal verification means, with\na particular focus on verifying systems software and examples of verifying some\nfile system.\u003c/p\u003e\n\u003ch2 id=\"what-do-you-mean-by-formally-verified-systems\"\u003eWhat do you mean by formally verified systems?\u003c/h2\u003e\n\u003cp\u003eSometimes we hear people say we have built this formally verified system that is\nprovably correct, and does all sorts of amazing things without bugs. But what\nwe mean by having a system formally verified? At a high level, a formally verified\nsystem often has a formal specification which mathematically stipulates what the\nsystem ought (and ought not) to do, followed by a proof that such a concrete\nrealisation of the system meets such a spec.\u003c/p\u003e","title":"System Proof 101"},{"content":"This is a very high level summary of the experience attending S-REPLS 14. Feel free to look at the respective website of the speakers for more details. To keep this blog concise, I thought I would just use introductory examples where possible to illustrate the idea rather than going into the actual theory behind them.\nGraded types and algebraic effects Graded type system is based on the idea of linear types. Linear types essentially allows one to restrict the usage of variables exactly once, which can be useful for resource management such as IO. Graded types go further than this and allows one to specify the number of times a variable can be used. For example, in the following code1, the variable a needs to be used exactly twice.\ndup : forall {a : Type} . a [2] -\u0026gt; (a, a) dup [x] = (x, x) This gets more powerful when combined with dependent types. In the following types, where we use dependent types to express the length of a vector, and with that, we can now enforce our map function to be applied exactly n times on a vector of length n.\nmap : forall {a : Type, b : Type, n : Nat} . (a -\u0026gt; b) [n] -\u0026gt; Vec n a -\u0026gt; Vec n b map [_] Nil = Nil; map [f] (Cons x xs) = Cons (f x) (map [f] xs) Apart from fine-grained tracking of resource usage at compile-time, Granule also has features for tracking security levels, e.g. enforcing a resource to be private and not exposed to the outside world at compile time (similar to the private membership in OOP, but this one makes returning private resources, for example, fail at compile time). It also has features for tracking effects, such as open, write, read files, etc.\nSomewhat Dynamic Build Systems This talk is about the experience of building Buck2 at Meta. Build systems can be broadly classified as static (like make) and dynamic (like Excel2). A static build system has all the dependencies known upfront and will not change during compilation. For example, make has this information written down by the programmer and can hence construct dependency graphs for this. A dynamic build system takes into account possible changes that could happen at runtime and adapts its build process at it goes.\nThe speaker argues that usually there are two graphs in a build system, one is the target graph which is specified by the user in terms of dependency. The other one is the actual action graph, which is generated by the build system based on the target graph, and respects the dependencies in the action graph (in other words, the action graph). Usually there are more flexibilities in the action graph, for example, a build system can generate parallel actions or serial actions, as long as they respect the dependencies.\nIn a large scale system, working out all the dependencies can require quite some time, therefore a dynamic action graph gives more flexibility, hence leading to better performance, etc. The way I think about this is that rather than statically finding all the dependencies bottom up, we use a dynamic, top-down approach to find files to build on demand, saving us the time to look at the dependencies every time from scratch.\nThe Dafny Programming language and Static Verifier I thought an example3 would help illustrate the use case of Dafny:\nfunction fib(x: nat): nat { if x == 0 then 0 else if x == 1 then 1 else fib(x - 1) + fib(x - 2) } method fib_m(n: nat) returns (y: nat) requires n \u0026gt; 0 ensures y == fib(n) { var x:= 0; y := 1; var i := 1; while i \u0026lt; n invariant 0 \u0026lt; i \u0026lt;= n invariant x == fib(i-1) invariant y == fib(i) { x, y := y, x + y; i := i + 1; } } This is an example proof of the fib_m empirical method matching the \u0026lsquo;mathematical\u0026rsquo; definition of fibonacci function. Dafny provides Hoare logic style specification where requires specifies the precondition and ensures provides the postcondition. As with all Hoare-style proof, the tricky part is on loops. A loop invariant is generally needed to help Dafny to prove properties of the program, which requires manual effort to generate. Otherwise Dafny is generally quite automatic in terms of generating proofs, compared to some of the more manual theorem prover like Coq where one would often need to step through each instruction line by line and prove that the pre and post condition holds for each of them.\nNote the general pattern of making machine checked proofs: we first need to have a specification of the system, in this case a mathematical definition of the fibonacci number. And then an actual implementation, in this case the fib_m method. We then define an abstraction relation between the spec and the impl, in this case the y == fib(m) relates the state in the implementation to the spec. And finally prove that the implementation indeed simulates/refines the spec, by checking that if the abstraction relation holds for the initial state of the implementation (i.e. precondition holds), then the it should also hold for the final state of the program (i.e. postcondition holds). This means that at every step, our implementation is abstracted by our specification, and therefore by transitivity, if it is abstracted in the beginning, it would be abstracted in the end.\nIn my opinion, the selling point of Dafny is its automation, which enables its usage at Amazon. This powerful automation can potentially shorten the proof to code ratio to about 1:10, which is usually the other way round using other more manual theorem provers.\nFinding cheaper straightline instruction sequences more cheaply This talk is about developing a bytecode optimiser to optimisations on Ethereum Virtual Machine (EVM) bytecode. The motivation here is particularly strong since one more instruction in the bytecode means more gas and hence more money :) Their main approach is to develop peephole optimisations on the bytecode to reduce the number of instructions, making the code more compact. This tool is based on the unbounded superoptimization paper which provides a new way of encoding the program semantics in a way that can be understand by an SMT solver, and then the satisfiability of the SMT formula can be translated to the existence of a program p that implements the original source code.\nThis tool has the potential to save millions of pounds if it were used at the beginning when Ethereum was introduced. The ebso is the super optimiser implemented in OCaml.\nConclusion Overall, this was a great event, with lots of well-prepared and engaging talks. It was nice to see all the development in more advanced type theories and infrastructure (such as build systems), theorem provers, language constructs.\nAcknowledgement I would like to acknowledge that the content of this post is merely a paraphrase of the works done by speakers at S-REPLS 14. So please give all the credit to them.\nBoth examples taken from Granule project\u0026#160;\u0026#x21a9;\u0026#xfe0e;\nYou might be surprised as to why Excel is a build system. Well, different cells can be thought of as different files, and formulas linking different cells are build rules. More details in Build Systems a la Carte\u0026#160;\u0026#x21a9;\u0026#xfe0e;\nAdapted from Dafny tutorial.\u0026#160;\u0026#x21a9;\u0026#xfe0e;\n","permalink":"https://incipit0.github.io/pweb/posts/s-repls14/","summary":"\u003cp\u003eThis is a very high level summary of the experience attending\n\u003ca href=\"https://www.cl.cam.ac.uk/events/s-repls14/\"\u003eS-REPLS 14\u003c/a\u003e.\nFeel free to look at the respective website of the speakers for more details.\nTo keep this blog concise, I thought I would just use introductory examples where\npossible to illustrate the idea rather than going into the actual theory behind them.\u003c/p\u003e\n\u003ch2 id=\"graded-types-and-algebraic-effects\"\u003eGraded types and algebraic effects\u003c/h2\u003e\n\u003cp\u003eGraded type system is based on the idea of linear types. Linear types essentially\nallows one to restrict the usage of variables exactly once, which can be useful\nfor resource management such as IO. Graded types go further than this and allows one\nto specify the number of times a variable can be used. For example, in the following\ncode\u003csup id=\"fnref:1\"\u003e\u003ca href=\"#fn:1\" class=\"footnote-ref\" role=\"doc-noteref\"\u003e1\u003c/a\u003e\u003c/sup\u003e, the variable \u003ccode\u003ea\u003c/code\u003e needs to be used exactly twice.\u003c/p\u003e","title":"Core Dump of S-REPLS 14"},{"content":"When I first encountered the concept of an adjunction, I got quite confused as to what it is, and why it is useful: Just how on earth is a left adjoint of a functor? What\u0026rsquo;s the matter of a free and forgetful functor, why is free left to forgetful but not the other way round. That\u0026rsquo;s where this blog comes from. I hope this blogpost can help demystify adjunction for you a little bit.\nDisclaimer: I am still not very certain that I understand this concept very well so there might be mistakes/misunderstandings in this post. This is merely an attempt to understand adjunction more intuitively through many examples. Please read it with a grain of salt, and, as always, let me know if you spot a mistake.\nDefinition Let\u0026rsquo;s first define what an adjunction is. It turns out there are many equivalent definitions, I will be using this one1:\nAn adjunction between two categories \\(C\\) and \\(D\\) is specified by:\nfunctors \\(F\\) and \\(G\\) For each \\(X\\in C\\) and \\(Y \\in D\\) a bijection \\(\\theta_{X,Y} \\): \\(D(F X, Y) \\cong C(X,G Y) \\) which is natural in \\(X\\) and \\(Y\\). Roughly speaking, this means if \\(F \\dashv G\\) there is a morphism in \\(D: F X \\to Y\\), then we can find a morphism in \\(C: X\\to G Y\\).\nExamples of adjunctions We list a few examples of adjunctions:\nFree and forgetful The free functor is left adjoint to the forgetful functor \\(F \\dashv U\\). In this case the free functor takes a set and converts it into a list monoid whose list elements come from the set, and with the list concatenation and empty list as the monoid operation and identity element.\n$$ \\begin{prooftree} \\AxiomC{\\(\\Sigma \\to U(M,\\cdot, e)\\)} \\UnaryInfC{\\(F\\ \\Sigma \\to (M,\\cdot, e)\\)} \\end{prooftree} $$\nDue to the universal properties that when we can go from a set to (the set component of) a monoid, we can always apply that function to every element in the list and hence go from the list monoid to the monoid.\nDiagonal and product and coproduct The diagonal functor is left adjoint to the product functor \\(\\Delta \\dashv \\times\\)\n$$ \\begin{prooftree} \\AxiomC{\\(\\Delta C=(C,C) \\to (X,Y)\\)} \\UnaryInfC{\\(C \\to \\times(X, Y)=X\\times Y\\)} \\end{prooftree} $$\nThis is due to the terminal nature of a binary product in a category. We see that we have a morphism to go from \\(C\\) to \\(X\\times Y\\), then this implies we can go from \\(C\\) to \\(X\\) and \\(Y\\), which means there is indeed a unique morphism from \\(C\\) to the binary product (by the UP of binary product).\nSimilarly, the coproduct is actually left adjoint to the diagonal functor \\(+ \\dashv \\Delta \\dashv \\times \\).\nInitial and terminal object Suppose \\(!: \\mathbf C\\to \\mathbf 1\\) is a functor that maps an arbitrary category to the terminal category, then it is left adjoint to the functor that maps the unique object in the terminal category the terminal object to any other category \\(C\\).\n$$ \\begin{prooftree} \\AxiomC{\\(! C\\to *\\)} \\UnaryInfC{\\(C \\to U(*)\\)} \\end{prooftree} $$\nWhy is that the case? Looking at the required property: for each \\(\\overline f\\), we need to have a unique \\(f\\) that maps from any object \\(C\\) to the object \\(U(*)\\). \\(f\\) always exists since there is only one object in the terminal category, hence we need to have an always-existing \\(\\overline f\\) as well, which forces the \\(U(*)\\) to be terminal.\nSimilarly \\(F\\dashv\\ ! \\dashv U\\) where \\(F\\) maps an object \\(*\\) to the initial object.\nThoughts So what is the pattern here? The more left a functor is, the more initial it will be and dually, the more right a functor is, the more terminal its destination object would be.\nLooking at the diagram, we note that for each \\(f\\), there is a unique \\(\\overline f\\) such that the triangle commutes, and vice versa. If we were to fix \\(R\\) and try to find \\(L\\), then we would want \\(L X\\) to be as \u0026ldquo;initial\u0026rdquo; as possible, since an initial object, by definition, has a unique morphism to any other object in the category. Dually, if we fix \\(L\\) and want to find \\(R\\), then we would want \\(R X\\) to be as terminal as possible, since we want to go from \\(X\\) to \\(R Y\\).\nReferences Awodey, S. (2010). Category theory. Oxford University Press (2nd ed.).\nThanks to Andrew Pitts for his course on Category theory, which provides the foundation for this blog post.\u0026#160;\u0026#x21a9;\u0026#xfe0e;\n","permalink":"https://incipit0.github.io/pweb/posts/adjunctions/","summary":"\u003cp\u003eWhen I first encountered the concept of an adjunction, I got quite confused as\nto what it is, and why it is useful: Just how on earth is a left adjoint of a functor?\nWhat\u0026rsquo;s the matter of a free and forgetful functor, why is free left to forgetful\nbut not the other way round. That\u0026rsquo;s where this blog comes from. I hope this blogpost\ncan help demystify adjunction for you a little bit.\u003c/p\u003e","title":"Making sense of adjunctions"},{"content":"This article is very much my attempt to understand monad as a design pattern in programming languages and why on earth it is useful. There are tons of monad tutorials online, but I found relatively few ones argue for the usefulness of a monad by having side by side code examples that achieve similar functionalties. If you found one, please do let me know!\nI try to draw connections between monads\u0026rsquo; original mathematical definition and its actual usage so that this mysterious concept does not come out of the blue.\nProgramming Let\u0026rsquo;s start with programming, since I like to understand abstract concepts from concrete examples and then look at the abstract definition.\nIntuition We tend to think of Monads as wrapping some value up in a box, and each time we perform computation on the monad, we take it out, perform computation, and always put it back in the box. This is a bit like a pipeline where we\u0026rsquo;ve got a chain of workers ready to assemble a car. Each worker takes out the component of a car from the box first, carries out their amazing work, and put the (half-)assembled component back into the box before handing it to the next worker.\nNow we might wonder what is the point of a box? Why not just hand the item to the next worker which is much faster than unboxing and boxing? And indeed, handing the item to the next worker directly would be faster if the work performed is pure or guaranteed to return a valid result. If this is not the case, for example, a worker might get tired while assembling and decides to go back home for a nap, or he/she might get a defective component and not know what to do with it. In these cases, the worker would still try to send a special box (perhaps an box with an empty label on it). The pipeline at this stage would take care of this and by receiving the special box, it will tell the worker downstream that this car can no longer be assembled, everyone go home.\nI think this is what is usually meant by \u0026ldquo;monads can make the code structured in a controlled manner\u0026rdquo;, since workers in the pipeline won\u0026rsquo;t go into some random state where people are just panicing that they are not getting the component from their upstream worker, or maybe fighting against each other as they are don\u0026rsquo;t know what to do with the defective component. Instead, the monadic structure or the pipeline is controlled in a way that when an error happens, it goes straight to the workers and they know what to do with it.\nIn other words, monads, as a design pattern, encapsulates the error handling logic in a controlled manner. And we shall we in actual code it makes it easy to write clear code as well without repeatedly writing error handling code such as if statements or try-catch blocks. Eric Kow 1 puts it in a nice way like this:\n\u0026ldquo;do X and then do Y, where Y may be affected by X\u0026rdquo;\nYet another good place to use a monad is in asynchronous/event-driven programming. In this case, a monad encapsulates the results of the computation, which may be nothing (i.e. the computation is not done yet), or some result (i.e. the computaiton is finished and the result is returned). This way of representing futures/promises are nice because they allow us to chain computations together easier, whereas the normal approach would be to use a callback function, which can get quite cumbersome sometimes.\nDefinition Here I will be using OCaml for examples.\nThe defining feature of a monad is a return and a bind operation:\ntype \u0026#39;a monad = None | Some of \u0026#39;a val return: \u0026#39;a -\u0026gt; \u0026#39;a monad val (\u0026gt;\u0026gt;=): \u0026#39;a monad -\u0026gt; (\u0026#39;a -\u0026gt; \u0026#39;b monad) -\u0026gt; \u0026#39;b monad The first one constructs a monad from a value, i.e. puts a box outside the plain value, the second one allows us to chain computations on monads together, by taking a monad and an operation on the value inside the monad, giving back a new monad.\nUse cases Maybe monad The option type in OCaml is a monad:\nlet return x = Some x let (\u0026gt;\u0026gt;=) m f = match m with | None -\u0026gt; None | Some x -\u0026gt; f x And a good use of this is to handle exceptions, such as division by zero:\nlet mdiv x y = if y = 0 then None else Some (x / y) let x = 3 and y = 0 in Some x \u0026gt;\u0026gt;= fun w -\u0026gt; mdiv w y \u0026gt;\u0026gt;= fun y -\u0026gt; print_endline \u0026#34;I am y\u0026#34;; return (y + 1) \u0026gt;\u0026gt;= fun z -\u0026gt; print_endline \u0026#34;I am z\u0026#34;; return (z * 2) In this example, we would get a maybe monad with value None, without any printed values. In a normal case, we would need write code that looks like:\nlet x = 3 and y = 0 in x |\u0026gt; fun x -\u0026gt; x / y |\u0026gt; fun y -\u0026gt; print_endline \u0026#34;I am y\u0026#34;; y + 1 |\u0026gt; fun z -\u0026gt; print_endline \u0026#34;I am z\u0026#34;; z * 2 which, in my opinion, is similar enough to the monad version, except this new one would have an exception thrown. And most people dislike exceptions because they alter the control flow abruptly, which might cause all sorts of issues like resource leak, etc. If, however, we do want to handle the exception properly, we might need to do something like:\nlet x = 3 and y = 0 in x |\u0026gt; fun x -\u0026gt; try x / y with Division_by_zero -\u0026gt; None |\u0026gt; function | None -\u0026gt; None | Some y -\u0026gt; print_endline \u0026#34;I am y\u0026#34;; y + 1 |\u0026gt; function | None -\u0026gt; None | Some z -\u0026gt; print_endline \u0026#34;I am z\u0026#34;; z * 2 You see how this becomes cumbersome quite quickly. Sure, you can abstract the match | None | Some part into a new function and call this function instead of writing it out every time. But in doing so, you are essentially reinventing a monad\u0026rsquo;s bind operator. On the other hand, Monad abstracts all the error handling in the powerful \u0026gt;\u0026gt;= operator, and allows us programmers to stream our main processing logic in a succint way, with the additional benefit of no suprise exceptions.\nHaskell IO monad In the world of Haskell, they try to separate the pure and impure world as much as possible, using a technique called tainting. This essentially means that they taint anything that has an side effect, such as IO with some special type constructor, (for example, a string becomes a IO string, where IO acts like a type constructor). This is exactly the return function mentioned above. To get a monad, we lift up/wrap up/taint (or whatever you want to call it) the value so that it is now contained in a box, and this box cannot interact with other pure values. For example, you can\u0026rsquo;t contantenate a string with a IO string without first explicitly taking the value out of the box. This encourages programmers to do these two things (pure and impure) separately, and only cross use them when necessary. It\u0026rsquo;s also useful for compiler optimisation purposes since the compiler can know which part of the code is pure and perform certain optimisations.\nAsynchronous programming Monads are a common technique used in asynchronous programming as well, for example, OCaml\u0026rsquo;s Lwt and Async library relies heavily on monads, as is Rust\u0026rsquo;s Tokio library. Monads are useful as they allow chaining of operations that would otherwise be defined as many callbacks and passed as arguments to asynchronous functions. Another nice feature of monad is their boxy nature, since in asynchronous programming, we frequently needed to maintain the state of a promise/future without the help of having multiple threads each having their own stacks/registers. The scheduler/worker thread knows nothing about the state, and it only goes around each future/promise and executes them when they are ready to be executed. In this case Monads as a container with all the necessary state information encapsulated ready for the thread to actually carry out the computation.\nIt is also worth mentioning that monads simplifies error handling with asynchronous programming, similar to what we have seen above, where we would guarantee to get a None in the end without any random control flow changes. Moreover, in asynchronous programming, the usual try/catch method might not work as expected, since it might only catch the exception by the code executed synchronously within it.\nContinuation-parsing style Continuation parsing style (CPS) is a technique used in compilers so that the generated program can have certain properties, such as tail-call, explicit evaluation order, etc. In fact, we can indeed model CPS with monads, with the following definition:\nmodule type Res = sig type t end module Cps_mon (M: Res) = struct type result = M.t type \u0026#39;a cnt = \u0026#39;a -\u0026gt; result type \u0026#39;a cps_mon = \u0026#39;a cnt -\u0026gt; result let return (x: \u0026#39;a): \u0026#39;a cps_mon = fun (k: \u0026#39;a cnt) -\u0026gt; k x let (\u0026gt;\u0026gt;=) (cps: \u0026#39;a cps_mon) (f: \u0026#39;a -\u0026gt; \u0026#39;b cps_mon) = fun (k: \u0026#39;b cnt) -\u0026gt; cps (fun (x: \u0026#39;a) -\u0026gt; f x k) end This gives an example of a monad that is different from many of the monads we will see in a monad introductory tutorial, in that it is not a sum type (i.e. Some or None), but a function type (or an exponential object in a category). Note the bind operator for this CPS monad first abstracts the application of f into a new continuation, and then apply the old cps onto it before abstracting all of these into another cps, which is exactly the style of CPS that allows us to chain things together.\nAs an example usage, we can turn the following example2 of a cps style fib function:\nlet rec fib m = if m = 0 then 1 else if m = 1 then 1 else fib (m-1) + fib (m-2) let rec fib_cps m k = if m = 0 then k 1 else if m = 1 then k 1 else fib_cps (m - 1) (fun a -\u0026gt; fib_cps (m - 2) (fun b -\u0026gt; k (a + b))) into something that looks like this:\nlet open (module Fib_cps = Cps_mon(Int)) in let rec fib_cps (m: \u0026#39;a cps_mon): (\u0026#39;a cps_mon) = m \u0026gt;\u0026gt;= function | x when x = 1 || x = 0 -\u0026gt; return 1 | x -\u0026gt; fib_cps (return (x - 1)) \u0026gt;\u0026gt;= fun y -\u0026gt; fib_cps (return (x - 2)) \u0026gt;\u0026gt;= fun z -\u0026gt; return (z + y) In this example it might not be that obvious how monads are useful in simplifying our program, but we can see how the chaining helps us streamline our programs rather than trying to nesting functions.\nCategory theory A monad3 is defined to be \u0026ldquo;a monoid in the category of endofunctors\u0026rdquo;. Endofunctors refer to functors that map from a category \\(C\\) to the same category \\(C\\). A monoid in such a category is an object \\(M\\) with two morphisms \\(\\mu: M \\times M \\rightarrow M\\) and \\(\\eta: I \\rightarrow M\\) that satisfy certain properties. To me, a more straightforward way of defining a monad on a category \\(C\\) would be an endofunctor \\(T: C \\rightarrow C\\) with two natural transformations \\(\\eta: \\mathrm{id}\\rightarrow T\\) and \\(\\mu: T\\circ T\\to T\\) such that the following two diagrams commute:\n(Notation: \\(T \\eta\\) is a natural transformation, whose component at \\(x\\): \\((T \\eta)_x \\triangleq T(\\eta_x)\\), where \\(\\eta_x\\) is the component of the natural transformation \\(\\eta\\) at object \\(x\\), i.e. a morphism. \\(\\eta_T\\) is the natural transformation \\((\\eta T)_x \\triangleq \\eta _{T(x)}) \\)\nThe first diagram is saying that \\(\\eta\\) is the left and right identity of \\(T\\), and the second diagram is saying that \\(\\mu\\) is associative.4\nSo how does this relate to the monad we have been looking at in programming languages? \\(T\\) is the \u0026ldquo;box\u0026rdquo; we use to model the computation/contain effectual code, it is a functor because \\(T(X)\\) allows us to contain map/contain an element \\(X\\) inside it, or the actual value inside the box. We can even map morphisms with \\(T\\) as well, just as functions in a programming language is now encapsulated by the monad. \\(\\eta\\) is the natural transformation that lifts up a value \\(X\\) into the box \\(T(X)\\), a mechanism going from a pure value to a tainted value. Finally \\(\\mu\\) allows us to sequence computations on monads, since originally we have two boxes \\(T(T(X))\\), which are then combined into a single one.\nWe can see there is indeed a correspondence here, where \\(T\\) is corresponds to the box itself, and \\(\\eta\\) and \\(\\mu\\) enable the return and bind operations by lifting a vanilla value onto a box and combining multiple boxes into one.\nConclusion In summary, Monads are not magic that enable new features like async programming. One could pretty much do them without using Monad, but it would either be awkward, or the programmer might just invent something that is very similar to monad itself. It is worth stressing again that they are merely design patterns that make our life easier when writing code that deal with side effects/states.\nThere is a link to all the monad tutorials online. This is where I found Eric\u0026rsquo;s notes. I hope this blog can be posted onto this list one day 😊.\u0026#160;\u0026#x21a9;\u0026#xfe0e;\nExample take from Compiler Construction course.\u0026#160;\u0026#x21a9;\u0026#xfe0e;\nMany of the notations and concepts are based on the Category Theory course and notes by Andrew Pitts.\u0026#160;\u0026#x21a9;\u0026#xfe0e;\nIndeed this is exactly the law that monad needs to satisfy, as stated here.\u0026#160;\u0026#x21a9;\u0026#xfe0e;\n","permalink":"https://incipit0.github.io/pweb/posts/monad/","summary":"\u003cp\u003eThis article is very much my attempt to understand monad as a \u003cem\u003edesign pattern\u003c/em\u003e\nin programming languages and why on earth it is useful. There are tons of monad\ntutorials \u003ca href=\"https://wiki.haskell.org/Monad_tutorials_timeline\"\u003eonline\u003c/a\u003e, but I\nfound relatively few ones argue for the usefulness of a monad by having side\nby side code examples that achieve similar functionalties. If you found one,\nplease do \u003ca href=\"/\"\u003elet me know\u003c/a\u003e!\u003c/p\u003e\n\u003cp\u003eI try to draw connections\nbetween monads\u0026rsquo; original mathematical definition and its actual usage so that\nthis mysterious concept does not come out of the blue.\u003c/p\u003e","title":"Monad in programming and category theory"},{"content":"Motivation Mnesia is a soft real-time embedded Database Management System written for Erlang, a programming language that powers the infrastructures of various organisations such as Cisco, Ericsson and the NHS. Due to Mnesia’s tight integration with Erlang, it is also impactful in open source projects such as RabbitMQ and ejabberd.\nHowever, the development of Mnesia has remained stagnant for years, resulting in the lack of features such as automatic conflict resolution: Mnesia leaves the handling of conflicts after network partitions entirely to the developer. Moreover, as a distributed database, Mnesia only provides two extreme forms of consistency guarantee: transactions and weak consistency. Existing solutions to this problem are either external libraries or commercial standalone products, none of which is integrated into Mnesia natively. This means Erlang developers often have to introduce new dependencies into their codebase or resort to less ideal alternative databases.\nThe question we want to ask is whether it would be possible to introduce automatic conflict resolution into Mnesia, so that developers do not need to resolve this conflict each time there is a network partition. To understand how we can achieve this, we first need to understand how Mnesia works.\nMnesia Architecture Mnesia is built on top of Erlang’s built-in memory and disk term storage ets and dets. These term storage can be thought of as primitive storage engines that provide constant (or logarithmic) access time for large amounts of data [28]. They support different data structures for storing data, such as set, bag, etc. Internally, these are implemented as hash tables or balanced binary trees. Mnesia also provides additional functionalities such as transactions and distribution on top of ets and dets.\nA Mnesia cluster generally has a leaderless architecture where every replica can handle client requests. A cluster of Mnesia nodes are connected via the Erlang distribution protocol, which uses TCP/IP as its carrier by default, providing reliable in-order delivery. Moreover, the connection is transitive, which means the nodes form a cluster of fully connected nodes (or a mesh).\nAccess contexts and consistency models A central API provided by Mnesia for table manipulation is given below. A user calls the activity function which takes in an access context, a function to be executed, and a list of arguments. Currently supported access contexts include transactions and dirty operations.\nAn example of access to Mnesia using transactions is given below.\nmnesia:transaction(fun () -\u0026gt; mnesia:write({tab_name, k, v}), mnesia:read({tab_name, k}) end). Or using (asynchronous) dirty operations:\nmnesia:async_dirty(fun () -\u0026gt; mnesia:write({tab_name, k, v}), mnesia:read({tab_name, k}) end). The above two examples showcase the two consistency models provided by Mnesia: transactional ACID guarantee and weak consistency. The former is almost the strongest consistency guarantee in a distributed system, while the latter is the weakest. We start to see that there is something \u0026ldquo;intermediate\u0026rdquo; missing: perhaps an intermediate consistency model between these two extremes.\nEventual consistency Eventual consistency is defined as follows: ``If no new updates are made to the object, eventually all accesses will return the last updated value\u0026rsquo;\u0026rsquo;. This is a much weaker guarantee than transactions, but is still better than weak consistency.\nWhen designing an API for Mnesia with eventual consistency, an natural extension would be to add a new access context. For example:\nmnesia:async_ec(fun () -\u0026gt; mnesia:write({tab_name, k, v}), mnesia:read({tab_name, k}) end). This would allow developers to use this without too much refactoring, or how the API works underneath, so that we are free to choose the exact implementation strategy for eventual consistency. There are indeed many ways to achieve eventual consistency, but they typically involve several steps:\nDecide the replication protocol, e.g. master-worker, leaderless, etc Decide the anti-entropy protocol, e.g. gossip, read-repair, broadcasting. Choose a conflict resolution protocol, e.g. CRDTs, LWW, etc. The first two factors are very much determined by Mnesia design already, so we will focus on the last one, which Mnesia does not address. We will focus on CRDTs in this blog post since it is quite a popular choice for conflict resolution.\nCRDTs Conflict-free Replicated Data Types (CRDTs) are a family of replicated data types with a common set of properties that enable operations to be performed locally on each node while always converging to a final state among replicas if they receive the same set of updates. There are two types of CRDTs: state-based and operation-based (op-based).\nIntuitively, state-based CRDTs propagate their states during the communication (or the anti-entropy protocol) between replicas, while op-based CRDTs send the operations. For example, a state-based Set CRDT would send elements in the set as its state, while op-based Set sends the operation such as add and remove. These CRDTs have their own pros and cons. In short, state-based CRDTs put less constraint on the channel but have larger communication overhead. Op-based CRDTs often require causal broadcast but have lower communication cost since they are only sending the operations rather than the entire state.\nMnesia\u0026rsquo;s dirty operation has an immediate synchronisation model, i.e. when a client sends an operation to a replica, it is immediately sent to all the replicas in the cluster. Moreover, this process does not involve any inspection of the current state of the database, which is needed for most state-based CRDTs (and some op-based CRDTs as well). For these two reasons, op-based CRDTs are a bit more suitable for our purpose of extending Mnesia to support automatic conflict resolution, or in particular, pure op-based CRDTs. These CRDTs are designed to not inspect the current state of the database and only broadcast the operations (and the associated payload). We are going to use a pure add-wins set, which has the following requirements:\nOperations must be delivered reliably. Operations need to be delivered in causal order1. When there are causally concurrent addition and deletion, then add-wins semantics specifies that addition takes precedence over deletion. With these requirements, we can now enjoy the nice property provided by the op-based CRDTs:\nAny two replicas of an op-based CRDT eventually converge under reliable broadcast channels that deliver operations in delivery order \\(\u0026lt;_d\\).\nExample 1 Buffering operations The first example is a simple case where there are no conflicting operations. In the following diagram, initially three replicated nodes are holding the element x in a set-like data structure. Now when there is a partition happening between node A and node B, as well as node A and node C (indicated by dashed lines), the insertion operation of y at node B cannot be propagated to node A. Now the replicated database is in an inconsistent state since node A is holding different elements from node B and node C. When the partition heals, Mnesia reports an error message to the developer and asks them to resolve the conflict manually.\nHypermnesia solves this issue by buffering the operations during the partition. In the second diagram below, node A and node B are buffering the addition of y and z respectively. When the partition heals, the buffered operations are propagated to the other nodes. By the property of op-based CRDTs, as long as replicas receive the same set of operations, they are guaranteed to be in the same state. This nice property helps us resolve conflicts automatically and achieve eventual consistency.\nNow buffering alone is not enough to achieve automatic conflict resolution as there are more complex cases with, say, concurrent additions and deletions. And this is where we need the power of a CRDT. Let\u0026rsquo;s look at the next example.\nExample 2 Concurrent addition and deletion When a network failure happens, communication between nodes temporarily stops but is not long enough for the failure detector to act. Transactions will completely stall during this period. Although dirty operations can carry on, replicas might end up in different states due to out-of-order message delivery. For example, in the following diagram, there might be a network failure between node B to A, resulting in B\u0026rsquo;s add a being delayed. If messages are delivered as they arrive, then node A and node C will end up in an inconsistent state. This is because addition and deletion in a set do not commute, and the two purple operations (add and delete) are concurrent, and they are applied in a different order on node A and node C, resulting in different final states.\nIn order to achieve convergence, and hence eventual consistency, concurrent operations need to commute. The exact semantics of whether addition or deletion wins depends on the actual application, and to achieve convergence, it is sufficient to define consistent semantics across replicas. Add-wins semantics is presented here but the remove-wins semantics is similar. To achieve add-wins semantics with a pure op-based Set CRDT, we require deletions to only remove elements that causally precede it. In the following diagram, the deletion with timestamp [2,0,0] removes the element (a,[1,0,0]) which is causally lower, but not (a,[0,1,0]) which is causally concurrent.\nBenchmarks So how does Hypermnesia actually perform when compared to transactions and dirty operations. We can see this by running benchmarks. This benchmark is a modified version of Mnesia\u0026rsquo;s built-in benchmark, extended in order to support the new access context.\nWe compare the throughput and latency of three operations: dirty, transactions and ec (the new eventually consistent access context introduced by Hypermnesia). Two diagrams shown below represent the throughput and latency against the number of generators/clients per node, both of them are in log scale (because the difference between dirty and transactions is often too large). For throughput we see that if we add more clients to the system, the database scales with the increased clients and the throughput increases as well. For latency, there is increase in all three operations, due to the inevitable overhead of more clients, but this affects mostly the dirty operation since its original latency is already quite low, and small increases in overhead can give a large overall increase. EC operations generally stay stable as we add more clients, which is a desirable property.\nConclusion In conclusion, Mnesia is a distributed embedded database built in and for Erlang/OTP. Its tight integration gives its outstanding performance, but its lack of automatic reconciliation after partition is a major drawback. Here we introduced Hypermnesia, a native extension of Mnesia with a new async_ec API that provides eventual consistency and hence automatic conflict resolution, exploiting the power of CRDTs.\nAppendix There are plenty more I did not cover in this post, feel free to check out my full dissertation, the code repo and the two videos below.\nPresentation Demo Talk at Code BEAM 2023 For those who are not familiar with causal delivery, intuitively, this is just saying that a message cannot possibly be delivered if its causal predecessor has not been delivered. For example, if my message of this dish is so delicious depends on the fact that I previously received an image of the dish, then the other people must see the image before seeing my praise.\u0026#160;\u0026#x21a9;\u0026#xfe0e;\n","permalink":"https://incipit0.github.io/pweb/posts/hypermnesia/","summary":"\u003ch2 id=\"motivation\"\u003eMotivation\u003c/h2\u003e\n\u003cp\u003eMnesia is a soft real-time embedded Database Management System written for Erlang,\na programming language that powers the infrastructures of various organisations\nsuch as Cisco, Ericsson and the NHS. Due to Mnesia’s tight integration with Erlang,\nit is also impactful in open source projects such as RabbitMQ and ejabberd.\u003c/p\u003e\n\u003cp\u003eHowever, the development of Mnesia has remained stagnant for years, resulting\nin the lack of features such as automatic conflict resolution: Mnesia leaves\nthe handling of conflicts after network partitions entirely to the developer.\nMoreover, as a distributed database, Mnesia only provides two extreme forms of\nconsistency guarantee: transactions and weak consistency. Existing solutions to\nthis problem are either external libraries or commercial standalone products,\nnone of which is integrated into Mnesia natively. This means Erlang developers\noften have to introduce new dependencies into their codebase or resort to less\nideal alternative databases.\u003c/p\u003e","title":"Hypermnesia: Eventual Consistency in Mnesia"},{"content":"Introduction In this blogpost we build a distributed datalog engine that can process datalog queries such as the one below in a distributed fashion. The key idea mimics the usual dataflow programming idea such as MapReduce where we shard our data and create a dataflow-graph to specify the computation we want.\nlink(\u0026#34;a\u0026#34;,\u0026#34;b\u0026#34;). link(\u0026#34;b\u0026#34;,\u0026#34;c\u0026#34;). link(\u0026#34;c\u0026#34;,\u0026#34;c\u0026#34;). link(\u0026#34;c\u0026#34;,\u0026#34;d\u0026#34;). link(\u0026#34;d\u0026#34;,\u0026#34;e\u0026#34;). link(\u0026#34;e\u0026#34;,\u0026#34;f\u0026#34;). reachable(X, Y) :- link(X, Y). reachable(X, Y) :- link(X, Z), reachable(Z, Y). We will build our engine from first principle by looking at how we perform single node evaluation of datalog queries, and then extend it to multiple nodes.\nDatalog Datalog is a declarative logic programming language. It is rooted in the database systems community and was first developed in the eighties and early nineties.\nSyntax A datalog program is a collection of datalog rules, and each rule has the form:\nA :- B1, B2, ..., Bn. where A is called the head of the rule, and B\u0026rsquo;s constitutes the body of the rule. A and B are called atoms in datalog (notice this is a different use of the term atom from Prolog, where atoms are just constants), and each atom has the form pred_sym(term, term, ...), where pred_sym is the predicate symbol, or the name of the atom, and terms are the arguments. Each term can either be a constant, which starts with a lower case letter, or a variable, which starts with a capital letter. For example, reachable(X, Y) is an atom, with the predicate symbol reachable and two arguments, X and Y, both of which are variables.\nSingle node Before we carry out distributed evaluation, we first need to understand how a single-node datalog engine works. Briefly, each datalog query can be compiled into a relational algebra (RA) statement, and we apply these RA queries with a bottom-up fashion until we reach the fix point.\nDatabase \u0026amp; Relational algebra It turns out that each datalog query can be mapped to a corresponding relational algebra operator. For example:\nreachable(X, Y) :- reachable3(X, Z, Y). can be mapped to\n\\[ \\mathrm{reachable}(X, Y) = \\pi_{X,Z}(\\mathrm{reachable3}(X, Z, Y)) \\]\nAnd rules like\nreachable(X, Y) :- reachable3(X, Z, Y). can be mapped to\n\\[ \\mathrm{reachable} = \\pi_{X,Z}(\\mathrm{link}\\bowtie_{2=1}\\mathrm{reachable}) \\]\nBottom-up evaluation Bottom-up evaluation1 starts from, as its name suggests, the bottom, or the initial input data, and repeatedly work towards a fixpoint, which is the final results we are looking for.\nLet\u0026rsquo;s consider an example where we want to find out all connected edges in a graph (a.k.a. transitive closure), given some initial connections between nodes, for example:\na b c d link(a, b). link(b, c). link(c, d). reachable(X, Y) :- link(X, Y). reachable(X, Y) :- link(X, Z), reachable(Z, Y). Applying the rule in iteration one would give us two extra edges, i.e.\nlink(a, c). link(b, d). And applying the rule again gives us\nlink(a, d). And we have computed all edges of the graph (i.e. the transitive closure) in two iterations. The termination condition is that we no longer get new edges after applying these rules.\nThere is a indeed an algorithm for doing such bottom up evaluation called semi-naive evaluation2. Intuitively, this algorithm takes the initial input and apply the RA operators derived from the input datalog rules on them to obtain new data, or deltas. These deltas are then used as the new input in the next iteration to obtain deltas for iteration two. We carry on this process until we reach a fixpoint, i.e. we longer get new data when applying rules on the data.\nDistributed evaluation We have seen how Datalog can be evaluated on a single node. It is time to evaluate it in a distributed environment.\nWhy \u0026amp; How Now before diving into the details of how to do distributed evaluation of datalog queries, one might ask what good it brings to us if we do this, and why is distributed evaluation a good way to do this. The benefits of distributed computing is to offload the computation from one node to many others. This sounds attractive since we can now offload the computation from one node to many others. But it also brings numerous other problems including how to coordinate multiple nodes so that the final result is consistent with single node computation.\nThe parallelism we want to extract here is data-level parallelism, i.e. we wish to partition the input data into multiple copies and (ideally) ask workers to perform computation on each copy independently. As we have seen above that datalog queries can be compiled into relational algebra operators like projection and join, the remaining question is how we can do these RA operators in a distributed way. As you might have guessed, operators like projections can be mapped nicely to a subset of data, i.e. \\[\\pi_X(R_1 \\cup R_2 \\cup \\ldots \\cup R_n) = \\pi_X(R_1)\\cup \\pi_X(R_2)\\cup \\ldots \\cup \\pi_X(R_n)\\]\nThe tricky case is actually join, if we are not careful with how we partition the data, two tuples that might have been able to generate a new tuple could be partitioned into different parts and hence cannot be joined. To this end we deploy a distributed hash join technique, often used in the distributed database query evaluation.\nDistributed join Joins are tricker as we cannot just partition input arbitrarily since this might result in tuples that could have been joined together, such as link(a, b) and link(b, c) ending up at different nodes and therefore cannot be joined together.\nWe use a technique called simple distributed (hash) join to resolve this issue. This algorithm originates from the database community. In short, it performs hash on the attributes to be joined and splits tuples according to such hash. In such way, the tuples above (link(a,b) and link(b,c)) will be hashed into the same partition and therefore can be joined together, and similarly for other tuples. In this way, we can now distribute our join across multiple nodes and each of them can perform the join independently. We have achieved data parallelism through this combination of distributed join and other relational embarrassingly parallel relational algebra operators.\nArchitecture The Erlog itself follows a popular master-worker approach, a bit like Apache Flink. The master is responsible for coordinating the process of distributing work and supervising the progress of workers, while the worker performs their work independently and report back to the coordinator when finished.\nOne common problem in such a MapReduce-like system is stragglers. While computing a Datalog query, there are often tasks that have to be completed before other tasks can start. One straggler can sometimes slow down the entire pipeline of computations. To this end, we adopt a LATE scheduler that can estimate the progress of a task and then detect slow workers. Based on this estimation, it can launch tasks speculatively to reduce the impact of slow workers.\nLimitations During my experience of implementing Erlog, I observed several limitations of performing distributed computation using a MapReduce framework:\nShuffle phase is heavy weight, and there are a lot of data to be moved around with a workload like this. Repeated storeage of intermediate results on each node wastes a lot of memory, and also lots of memory bandwidth. To solve this issue, a shared memory system like Spark can be useful. Conclusion In this post we demonstrated a way of evaluating datalog queries in a distributed environment. Starting from how to evaluate a datalog query to extending it to distributed evaluation. There are actually many connections between datalog and the database query community, therefore lots of chances for research and optimisation to this existing approach. Hope you find this post interesting and perhaps try playing with Datalog yourself3. There are also production-ready single-node Datalog engines such as Soufflé which is also blazingly fast.\nAppendix Feel free to checkout the full dissertation for a more formal and complete description of the project and the code repo.\nAs opposed to top-down evaluation strategy, which starts from the goal and gradually work towards the given condition.\u0026#160;\u0026#x21a9;\u0026#xfe0e;\nThere is also a naive evaluation which uses the full data rather than deltas.\u0026#160;\u0026#x21a9;\u0026#xfe0e;\nThis post from my project supervisor is a more tutorial style guide on how to build a datalog engine. Check it out!\u0026#160;\u0026#x21a9;\u0026#xfe0e;\n","permalink":"https://incipit0.github.io/pweb/posts/erlog/","summary":"\u003ch2 id=\"introduction\"\u003eIntroduction\u003c/h2\u003e\n\u003cp\u003eIn this blogpost we build a distributed datalog engine that can process datalog\nqueries such as the one below in a distributed fashion. The key idea mimics\nthe usual\n\u003ca href=\"https://www.sigops.org/2020/the-remarkable-utility-of-dataflow-computing/\"\u003edataflow programming\u003c/a\u003e\nidea such as MapReduce where we shard our data\nand create a dataflow-graph to specify the computation we want.\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"color:#b0c4de;background-color:#282c34;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"\u003e\u003ccode class=\"language-erlang\" data-lang=\"erlang\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#ef8383\"\u003elink\u003c/span\u003e(\u003cspan style=\"color:#98c379\"\u003e\u0026#34;a\u0026#34;\u003c/span\u003e,\u003cspan style=\"color:#98c379\"\u003e\u0026#34;b\u0026#34;\u003c/span\u003e).\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#ef8383\"\u003elink\u003c/span\u003e(\u003cspan style=\"color:#98c379\"\u003e\u0026#34;b\u0026#34;\u003c/span\u003e,\u003cspan style=\"color:#98c379\"\u003e\u0026#34;c\u0026#34;\u003c/span\u003e).\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#ef8383\"\u003elink\u003c/span\u003e(\u003cspan style=\"color:#98c379\"\u003e\u0026#34;c\u0026#34;\u003c/span\u003e,\u003cspan style=\"color:#98c379\"\u003e\u0026#34;c\u0026#34;\u003c/span\u003e).\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#ef8383\"\u003elink\u003c/span\u003e(\u003cspan style=\"color:#98c379\"\u003e\u0026#34;c\u0026#34;\u003c/span\u003e,\u003cspan style=\"color:#98c379\"\u003e\u0026#34;d\u0026#34;\u003c/span\u003e).\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#ef8383\"\u003elink\u003c/span\u003e(\u003cspan style=\"color:#98c379\"\u003e\u0026#34;d\u0026#34;\u003c/span\u003e,\u003cspan style=\"color:#98c379\"\u003e\u0026#34;e\u0026#34;\u003c/span\u003e).\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#ef8383\"\u003elink\u003c/span\u003e(\u003cspan style=\"color:#98c379\"\u003e\u0026#34;e\u0026#34;\u003c/span\u003e,\u003cspan style=\"color:#98c379\"\u003e\u0026#34;f\u0026#34;\u003c/span\u003e).\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#00b1f7\"\u003ereachable\u003c/span\u003e(\u003cspan style=\"color:#dcaeea\"\u003eX\u003c/span\u003e, \u003cspan style=\"color:#dcaeea\"\u003eY\u003c/span\u003e) :\u003cspan style=\"color:#c7bf54\"\u003e-\u003c/span\u003e \u003cspan style=\"color:#ef8383\"\u003elink\u003c/span\u003e(\u003cspan style=\"color:#dcaeea\"\u003eX\u003c/span\u003e, \u003cspan style=\"color:#dcaeea\"\u003eY\u003c/span\u003e).\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#00b1f7\"\u003ereachable\u003c/span\u003e(\u003cspan style=\"color:#dcaeea\"\u003eX\u003c/span\u003e, \u003cspan style=\"color:#dcaeea\"\u003eY\u003c/span\u003e) :\u003cspan style=\"color:#c7bf54\"\u003e-\u003c/span\u003e \u003cspan style=\"color:#ef8383\"\u003elink\u003c/span\u003e(\u003cspan style=\"color:#dcaeea\"\u003eX\u003c/span\u003e, \u003cspan style=\"color:#dcaeea\"\u003eZ\u003c/span\u003e), \u003cspan style=\"color:#c1abea\"\u003ereachable\u003c/span\u003e(\u003cspan style=\"color:#dcaeea\"\u003eZ\u003c/span\u003e, \u003cspan style=\"color:#dcaeea\"\u003eY\u003c/span\u003e).\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eWe will build our engine from first principle by looking at how we perform single\nnode evaluation of datalog queries, and then extend it to multiple nodes.\u003c/p\u003e","title":"Erlog: A Distributed Datalog Engine"},{"content":" “Sir, give us this bread always.”\n\u0026ldquo;I am the bread of life; whoever comes to me shall not hunger, and whoever believes in me shall never thirst. \u0026quot;\nThe thief comes only to steal and kill and destroy. I came that they may have life and have it abundantly.\n","permalink":"https://incipit0.github.io/pweb/bread/","summary":"\u003cblockquote\u003e\n\u003cp\u003e“Sir, give us this bread always.”\u003c/p\u003e\n\u003cp\u003e\u0026ldquo;I am the bread of life; whoever comes to me shall not hunger, and whoever believes in me shall never thirst. \u0026quot;\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003cblockquote\u003e\n\u003cp\u003eThe thief comes only to steal and kill and destroy. I came that they may have\nlife and have it abundantly.\u003c/p\u003e\n\u003c/blockquote\u003e","title":"🥖"},{"content":"Feel free to reach me on:\n","permalink":"https://incipit0.github.io/pweb/contact/","summary":"\u003cp\u003eFeel free to reach me on:\u003c/p\u003e","title":"Contact"},{"content":"Hello, my name is Shuntian Liu, and I typically go by Vincent. But feel free to address me with either of the name :)\nI am a PhD student studying Computer Science @ University of Cambridge, supervised by Martin Kleppmann.\nI am interested in the development of distributed systems, database systems, virtualisation, as well as some theoretical parts such as model checking, type theory and denotational semantics.\nFeel free to reach out if you want to discuss anything in my posts.\n","permalink":"https://incipit0.github.io/pweb/about/","summary":"\u003cp\u003eHello, my name is Shuntian Liu, and I typically go by Vincent. But feel free to\naddress me with either of the name :)\u003c/p\u003e\n\u003cp\u003eI am a PhD student studying Computer Science @ University of Cambridge, supervised\nby \u003ca href=\"https://martin.kleppmann.com/\"\u003eMartin Kleppmann\u003c/a\u003e.\u003c/p\u003e\n\u003cp\u003eI am interested in the development of distributed systems, database systems,\n\u003ca href=\"https://github.com/xapi-project/xen-api\"\u003evirtualisation\u003c/a\u003e,\nas well as some theoretical parts such as model checking, type theory and denotational\nsemantics.\u003c/p\u003e\n\u003cp\u003eFeel free to reach out if you want to discuss anything in my posts.\u003c/p\u003e","title":"About"},{"content":"Denotational Semantics is a unique course offered by the Computer Lab at UoC. It can be intimidating at first glance but at the same time bring you lots of fun and frustration at the same time. In this blog post we will look at three proof techniques in domain theory and investigate the connections between them.\nIn this blog we look at three techniques that can be used to prove the fixpoint of a function, namely, Tarski\u0026rsquo;s fixpoint theorem, lfp1 + lfp2 and Scott induction. We will show that they are equivalent to each other by working on an example.\nlfp1 + lfp2 lfp1 and lfp2 are two properties of the least pre-fixed point of a function \\(f: D\\to D\\), where \\(D\\) is a poset. We define a pre-fixed point of \\(f\\) to satisfy the property that \\(f(d)\\sqcup d\\). And we denote the least pre-fixed point of \\(f\\), if it exists, to be \\(\\mathit{fix}(f)\\) which is specified by two properties:\n$$ \\begin{align} \\mathrm{(lfp1)}\\qquad \u0026amp; f(\\mathit{fix}(f)) \\sqsubseteq \\mathit{fix}(f) \\newline \\mathrm{(lfp2)}\\qquad \u0026amp; \\forall d\\in D. f(d)\\sqsubseteq d\\implies \\mathit{fix}(f)\\sqsubseteq d. \\end{align} $$\nlfp1 is saying that \\(\\mathit{fix}(f)\\) is a pre-fixed point of \\(f\\), while lfp2 says that it is least such. These two properties start from the definition of the least pre-fixed point, i.e. it is 1. pre-fixed; 2. least.\nTarski\u0026rsquo;s fixpoint Tarski\u0026rsquo;s fixpoint theorem states that for a function \\(f:D\\to D\\), where \\(D\\) is a domain, then \\(f\\) has a least pre-fixed point, given by:\n\\[ \\mathit{fix}(f) = \\bigsqcup_n f^n(\\bot) \\]\nMoreover, it is also a fixpoint of \\(f\\), which means that \\(f(\\mathit{fix}(f)) = \\mathit{fix}(f)\\), and because all fixpoints are also pre-fixed point, this makes it a least fixpoint 1.\nProving the Tarski\u0026rsquo;s fixed point theorem involves two parts: firstly, to prove that it is a pre-fixed point, i.e. lfp1, this involves induction on \\(f(\\bot)\\sqsubseteq \\bot\\) and using the monotonicity of \\(f\\). And to show it is a least one, we can show that \\(\\mathit{fix}(f)\\) is below any other pre-fixed point using the transitive property of the poset and the property that \\(\\sqcup_n f^n(\\bot) = \\sqcup_{n+1} f^{n+1}(\\bot)\\). We can use the same property to prove that \\(\\mathit{fix}(f)\\) is indeed a fixpoint in addition to being a pre-fixed point.\nIntuitively, Tarski\u0026rsquo;s theorem is saying that if we have a \\(\\bot\\) element, and if we have a continuous function \\(f\\), then we can always obtain the fixpoint of this function by repeatedly applying it to \\(\\bot\\) until we don\u0026rsquo;t see changes. We are guaranteed to arrive/terminate at the lub by the definition of a domain.\nScott induction Scott induction is a technique to prove that a fixpoint has a certain property. It says that if \\(f: D\\to D\\) is a continuous function on a domain \\(D\\). Then for any admissible subset \\(S\\subseteq D\\), then the following implication holds:\n$$ \\begin{prooftree} \\AxiomC{\\(\\forall d\\in D.(d\\in S \\implies f(d)\\in S)\\)} \\RightLabel{ \\(S\\) admissible} \\UnaryInfC{\\(\\mathit{fix}(f)\\in S\\)} \\end{prooftree} $$\nwhere admissibility is defined as every chain in \\(S\\) has a lub and \\(\\bot\\in S\\).\nIn other words, if we know that \\(f\\) preserves the property of \\(d\\in S\\), then we know that \\(f\\) preserves it to the lub, hence the least pre-fixed point \\(\\mathit{fix}(f) = \\sqcup_nf^n(\\bot)\\) which is obtained by repeatedly applying \\(f\\) to the bottom element (by Tarski\u0026rsquo;s theorem) \\(\\bot \\in S\\) must also be in \\(S\\). The existence of the bottom element is implied by the fact that \\(D\\) is a domain.\nExample question To better understand these theorems, we apply them to a example question:\nProblem statement Let \\(f, g:D\\to D\\) be continuous functions on domain \\(D\\). Prove\n$$ \\mathit{fix}(f\\circ g) = f(\\mathit{fix}(g\\circ f)) $$\nby showing\n\\(\\mathit{fix}(f\\circ g) \\sqsubseteq f(\\mathit{fix}(g\\circ f))\\) \\(f(\\mathit{fix}(g\\circ f)) \\sqsubseteq \\mathit{fix}(f\\circ g) \\) Using lfp1 + lfp2 Here is the proof for the first part $$ \\begin{prooftree} \\AxiomC{} \\UnaryInfC{\\(f(\\mathit{fix}(g\\circ f)) \\sqsubseteq f(\\mathit{fix}(g\\circ f))\\)} \\RightLabel{ definition of \\(\\mathit{fix}(g\\circ f)\\)} \\UnaryInfC{\\(f\\circ g(f(\\mathit{fix}(g\\circ f))) \\sqsubseteq f(\\mathit{fix}(g\\circ f))\\)} \\RightLabel{ definition of \\(\\mathit{fix}(f\\circ g)\\)} \\UnaryInfC{\\(\\mathit{fix}(f\\circ g) \\sqsubseteq f(\\mathit{fix(g\\circ f)}) \\)} \\end{prooftree} $$\nAnd the second part $$ \\begin{prooftree} \\AxiomC{} \\UnaryInfC{\\(g(\\mathit{fix}(f\\circ g)) \\sqsubseteq g(\\mathit{fix}(f\\circ g))\\)} \\UnaryInfC{\\(g\\circ f(g(\\mathit{fix}(f\\circ g))) \\sqsubseteq g(\\mathit{fix}(f\\circ g))\\)} \\RightLabel{ lfp2 of \\(\\mathit{fix}(g\\circ f)\\)} \\UnaryInfC{\\(\\mathit{fix}(g\\circ f) \\sqsubseteq g(\\mathit{fix}(f\\circ g))\\)} \\RightLabel{ \\(f\\) is monotine} \\UnaryInfC{\\(f(\\mathit{fix}(g\\circ f)) \\sqsubseteq f\\circ g(\\mathit{fix}(f\\circ g))\\)} \\RightLabel{ definition of \\(\\mathit{fix}(f\\circ g)\\)} \\UnaryInfC{\\(f(\\mathit{fix}(g\\circ f)) \\sqsubseteq \\mathit{fix}(f\\circ g)\\)} \\end{prooftree} $$\nThese proofs can be read either way, but it is better to read them \u0026ldquo;upwards\u0026rdquo; to see how we go from a conclusion to an axiom, using different proof techniques along the way.\nUsing Scott induction The hard part of Scott induction is usually to identify the set that is admissible and can be used to prove the property we want. For this question, the properties we want to prove are the less than relation, so we use this as our \\(S\\)\nDefine \\(S = \\{e\\ |\\ e\\sqsubseteq f(\\mathit{fix}(g\\circ f)) \\}\\) this is admissible because it is a downset, i.e. \\(\\downarrow f(\\mathit{fix}(g\\circ f))\\). Now we can apply Scott induction to prove \\(\\mathit{fix}(g\\circ f)\\in S\\): $$ \\begin{prooftree} \\AxiomC{\\(e\\sqsubseteq f(\\mathit{fix}(g\\circ f)) \\iff e\\in S\\)} \\UnaryInfC{\\(g(e)\\sqsubseteq g\\circ f(\\mathit{fix}(g\\circ f)) \\)} \\UnaryInfC{\\(g(e)\\sqsubseteq \\mathit{fix}(g\\circ f) \\)} \\UnaryInfC{\\(f\\circ g(e)\\sqsubseteq f(\\mathit{fix}(g\\circ f))\\iff f\\circ g(e)\\in S \\)} \\end{prooftree} $$\nDefine \\(S = \\{d\\ |\\ f(d)\\sqsubseteq \\mathit{fix}(f\\circ g)\\}\\), this set is the inverse image of the downset \\(\\downarrow \\mathit{fix}(f\\circ g)\\) under \\(f\\), denoted as \\(f^{-1}(\\downarrow \\mathit{fix}(f\\circ g)) \\), since we know that \\(\\downarrow \\mathit{fix}(f\\circ g))\\) is admissible, then so is its inverse image. $$ \\begin{prooftree} \\AxiomC{\\(d\\in S\\iff f(d) \\sqsubseteq \\mathit{fix}(f\\circ g)\\)} \\UnaryInfC{\\(f(g\\circ f(d))) \\sqsubseteq f\\circ g( \\mathit{fix}(f\\circ g))\\)} \\UnaryInfC{\\(g\\circ f(d)\\in S\\iff f(g\\circ f(d))) \\sqsubseteq \\mathit{fix}(f\\circ g)\\)} \\end{prooftree} $$\nUsing Tarski\u0026rsquo;s theorem The last method uses Tarski\u0026rsquo;s representation of \\(\\mathit{fix}(f)\\), and it is actually similar for both subquestions so we focus on the first one:\n$$ \\begin{align} f(\\mathit{fix}(g\\circ f)) \u0026amp;= f(\\bigsqcup_n (g\\circ f)^n(\\bot)) \\\\ \u0026amp;= f(\\underbrace{g\\circ f\\circ g\\circ f\\cdots g\\circ f(\\bot)}_{n}) \\\\ \u0026amp;= f\\circ g\\cdots\\circ f\\circ g(f(\\bot)) \\\\ \u0026amp;= \\bigsqcup_n(f\\circ g)^n(f(\\bot)) \\end{align} $$\nAnd note that \\(\\mathit{fix}(f\\circ g)=\\bigsqcup_n(f\\circ g)^n(\\bot))\\sqsubseteq \\bigsqcup_n(f\\circ g)^n(f(\\bot))) = f(\\mathit{fix}(g\\circ f))\\) since the \\(\\bot \\sqsubseteq f(\\bot)\\) and \\((f\\circ g)^n\\) is a continuous function.\nThe second subquestion just requires some massage of the form and we should get a similar form to the first one.\nRemarks The above three methods might look rather differnet, but in fact, they are related. I think it should always be possible, if we can prove something with one method, then we should be able to do it with other two methods. The lfp1+lfp2 method starts from the definition, and uses the properties of lfp. Tarski\u0026rsquo;s expansion actually \u0026ldquo;embeds\u0026rdquo; the two properties within one representation, i.e. \\(\\sqcup_n f^n(\\bot)\\). Scott induction tries to prove that \\(f\\) preserves some property. There is often something special about the property that makes our f preserve it, rather than something special about \\(f\\). Often this property has something to do with \\(f\\), since Scott induction\u0026rsquo;s premise is quite weak \\((d\\in S\\implies f(d)\\in S)\\), but there is a lot of freedom in choosing what \\(S\\) is.\nAside: applications of denotational semantics I want to give a few applications of an area that is as abstract as denotational semantics, in case you were thinking that this is just some useless abstract math:\nGive semantics to while loops without recursion. The denotational semantics of a language PCF (programming computable function) is based the existence of fixpoint. Otherwise the while loop cannot be defined without using some kind of recursive definition. If we want to exclude recursion as a \u0026ldquo;built-in\u0026rdquo;/first-class element of our semantics, then the fixpoint semantics becomes essential to us.\nProving contextual equivalence. Contextual equivalence of two programs can be thought of as two black boxes with pluggable holes in them. If they are contextually equivalent, then we cannot distinguish them by plugging in different values into the hole and observe the behaviour of the program (crucially, behaviour here includes both output non-terminating behaviour, i.e. the program steps into something that is not a value and cannot step further). One of the central results of this course is that if two terms have the same denotation, then they are contextually equivalent, i.e.\n$$ [\\![M]\\!] = [\\![N]\\!] \\implies M \\cong_{\\mathrm{ctx}} N $$\nThe converse is not true, though, due to the parallel or function.\nCompiler optimisation. Domain theory is also useful in compiler optimisation where, for example, we want to apply some data flow analysis and want to show that our algorithm terminates. Or if we want to do strictness analysis, an algorithm that helps transfrom CBN to CBV, we need to use Tarski\u0026rsquo;s construction to help us solve some of the equations.\nCRDTs (conflict-free replicated data types). These are some special replicated data types that guarantees convergence. Some of these data structures use something called a join semilattice, where every two states in it has a lub, to resolve conflicts.\nAcknowledgements This blog post is by no means my original idea, hence I want to acknowledge the inspiration from the excellent courses denotational semantics, optimising compilers. And also this and this exam question.\nAppendix 女生说话像 Denotational Semantics 的正确方式 话 换成 Denotational semantics 你好笨 换成 The question was elementary with many candidates achieving full marks. 不想去 换成 Proof of the Proposition on Slide 65 [NON-EXAMINABLE] 不行 换成 \\(M_1\\cong_{\\mathrm{ctx}} M_2 \\implies [\\![M_1]\\!] = [\\![M_2]\\!] \\) 随便 换成 \\(\\bot\\)（gives no information whatsoever) 你去忙吧 换成 Why does \\(w = f_{[B],[C]}(w)\\) have a solution solution? 我不会 换成 \\([\\![P]\\!] = \\mathit{por}: \\mathbb B_\\bot \\to \\mathbb B_\\bot \\to \\mathbb B_\\bot\\) in PCF 你好烦 换成 We will not give the proof of this proposition here. 要你管 换成 Thesis: All computable functions are continuous 滚（出去） 换成 \\(f(\\bigsqcup_n d_n) = \\bigsqcup_n f(d_n)\\) 帮我个忙 换成 \\(\\mathit{fix}(f) = f(\\mathit{fix}(f))\\) 你陪我 换成 \\(x\\in S \\implies f(x)\\in S\\) 气死我了 换成 \\(\\mathit{fix}(\\mathbf{fn}\\ x:\\tau.\\ x)\\) It actually took me a while to realise this. To elaborate on this, it is like 0 is the smallest positive real number, and since 0 is an integer and all integers are real numbers, 0 is also the smallest positive integer.\u0026#160;\u0026#x21a9;\u0026#xfe0e;\n","permalink":"https://incipit0.github.io/pweb/posts/fixpoint/","summary":"\u003cp\u003e\u003ca href=\"https://www.cl.cam.ac.uk/teaching/current/DenotSem/\"\u003eDenotational Semantics\u003c/a\u003e is\na unique course offered by the Computer Lab at UoC. It can be intimidating at first\nglance but at the same time bring you lots of fun and frustration at the same time.\nIn this blog post we will look at three proof techniques in domain theory and investigate the connections between them.\u003c/p\u003e\n\u003cp\u003eIn this blog we look at three techniques that can be used to prove the fixpoint\nof a function, namely, Tarski\u0026rsquo;s fixpoint theorem, lfp1 + lfp2 and Scott induction.\nWe will show that they are equivalent to each other by working on an example.\u003c/p\u003e","title":"Equivalence of proof techniques in proving the fixpoint properties"}]