Functional programming language: Introduction to LISP/Scheme minor languages

1 Overview

Since Professor Qiu Zongyan translated " Structure and Interpretation of Computer Programs " (Structure and intepretation of Computer Programs, SICP after) the second edition, this introductory programming textbook MIT computer science began more and more attention to Chinese developers. At the same time, it is also concerned about the functional programming (Functional Programming) it introduces, and the Scheme language used in the examples.

Back in time 30 years ago, in 1975, Bill Gates and Paul Allen wrote the legendary BASIC version-they later sold to MITS in exchange for the "first pot of gold" BASIC version. In the same year, Gerald Sussman—he was the author of SICP—invented the Scheme language. In fact, Scheme is not a new language. To be precise, it is just a variant and a dialect of LISP. As early as 1958, John McCarthy began to study a language "used to process list data" -this is also the origin of the name LISP (LISt Processing). " List processing" at first glance is a rather specific problem, but in fact this type of problem has far-reaching and important connotations, and we will see the story here later.

 

2. Introduction to Scheme language features and examples

Compared with other dialects of LISP, the biggest feature of Scheme may be that it can be compiled into machine code. In other words, it runs more efficiently. In addition, Scheme can be said to be quite satisfactory in terms of language. The philosophy that LISP has always admired is " micro core + high scalability ", and Scheme has also taken this feature to its extreme. Scheme built-in keywords (keywords) are pitifully few, even operations such as greater than less than, addition, subtraction, multiplication, and division appear in the form of functions. It can even be an exaggeration to say that as long as there are define keywords and parentheses, all programs can be written. However, a side effect of this style is that there will be a lot of parentheses in the program, so some people call LISP "Lots of Irritating, Spurious Parentheses" (Lots of Irritating, Spurious Parentheses). For example, the following program is used to square a value:

(define (square x)

      (* x x))

(display (square 3))

For those of us who started with the C language and are accustomed to procedural programming (as opposed to "functional programming"), when we first came into contact with LISP/Scheme, the first touch was probably: Scheme does not distinguish between data And operation . Still taking the example of "squaring", "square of x" can be expressed as "using 1 as the base and multiplying by x" twice". If you use C++ language, this logic can be implemented like this:

int square(int x) {

      return 1 * x * x;

}

In Scheme, we can also achieve this:

(define (twice func base arg)

      (func base (func base arg)))

(define (square x)

      (twice * 1 x))

What are the characteristics of this realization? The biggest feature is that an operation (multiplication operation) is passed as a parameter. According to the "black word" of program design, if a program unit can be passed as a parameter and return value, then this unit is called a " first class " (first class) . In languages ​​such as C/C++/Java, although it is also possible to pass "operations" in the form of function pointers, functors, etc., it is packaged after all. In Scheme , another function can be directly passed as a parameter to the function, and It can be passed back to another function as a return value, and the function (that is, the "operation") is completely treated as a first-class citizen .

What are the benefits of this? In the above example, we abstracted the logic of "execute an operation twice" and got the double function. If we want to implement "do the addition operation twice with 0 as the base" (that is, "multiply by 2"), we only need to write:

(define (double x)

      (twice + 0 x))

The double function here is "a function that operates on other functions", and its result depends on what function is passed in as a parameter. Such "functions of functions" are called "high-order functions" in functional programming terms. The ability to naturally implement higher-order functions is the second important feature of Scheme. As mentioned earlier, the name LISP stands for "list processing". In fact, the ability to process list data comes from the use of higher-order functions. For example, we have a list like this:

{1, 2, 3, 4, 5}

For this list, we need to do two things:

1. Double each element to get a new list: {2, 4, 6, 8, 10}

2. Squaring each element to get a new list: {1, 4, 9, 16, 25}

Using Java language, we can achieve this:

List<int> doubleList(List<int> src) {

      List<int> dist = new ArrayList<int>();

      for(int item : src) {

            dist.add(item * 2);

}

return dist;

}

List<int> squareList(List<int> src) {

      List<int> dist = new ArrayList<int>();

      for(int item : src) {

            dist.add(item * item);

}

return dist;

}

The problem is clear at a glance: Except for the two bold lines of code, these two methods are almost completely duplicated. When you think about it, these two methods actually do very similar things: traverse a list and map each element of the original list to the new list according to a "certain rule". Because this "certain rule" is a mapping operation for elements, in order to abstract this list operation, we must implement a higher-order function to pass the actual mapping operation in the form of parameters. Therefore, in Scheme that can easily implement higher-order functions, the above logic is quite easy to implement:

(define (double-list src)

      (map double src))

(define (square-list src)

      (map square src))

Because of the power and convenience of higher-order functions in terms of operational logic abstraction, many people have begun to seek to treat operations as first-class citizens in "mainstream" procedural languages, and then realize higher-order functions. For example, C# allows methods to be passed as parameters or return values ​​in the form of delegates, and high-level operations such as find are added to the List<T> type; FunctionalJ in the Java world ( http://functionalj.sourceforge.net/ ) On the basis of the generics provided by Java5, commonly used list operations such as filter and map are provided. If the previous example is implemented with FunctionalJ, it can be written as:

// double and square are Function instances

List<int> doubleList(List<int> src) {

      return Functions.map(double, src);

}

List<int> squareList(List<int> src) {

return Functions.map(square, src);

}

 

3. Advantages and disadvantages

The phrase "do not distinguish between data and operation" is simple to say, but there is actually an important philosophical question behind it, that is, the question of "what is time". According to the concept of procedural programming, "time" is the operation of an internal variable, and the program records the instantaneous state of the system at various points in time in the form of local variables; and according to the concept of functional programming, "time" is the operation of external variables. The function is passed in in the form of parameters, and there is no local state inside the function, and there is no assignment operation. Or to put it more simply, calling the same function with the same parameters at any time will definitely get the same result. This property is called " referential transparency" . If the operation does not have referential transparency, it cannot be passed as a parameter or return value, because the calling environment and sequence may change the results of higher-order functions.

Programs with referential transparency have an additional benefit: they are inherently thread-safe . No matter how many threads there are and in what order they are accessed, as long as the program is referentially transparent, no additional thread synchronization mechanism is needed to ensure the correct results. This is particularly important in server-side applications, especially web applications, that face many users at the same time. Rod Johnson advocates "stateless Java server-side applications" in his book "J2EE Development without EJB", and enterprise application developers also benefit a lot from the idea of ​​functional programming.

It is said that LISP's original invention was quite unintentional: McCarthy only implemented an abstract grammar based on lambda operations and threw it aside, but his students found that writing programs in such a minimalist grammar had some fun. Some people say that computer scientists are a group of people who like reduction, but the invention of LISP proves from a practical point of view: basically all program structures can be reduced to lambda operations. According to the Church Calculus theory invented by Alonzo Church, all functions that can be calculated efficiently—including fixed-value functions—can be defined by lambda operations. For example, the data "0" and the operation "plus 1" can be defined by lambda operations as follows:

(define zero (lambda (f) (lambda (x) x)))

(define (add-1 n)
  (lambda (f) (lambda (x) (f ((n f) x)))))

On this basis, lambda operations can be used to define the entire natural number system. This is an extreme example. In many other places, LISP/Scheme can also analyze the concepts we are accustomed to in a similar way, allowing us to gain deeper insights. For example, in the second chapter of SICP " Structural Data Abstraction ", we saw with our own eyes: the usual " procedure-oriented programming " and " object-oriented programming ", to a large extent, are nothing more than different syntactic sugars using the same set of lambda operations. That's it . When programmers who are familiar with object-oriented learning Scheme, they often get some new understandings because they bridge the gap between "data" and "operation". In addition, the syntax of Scheme is extremely simple, and the most commonly used keywords are probably no more than 5, so it has a unique advantage as a teaching language-many schools use Java as a programming language for college students. When you look at these poor students Two months later, when you are still entangled with weird syntax such as "anonymous inner class" and complicated library such as "IO stream decorator", it is not difficult for you to understand what I mean.

But this simplicity has also become the biggest obstacle to the promotion of Scheme in enterprise applications: enterprise applications do not require many elegant possibilities, but a feasible solution. Although Scheme implementation versions such as PLT provide tool libraries such as XML and servlet, overly flexible syntax, lack of best practices, and lack of support from large vendors have made Scheme finally unable to become the mainstream of enterprise applications. However, although there are few real applications, ideas from functional programming still inspire enterprise application developers. For example, the continuation feature introduced by WebWork2.2 is derived from the concept of functional programming.

Last-but not the least-it should be stated: Although it is rarely seen in enterprise applications, LISP/Scheme is widely used in scientific computing, artificial intelligence, mathematical modeling and other fields, so it is called It is somewhat unfair to be a "minor language". On the whole, LISP/Scheme is better than writing algorithmic logic, but not good at I/O operations. Of course, we can say that this is the reason why it is frustrated in the enterprise application field, but why can't it be the result of it?

Guess you like

Origin blog.csdn.net/smilejiasmile/article/details/107644992