📜 ⬆️ ⬇️

Six programming paradigms that change your view of code

Periodically, I come across programming languages ​​that are so distinctive that they change my view of the code as a whole. In this article I would like to share some of my favorite finds.

Here you will not find the outdated message “functional programming will save the world!”; My list consists of far less popular titles. I bet many of the readers have never heard of most of the languages ​​and paradigms that will be discussed, so I hope you will be just as interesting to deal with them as I am.

Note: please note that I have very limited experience with most of these languages: the ideas on which they are built seem to me worthy of attention, but I cannot call myself an expert. Therefore, please point out errors and suggest corrections. And if you find any other ideas and paradigms that I missed, share!
')


Default competitiveness



Examples of languages : ANI , Plaid

Let's start with what really shakes the imagination: there are languages ​​in which competition is assumed by default. That is, all lines of code run parallel to each other!

For example, imagine that you wrote three lines of code — A, B, and C.

A; B; C; 

In most languages, A will be executed first, then B, and then C. But in ANI and others like it A, B and C will be executed simultaneously!

In ANI, controlling the flow and aligning the lines of code in a specific order is only a side effect of the dependencies that are written between different lines. For example, if B contains a reference to a variable that is defined in A, then A and C will be executed at the same time, and B - later, when A. is completed.

Let's take an example from ANI. As stated in the tutorial , programs written in ANI consist of pipes (pipes) and gate valves (latches) with which the flow of data is controlled. This unusual syntax is not so easy to understand, and the language itself seems to be dead, but it offers very interesting concepts.

Here is an example of implementing “Hello, world!” On ANI:

 "Hello, World!" ->std.out 

Using ANI-accepted terms, we send the “Hello, world!” (Text) object to the std.out stream. And what if you send some more text to std.out?

 "Hello, World!" ->std.out "Goodbye, World!" ->std.out 

Both of these lines will be executed in parallel, so the text will be output to the console in an arbitrary sequence. Now let's see what happens if we include the variable in one line and refer to it in the other.

 s = [string\]; "Hello, World!" ->s; \s ->std.out; 

The first line declares a valve (something like a variable) called s, which contains the text; the second line sends the text “Hello, world!” to s; the third one opens the valve to s and sends the content to std.out. Here you can observe how the ordering in ANI is implicitly happening: since each line depends on the previous one, the lines of code will be executed in the sequence in which they are written.

The Plaid language also, according to the creators' assurances, supports concurrency by default, however, it uses the permissions model (more in this article ) to configure flow control. Plaid also experiments with other interesting concepts, such as state-oriented programming, where state changes come to the fore: objects are defined not as classes, but as a series of states and transitions that the compiler can track. This approach seems to me entertaining - time in it is defined as a language construct of the first class. Rich Hickey wrote about this in his article Are we there yet .

Multi-core is now on the rise, and achieving competitiveness is still more difficult than we would like. ANI and Plaid offer a non-trivial approach to the problem, which could improve performance at times. The only question is whether the principle of "parallelism by default" will make managing competition easier.

Dependent types



Examples of languages : Idris , Agda , Coq

You are probably accustomed to systems represented in languages ​​such as C and Java, where the compiler can define the type of variable — an integer, list, or text. But what if he could define a variable as a “positive integer,” “two-point list,” or “text that is a palindrome”?

Languages ​​that support dependent types are built on this idea: you can specify types that will check the value of your variables at the compilation stage. As an experiment, the shapeless library for Scala has added partial (in other words, not quite yet refined) support for dependent types in Scala and offers an easy way to get acquainted with examples.

Here's how to declare a Vector that contains the values ​​1, 2, 3 in the shapeless library:

 val l1 = 1 :#: 2 :#: 3 :#: VNil 

