Provide a detailed summary of the following web content, including what type of content it is (e.g. news article, essay, technical report, blog post, product documentation, content marketing, etc). If the content looks like an error message, respond 'content unavailable'. If there is anything controversial please highlight the controversy. If there is something surprising, unique, or clever, please highlight that as well: Title: Value-Oriented Programming Site: accu.org The research Val programming language uses value-oriented programming. Lucian Radu Teodorescu explores this paradigm. Robert C. Martin argues that we’ve probably invented all possible programming languages [ Martin11 ]. There are two dimensions we need to analyse programming languages by: syntax and semantic class. In terms of syntax, we’ve experimented just about everything; we’ve probably seen all types of syntax we can have. 1 If we look at the semantic class of a programming language – i.e., the paradigm that the language emphasizes – we don’t have too many choices. The approach that Robert C. Martin has on semantic classes is particularly interesting. He argues that a programming paradigm is best characterised by what it removes, not by what it adds; for example, structured programming is the paradigm that removes goto s from the language, constraining the direct transfer of control, which leads to programs that are easier to reason about. Imposing constraints on the possible set of programs that can be expressed in a language adds discipline to the language and can improve the language. In this article, we will build on this idea and show that we haven’t yet reached the end of the stack with the things we can remove from languages. We look at value-oriented programming , a programming paradigm that the Val programming language [ Val ] proposes, and how this paradigm can improve safety, local reasoning, and the act of programming. Programming paradigms and their constraints Let’s start by looking at how Robert C. Martin describes mainstream paradigms in terms of their restrictions. 2 Before doing that, let us issue a warning to the reader and soften some claims we will make. Whenever we say that a language restricts the use of a feature , we don’t actually mean that the language completely forbids it; it’s just that there is an overall tendency not to use that feature, even if the language allows it (directly or indirectly). In practice, languages don’t strictly follow a single paradigm. Moreover, the idea of a programming paradigm is an abstraction that omits plenty of details; we can’t have a clear-cut distinction between languages just by looking at their programming paradigms. Modular programming is a paradigm that imposes constraints on the source code file size. The idea is to disallow/discourage putting all the source code of a program into one single file. One can have the same reasoning applied to the size of functions. Doing this will enable us, the readers of the programs, to understand the code more easily; we don’t have to fit the entire codebase in our head, we can focus on smaller parts. Structured programming can be seen as a discipline imposed on direct transfer of control. We don’t use goto instructions, but rather construct all our programs of sequence, selection, or iterations. This allows us to easily follow the flow of the program, and manually prove the correctness of the code (when applicable). 3 Structured programming also allows (to some degree) local reasoning about the code; this is something of great interest for the purpose of this article. Object-oriented programming is a paradigm that adds restrictions on the use of pointers to functions and indirect transfer of control. In languages like C, to achieve polymorphic behaviour, one would typically use function pointers. In OOP, one would hide the use of function pointers inside virtual tables, which would be implementation details for class inheritance. This will make polymorphism easier to use, safer (fewer needs of casts) and it also allows us to easily implement dependency inversion. In turn, dependency inversion allows us to have loose coupling between our modules, making the code easier to reason about. Functional programming can be thought as a paradigm that imposes discipline on assignment of variables. If the assignment is not allowed, all values are immutable, which leads to function purity. Function purity allows easier reasoning about the functions (i.e., function results depend solely on their input arguments), improves safety, allows a set of optimisations that are not typically available with impure functions (removing unneeded function calls, adding memoisation, reordering of function calls, etc.) and allows code to be automatically parallelised. We can see a pattern here: we restrict the ability to write certain types of programs, we get some guarantees back from the language, and these guarantees can help us write better programs. Removal of features can be beneficial. To make a parallel, this is similar to governments that make laws to prevent certain (unethical) things to happen, but the removal of something that was previously allowed will make the society a better place. In general, switching from one programming paradigm to another is hard, as we have to change our mental model for reasoning about code; things that are common practice in one paradigm may be restricted in another. Both the strategies and the patterns that we use must change. That is why I prefer the syntagm programming paradigm rather than semantic class when discussing these categories of programming languages. Before looking at what restrictions the value-oriented programming paradigm instills, let’s first look at the main benefits of its restrictions: safety and local reasoning. Safety in programming languages There is a lot of confusion around the word safety in the context of programming languages, so I will spend some time to define it for this article. I will follow the line of thought from Sean Parent [ Parent22 ]. All programs consist of operations/commands; we will represent such an operation as C . Following Hoare formalism [ Hoare69 ], we can associate preconditions (noted as P ) and postconditions (noted as Q ) to these operations. Thus, the programs can be represented as sets of triplets of the form { P } C { Q }. For a correct program, in the absence of any errors, for all operations in the program, if the preconditions P hold, then, after executing C , the postconditions Q will also hold. For example, in C++, if i is a variable of type int , then for the operation i++ we can say that the precondition is that i is not the maximum integer value and that the postcondition is that the new value of i is one greater than its initial value. Let us look at all the possible cases there can be when we execute the operation in { P } C { Q } (see Table 1). If all the operations in a program are in the first scenario (i.e., both preconditions and postcondition hold) then we say the program is well-formed and correct. Typically, it is impossible for programming languages to guarantee that all the code expressible in the language falls under this scenario. The second scenario lays the emphasis on correctness in the presence of errors. 4 The program accepts the fact that there might be errors that can lead to failure to satisfy postconditions for all operations, but it’s important for these errors to be handled correctly. For example, it may be impossible for a program to make a network call to a server if the network cable is unplugged; if that is correctly treated as an error, the program is correct (assuming that everything else is also treated appropriately). It is outside the scope of this article to delve into what it means to have correct error reporting, but this is not something hard to do (see [ Parent22 ] for details). The bottom line is that error handling is the key to program correctness. The last case is the one in which we are trying to execute an operation, but the preconditions don’t hold. This case is about the safety of the language. The only way in which preconditions may fail is when the program is invalid (i.e. there is a bug); so safety in a programming language concerns its response to invalid programs. We call an operation safe if it cannot lead to undefined behaviour, directly or indirectly. If we have an operation that may corrupt memory, it can lead to crashes while executing the operation, or the program may crash at any later point, or the program may execute the code of any other arbitrary program, or the program may continue to function normally — the behaviour is undefined, so that operation is unsafe. We call an operation strongly safe if the program terminates when executing it if the preconditions of the operation don’t hold. That is, programs consisting of strongly safe operations will catch programming failures and report them as fast as they can. Operations that are safe but not strongly safe may result in unspecified behaviour. The operation can result in invalid values or can execute infinite loops. For example, the implementation of a square root function that is just safe can produce invalid values for negative numbers or can loop forever. Unlike undefined behaviour, though, unspecified behaviour is bounded by the normal rules of the language: no matter what else happens, the same program is still executing when that behaviour completes. Ideally, we want all our programs to be strongly safe ; but, unfortunately, strong safety is much harder to achieve than simple safety , because safety is a transitive property, while strong safety is not. An operation cannot contain undefined behaviour if it consists of a series of operations that cannot have undefined behaviour, which makes safety transitive. On the other hand, we can violate the preconditions of an operation without violating any of the preconditions of the operations it consists of. The non-transitivity of strong safety is illustrated by the C++ code in Listing 1. In this example, all the operations are safe , thus calling floorSqrt cannot lead to undefined behaviour. We have a precondition that the given argument must be positive. But passing a negative number to this function will not invalidate the preconditions of any operation in the function. 5 All the operations inside the function have their preconditions hold, but overall, the precondition doesn’t hold. In this case, passing a negative value as an argument can led to an infinite loop. To conclude, strong safety is hard to achieve systematically, but we can achieve safety systematically. A programming language should aim at allowing only safe programs (unless explicitly overridden by the programmer). To become safe , a language must add restrictions on the operations that can lead to undefined behaviour. Operations that lead to undefined behaviour in C++ are the ones that contain: memory safety violations (spatial and temporal), strict aliasing violations, integer overflows, alignment violations, data races, etc. Locally and globally detectable undefined behaviour Undefined behaviour can be of two types, depending on the amount of code we need to inspect in order to detect it: 6 Locally detectable , if we can detect it by analysing the operation in question, or surrounding code. Examples: integer overflow, alignment violations, null-pointer dereference, etc. Globally detectable , if we need to analyse the whole program to detect safety issues. Examples: most memory safety violations, data races, etc. Locally detectable undefined behaviour can be easily fixed in a programming language. The language can insert special code to detect violations and deal with them. Globally detectable undefined behaviour is trickier to deal with because it cannot be easily detected. In an unsafe language, one needs a certain discipline to ensure that this kind of undefined behaviour cannot occur. Listing 2 shows a C++ function that, at first glance, seems perfectly fine. But, on the caller side, we use the function in a way that will cause undefined behaviour. The push_back call might need to reallocate memory, move all the objects contained in the vector and might invalidate all the pointers to the original objects; if the given object is part of the original vector memory, then we would be accessing an invalid object. This is clearly a bug and it cannot be detected only by looking at the function that has the undefined behaviour. We need to also look at all the possible ways this function is called. Listing 3 shows another case in which we access invalid memory. Here, in a similar way, we take a string_view object from an object that is part of our vector. The ownership of the actual string data belongs to the Person object, which belongs to the vector. But, while utilising the string_view object we are changi