Further understanding of functional programming in Python

  • 2020-05-09 18:46:16
  • OfStack

We'd better start with the hardest question: "what exactly is functional programming (FP)?" One answer might say that FP is what you do when you program in languages such as Lisp, Scheme, Haskell, ML, OCAML, Clean, Mercury, Erlang (or whatever). This is a safe answer, but it does not quite clarify the question. Unfortunately, even function programmers have a hard time getting a sense of what FP really is. The story of "the blind men and the elephant" seems appropriate to describe this situation. It is also safe to compare FP with "command programming" (at least to a large extent) using operations such as C, Pascal, C++, Java, Perl, Awk, TCL, and most other languages.

On a personal level, I would describe functional programming roughly as having at least a few characteristics. Functional language makes these things easy and others difficult or impossible:

The       function is class 1 (object). That is, everything you can do with the "data" can be done using the function itself (such as passing one function to another).       USES recursion as the primary control structure. In some languages, there are no other "loop" constructs.       focuses on list LISt processing (for example, the name Lisp). Lists are often used with recursion 1 of sublists instead of loops.       "pure" functional language can avoid side effects. This does not include the most common pattern in command languages, which is to specify the first and then assign another value to the same variable to track program state.       FP does not encourage or allow statements at all, but instead USES expression evaluation (in other words, functions plus independent variables). In the pure case, a program is an expression (plus a supported definition).       FP CARES about what to calculate, not how to calculate. Many FP take advantage of "higher level" functions (in other words, functions that operate on some functions, and those that operate on others).

Advocates of functional programming argue that all of these features lead to faster development of shorter and less error-prone code. Moreover, advanced theorists in computer science, logic, and mathematics have found it much easier to demonstrate the formal performance of functional languages and programs than of command languages and programs.
Inherent Python function capability

Since Python 1.0, Python has most of the FP characteristics listed above. But for most of the Python features, they are presented in a very mixed language. Largely because of the OOP feature of Python, you can use what you want and ignore the rest (until you need it later). With Python 2.0, some nice "syntactic gloss" is added to the list connotation. While the list contains no new capabilities, they make many of the old capabilities look much better.

The basic elements of FP in Python are the functions map(), reduce(), and filter(), as well as the operator lambda. In Python 1.x, the apply() function is also convenient for applying the list return value of one function directly to another. Python 2.0 provides an improved syntax for this purpose. It may come as a surprise, but few of these functions (and the basic operators) are nearly enough to write any Python program; In particular, all flow control statements (if, elif, else, assert, try, except, finally, for, break, continue, while, def) can be handled in a functional style using only FP functions and operators. While eliminating virtually all flow control commands in a program might only be useful for joining the "chaotic Python" race (as compared to code that looks a lot like Lisp), it's worth understanding how FP USES functions and recursion to represent flow control.


Eliminate flow control statements

The first thing to consider when we perform the de-connection is the fact that Python "short-circuited" the evaluation of a Boolean expression. This provides the if/elif/else block of the expression version (assuming that each one calls a function, which is always possible). Here's how:
Listing 1. The "short circuit" condition call in Python


# Normal statement-based flow control 
  
if
   <cond1>: func1() 
  elif
   <cond2>: func2() 
  else
  :   func3() 
 
  # Equivalent "short circuit" expression 
(<cond1> 
  and
   func1()) 
  or
   (<cond2> 
  and
   func2()) 
  or
   (func3()) 
 
  # Example "short circuit" expression 
>>> x = 3 
>>> 
  defpr
  (s): 
  return
   s 
>>> (x==1 
  and
   pr(
  'one')) 
  or
   (x==2 
  and
   pr(
  'two')) 
  or
   (pr(
  'other')) 
  'other' 
>>> x = 2 
>>> (x==1 
  and
   pr(
  'one')) 
  or
   (x==2 
  and
   pr(
  'two')) 
  or
   (pr(
  'other')) 
  'two'

Conditional calls to the expression version seem to be little more than a positional trick; However, it would be more interesting if we noticed that the lambda operator had to return an expression. Because -- as shown earlier -- the expression can be short-circuited to contain the condition block, the lambda expression is very common in expressing the condition return value. Build on our example:
Listing 2. Lambda short-circuited in Python


>>> pr = 
  lambda
   s:s 