Thus, a variable 11 is created, in the signature of which it is indicated not only that it is a Vector, which contains Ints, but also that the length of a vector is 3. The compiler can use this information to catch errors. Let's apply the vAdd method to a vector to create a pairwise addition of two vectors:

 val l1 = 1 :#: 2 :#: 3 :#: VNil val l2 = 1 :#: 2 :#: 3 :#: VNil val l3 = l1 vAdd l2 // Result: l3 = 2 :#: 4 :#: 6 :#: VNil 

In the above example, everything works fine, since the system knows that both vectors have a length of 3. However, if we tried to apply vAdd to vectors of different length, we would get an error already at the compilation stage, before execution!

 val l1 = 1 :#: 2 :#: 3 :#: VNil val l2 = 1 :#: 2 :#: VNil val l3 = l1 vAdd l2 // Result: a *compile* error because you can't pairwise add vectors // of different lengths! 

Shapeless is an excellent library, but as far as I know, it is still a bit damp: it supports only a small set of dependent types, it gives heavy code and signatures. Idris makes types an object of the first class in a programming language, as a result of which the system of dependent types is more accurate and more powerful. For a detailed comparison, read Scala vs Idris: Dependent Types, Now and in the Future .

Formal verification methods have existed for a long time, but they often turn out to be too cumbersome for general programmers to find them useful. Dependent types in languages ​​like Idris, and in the future, possibly Scala, can provide more compact and practical alternatives that will significantly expand the capabilities of the system in terms of detecting errors. Of course, no system of dependent types is able to catch all the errors that arise due to the limitations imposed by the problem of stopping. However, if used wisely, dependent types can be a significant leap for static type systems.

Concatenative programming



Examples of languages : Forth , cat , joy

Have you ever wondered how it would be to write code without variables and using functions? Me neither. But, obviously, some guys thought and as a result invented concatenative programming . The idea is that a language consists of functions that add data to the stack or throw data from the stack; programs are built almost exclusively with the help of a functional composition ( concatenation is the composition ).

It sounds foggy, so let's take a simple example from cat :

 2 3 + 

Here we add two numbers to the stack, and then call the + function, which throws both from the stack and adds their sum. The output of this code is 5. Now consider the example a little more interesting:

 def foo { 10 < [ 0 ] [ 42 ] if } 20 foo 

Let's sort each line:

  1. We first declare the function foo. Note that in cat the input parameters of the functions are not specified: all parameters are read from the stack.
  2. foo calls the function <, which adds the first number to the stack, compares it to 10 and adds the value True or False.
  3. Next, we add the values ​​0 and 42 to the stack, and we enclose them in brackets so that they are added without passing the test. The reason is that they will be used as the then and else branches, respectively, when calling the “if” function in the next line.
  4. The if function throws three things off the stack: a logical condition, a then branch, and an else branch.
  5. Finally, we add the number 20 to the stack and call the function foo.
  6. In the end, we get the number 42.

For a more detailed introduction you can refer to The Joy of Concatenative Languages .

This programming style has some interesting properties: programs can be split and combined in countless ways to create new programs. It is also worth noting the compressed syntax (even more compressed than that of LISP), which makes the program extremely concise and powerful support for metaprogramming. In my opinion, concatenative programming gives a lot of food for thought, but its practicality is doubtful. It seems that with this approach it is necessary to constantly remember or imagine the current state of the stack, instead of restoring it by the names of variables in the code, which makes it harder to make decisions

Declarative programming



Examples of languages : Prolog , SQL

Declarative programming appeared many years ago, but this concept is still not familiar to most programmers. The point is this: in most of the common languages ​​you describe how to solve a problem; in declarative languages, you simply indicate what result you want to come to, and the language itself determines what needs to be done for this.

For example, if you create a sorting algorithm in C from scratch, then you need to write instructions for merge sorting, which will be step by step written, how to recursively divide all data into two parts, and then merge them in the appropriate sorting order; Here is an example . If you sort the numbers in a declarative language like Prolog, instead of all this, you write the desired result: "I want to get a list of the same components, but the number at position i must be less than or equal to the number at position i + 1." Compare the solution on C, which was discussed above, with this code from Prolog :

 sort_list(Input, Output) :- permutation(Input, Output), check_order(Output). check_order([]). check_order([Head]). check_order([First, Second | Tail]) :- First =< Second, check_order([Second | Tail]). 

If you have ever used SQL, then you have experience in declarative programming, even if you may not have been aware of this. When you send a query of type select X from Y where Z, you describe the type of data you want to receive; How to execute this query is decided by the database engine itself. In most databases, you can use the explain command to look at the execution flow and get an idea of ​​how it all happened behind the scenes.

The beauty of declarative languages ​​is that they allow you to work at a higher level of abstraction. Your job is to write the specifications for the output you are looking for. For example, the code for a simple Sudoku game in Prolog simply indicates how the horizontal, vertical, and diagonal rows of a solved puzzle should look like:

 sudoku(Puzzle, Solution) :- Solution = Puzzle, Puzzle = [S11, S12, S13, S14, S21, S22, S23, S24, S31, S32, S33, S34, S41, S42, S43, S44], fd_domain(Solution, 1, 4), Row1 = [S11, S12, S13, S14], Row2 = [S21, S22, S23, S24], Row3 = [S31, S32, S33, S34], Row4 = [S41, S42, S43, S44], Col1 = [S11, S21, S31, S41], Col2 = [S12, S22, S32, S42], Col3 = [S13, S23, S33, S43], Col4 = [S14, S24, S34, S44], Square1 = [S11, S12, S21, S22], Square2 = [S13, S14, S23, S24], Square3 = [S31, S32, S41, S42], Square4 = [S33, S34, S43, S44], valid([Row1, Row2, Row3, Row4, Col1, Col2, Col3, Col4, Square1, Square2, Square3, Square4]). valid([]). valid([Head | Tail]) :- fd_all_different(Head), valid(Tail). 

Here's how to start the game described above:

 | ?- sudoku([_, _, 2, 3, _, _, _, _, _, _, _, _, 3, 4, _, _], Solution). S = [4,1,2,3,2,3,4,1,1,2,3,4,3,4,1,2] 

Unfortunately, there are drawbacks to this method: declarative languages ​​often face performance problems. A simple sorting algorithm, which we cited above, will most likely give O (n!); In the Sudoku game, the search is done by force, most developers have to include additional hints and indices in the subsystem to avoid costly and inefficient plans when performing SQL queries.

Symbolic programming



Example language : Aurora

The Aurora language is an example of symbolic programming . The code that you write on it and others like it, in addition to simple text, may include images, mathematical equations, graphs, and so on. This allows you to describe and work with a wide variety of data in their original format, instead of translating everything into text. In addition, Aurora is a highly interactive language that immediately shows you the result after each line of code, just like the REPL on steroids.

The Aurora language was created by Chris Granger , whose authorship also belongs to the Light Table IDE . He talks about the motivation that prompted him to invent Aurora, in his post Toward a better programming . He pursued goals such as making programming more visual, direct, and keeping random complication to a minimum. If you want to learn more, be sure to check out the materials from Bret Victor - Inventing on Principle , Media for Thinking the Unthinkable and Learnable Programming .

Knowledge based programming



Example language : Wolfram

Like the Aurora described above, the Wolfram language is also based on the principles of symbolic programming. But a symbolic layer is just a way to provide a stable interface for what makes up the core — that is, knowledge-based programming, a wide range of libraries, algorithms, and data built into the language. Thanks to this approach, you can easily and simply do anything: build graphs based on statistics on your Facebook account, edit pictures, watch the weather, process requests in natural language, plot routes on a map, solve equations, and so on.

Of all the existing languages, I think Wolfram has the most extensive “standard library” and data set. I am also very inspired by the idea that communication with the Internet community is becoming an integral part of the code writing process: this is reminiscent of the IDE, where the auto-complete function searches Google. It will be interesting to check whether the symbolic programming model is as flexible as Wolfram claims, and if it can properly use all this data.

Source: https://habr.com/ru/post/327898/


All Articles