>>> namenum = 
  lambda
   x: (x==1 
  and
   pr(
  "one")) \ 
....     
  or
   (x==2 
  and
   pr(
  "two")) \ 
....     
  or
   (pr(
  "other")) 
>>> namenum(1) 
  'one' 
>>> namenum(2) 
  'two' 
>>> namenum(3) 
  'other'


Functions are class 1 objects

The example above already shows the function's place in class 1 of Python, but in a subtle way. When creating a function object using the lambda operation, we have something completely normal. Because of this, we can bind objects to the names "pr" and "namenum" in exactly the same way that we bind the number 23 or the string "spam" to these names. But just as we can use the number 23 without binding it to any name (in other words, like the function argument 1), we can use the function object created with lambda without binding it to any name. A function is just another value that we do something to in Python.

The main action we perform on class 1 objects is to pass them to the FP built-in functions map(), reduce(), and filter(). Each of these functions accepts a function object as its first independent variable.

      map() performs the passed function on each corresponding item in the specified list and returns a list of results.
      reduce() performs the passed function for each subsequent item, returning the internal accumulation of the final result; For example, reduce(lambda n,m:n*m, range(1,10)) means "factorial of 10" (in other words, multiply each term by the product of the previous multiplication).
      filter() USES the passed function to "evaluate" each item in the list, and then returns a list of items that have been screened and passed the passed function test.

We also often pass function objects to our own custom functions, but they are often equivalent to a combination of the built-in functions described above.

By combining the three built-in FP functions, you can perform an amazing 1 series of "flow" operations (none using statements, just expressions).


Function loops in Python

Replace loop with replace condition block 1 as simple. for can be converted directly to map(). For our conditional execution, we need to reduce the statement block to a single 1 function call (we are getting closer to the usual practice) :
Listing 3. The function 'for' loop in Python


for
   e 
  in
   lst: func(e) 
  # statement-based loop
map(func,lst)  
  # map()-based loop

In addition, there are similar techniques for functional methods of continuous program flow. That is, command programming usually involves something close to "do this, then do that, then do something else." Statements like this. map() lets us do exactly that:
Listing 4. The function in Python operates continuously


# let's create an execution utility function
do_it = 
  lambda
   f: f()
  # let f1, f2, f3 (etc) be functions that perform actions
map(do_it, [f1,f2,f3]) 
  # map()-based action sequence

In general, our entire main program can be the map() expression and the 1 series of functions that the program needs to execute. Another convenient feature of class 1 functions is that they can be placed in a list.

The conversion of while is a little more complicated, but it can still be done directly:
Listing 5. The function 'while' loop in Python


# statement-based while loop
  
while
   <cond>:
 <pre-suite>
 
  if
   <break_condition>:
 
  break
 else
  :
 <suite>
  # FP-style recursive while loopp
  
defwhile_block
  ():
 <pre-suite>
 
  if
   <break_condition>:
 
  return
   1
 
  else
  :
 <suite>
 
  return
   0
while_FP = 
  lambda
  : (<cond> 
  and
   while_block()) 
  or
   while_FP()
while_FP()

The conversion of while still requires the while_block() function, which itself contains statements rather than just expressions. But we need to do a step forward elimination of this function (for example, short circuit if/else in the template). Also, because the loop body (by design) cannot change any variable values < cond > It is difficult to use in 1-like tests, such as while myvar==7 (then, the whole thing will be changed in while_block()). One way to add a more useful condition is to have while_block() return a more interesting value, and then compare this return value with the termination condition. It's worth looking at 1 for a concrete example of these elimination statements:
Listing 6. The function 'echo' loop in Python


# imperative version of "echo()"
  
defecho_IMP
  ():
 
  while
   1:
 x = raw_input(
  "IMP -- ")
 
  if
   x == 
  'quit':
  
  break
 else
  print
   x
echo_IMP()
  # utility function for "identity with side-effect"
  
defmonadic_print
  (x):
 
  print
   x
 
  return
   x
  # FP version of "echo()"
echo_FP = 
  lambda
  : monadic_print(raw_input(
  "FP -- "))==
  'quit' 
  or
   echo_FP()
echo_FP()

What we have done is try to represent the applet involving I/O, loops, and conditional statements as a pure expression with recursion (in fact, as a function object that can be passed anywhere else if needed). We did still take advantage of the utility function monadic_print(), but this function is completely 1-like and can be reused in every function program expression we create later (it is a one-time cost). Note that any expression containing monadic_print(x) evaluates to the same result, as if it contained only x 1. FP (Haskell in particular) has a "single body" meaning for functions that do nothing and have side effects in the process.


Eliminate side effects

After eliminating the need for perfect, meaningful statements and replacing them with obscure, nested expressions, a natural question is: "why? !" All of my descriptions of FP are done using Python. But the most important feature -- and probably the most useful in this case -- is that it eliminates side effects (or at least has some effect on specific areas, such as monosystems). The vast majority of program errors -- and the problems that prompt programmers to resort to debugging to solve them -- occur because variables contain unexpected values during program execution. Function programs simply avoid this particular problem by not assigning values to variables at all.

Let's look at a fairly generic command code. Its purpose is to print out a list of pairs of Numbers whose product is greater than 25. The Numbers that make up each pair are themselves selected from the other two lists. This is similar to what programmers actually do in their program segments. The command method to achieve this purpose is as follows:
Listing 7. The Python code for the command "print a large product.


# Nested loop procedural style for finding big products
xs = (1,2,3,4)
ys = (10,15,3,22)
bigmuls = []
  # ...more stuff...
  
for
   x 
  in
   xs:
 
  for
   y 
  in
   ys:
 
  # ...more stuff...
  
   if
   x*y > 25:
  bigmuls.append((x,y))
  
  # ...more stuff...
# ...more stuff...
  
print
   bigmuls

The project is so small that nothing can go wrong. But our purpose may be embedded in code that does many other things at once. The parts annotated with "more stuff" are where side effects can cause errors to occur. At any one of these places, the variables xs, ys, bigmuls, x, y are likely to get unexpected values in the assumed abrief code. Furthermore, after executing this piece of code, all variables may have values of 1 that the code may or may not need later. Obviously, this type of error can be prevented using function/instance style encapsulation and scoping considerations. Also, you can always del them after executing variables. In practice, however, these types of errors are common.

The target's functional approach completely eliminates these side effects and errors. Here is a possible 1 piece of code:
Listing 8. Code for the function Python for "printing a large product.


bigmuls = 
  lambda
   xs,ys: filter(
  lambda
   (x,y):x*y > 25, combine(xs,ys))
combine = 
  lambda
   xs,ys: map(None, xs*len(ys), dupelms(ys,len(xs)))
dupelms = 
  lambda
   lst,n: reduce(
  lambda
   s,t:s+t, map(
  lambda
   l,n=n: [l]*n, lst))
  print
   bigmuls((1,2,3,4),(10,15,3,22))

In the example, we bind the anonymous (lambda) function object to the name, but this is not necessarily necessary. We can just nest definitions. This is done for readability purposes; But it's also because combine() is a good utility function that can be found anywhere (a list of all pairs of elements from two input lists). The subsequent dupelms() is mainly a way to help combine() work. Even if the 1 function example is more verbose than the command example, the new code in bigmuls() itself may be one less than the code in the command version, once utility functions are reused.

The real advantage of such a function example is that no variable changes any of its values. There are no possible unexpected side effects in the later code (and none in the earlier code). Obviously, the fact that it has no side effects on its own does not guarantee that the code is correct, but even so, it is an advantage. Note, however, that Python (unlike many functional languages) does not prevent the names bigmuls, combine, and dupelms from being re-bound. If combine() begins to make other sense later in the program, all efforts are wasted. You can gradually build an Singleton class to contain this type of immutable binding (for example, s.bigmuls, etc.); But this column does not cover that.

One issue of particular note is that our specific purpose was to customize the new features in Python 2. The best (and functional) technique is neither the command examples provided above nor the function instances, but:
Listing 9. The list of "bigmuls" contains the Python code


print
   [(x,y) 
  for
   x 
  in
   (1,2,3,4) 
  for
   y 
  in
   (10,15,3,22) 
  if
   x*y > 25]


conclusion

I have described the method used to replace each Python flow control construct with a functional equivalent (eliminating side effects in the process). A valid conversion to a particular program will bring some additional considerations, but we already know that the built-in functions are regular and complete. In a later column, we'll consider some more advanced functional programming techniques; I would like to explore more pros and cons of the functional style.


Related articles: