A tutorial on functional programming based on Python

Source: Internet
Author: User
Many functional articles describe abstract functional techniques such as composition, pipelining, and higher-order functions. Unlike this article, it shows the imperative, non-functional code examples that people write on a daily basis, and the conversion of these examples into functional styles.

The first part of the article will rewrite some short data conversion loops into functional maps and reduces. The second part selects the longer loops, breaks them down into units, and then changes each unit into a functional one. The third part chooses a long continuous data transformation cycle, then decomposes it into functional pipeline.

Examples are written in Python because many people find python easy to read. To prove that functional techniques are the same for many languages, many examples avoid using Python-specific syntax: Map,reduce,pipeline.

When people talk about functional programming, they mention a lot of "functional" features. Refers to immutable data 1, first Class object 2, and tail call Optimization 3. These are language features that help with functional programming. Refer to Mapping (mapping), reducing (induction), piplining (pipeline), recursing (recursion), Currying4 (KLA), and use of higher-order functions. These are the programming techniques used to write functional code. Refer to Parallel 5, lazy computing 6, and deterministic. These are properties that facilitate functional programming.

Ignore all of them. You can use a sentence to describe the characteristics of functional code: avoid side effects. It does not depend on or change data other than the current function. All the other "functional" things originate from this. Take it as a guideline when you study.

This is a non-functional approach:

A = 0def increment1 ():  Global a  A + = 1

This is a function-style approach:

Def Increment2 (a):  return a + 1

Do not iterate on the lists. Use map and reduce.
Map (map)

Map accepts a method and a set as a parameter. It creates a new empty collection, invokes the incoming method as an argument for each element in the collection, and then inserts the return value into the newly created collection. Finally, the new collection is returned.

This is a simple map that takes a list of names and returns a list with the name length:

name_lengths = Map (len, ["Mary", "Isla", "Sam"]) Print name_lengths# = [4, 4, 3]

Next, this map will perform a square operation on each element in the incoming collection:

Squares = map (lambda x:x * x, [0, 1, 2, 3, 4]) Print squares# = [0, 1, 4, 9, 16]

This map does not use a named method. It is an anonymous and inline method defined using lambda. The lambda parameter is defined to the left of the colon. The method body is defined to the right of the colon. The return value is the result of the method body running.

The following non-functional code accepts a list of true names, and then replaces the real name with a randomly assigned codename.

Import random names = [' Mary ', ' Isla ', ' Sam ']code_names = [' Mr Pink ', ' Mr. Orange ', ' Mr Blonde '] for I in range (len (name s):  names[i] = Random.choice (code_names) Print names# = [' Mr Blonde ', ' Mr. Blonde ', ' Mr Blonde ']

(as you can see, this algorithm may give multiple spies the same secret code.) Hopefully not confused in the task. )

This can be rewritten with a map:

Import random names = [' Mary ', ' Isla ', ' Sam '] secret_names = map (lambda x:random.choice ([' Mr. Pink ',                      ' Mr Orange ', 
   ' Mr Blonde ']),          names)

Exercise 1. Try to rewrite the following code with map. It takes a list of real names as arguments, and then uses a more stable strategy to produce a code name to replace those names.

names = [' Mary ', ' Isla ', ' Sam '] for I in range (len (names)):  names[i] = hash (names[i]) Print names# = [630681979613 3686941, 8135353348168144921,-1228887169324443034]

(I hope the spy's memory is good enough not to forget the code in the execution of the task.) )

My solution:

names = [' Mary ', ' Isla ', ' Sam '] secret_names = map (hash, names)

Reduce (Iteration)

Reduce accepts a method and a set of parameters. Returns the result of iterating through all the elements in the container through this method.

This is a simple reduce. Returns the and of all the elements in the collection.

sum = reduce (lambda A, x:a + x, [0, 1, 2, 3, 4]) Print sum# = 10

X is the current element of the iteration. A is the summation and the value that the lambda returns on the previous element. Reduce () traverses the element. Each iteration performs a lambda on the current A and X and then returns the result as a for the next iteration.

What is a for the first iteration? No iterative results were passed before this. Reduce () uses the first element in the collection as a for the first iteration, and then begins the iteration from the second element. In other words, the first x is the second element.

This code notes the frequency with which the word ' Sam ' appears in the string list:

sentences = [' Mary read a story to Sam and Isla. ',       ' Isla cuddled Sam ', '       Sam chortled. ' Sam_count = 0for sentence In sentences:  Sam_count + = Sentence.count (' Sam ') Print sam_count# = 3

The following is written in reduce:

sentences = [' Mary read a story to Sam and Isla. ',       ' Isla cuddled Sam. ', '       Sam chortled. ' Sam_count = reduce (lambda A, x:a + x.count (' Sam '),          sentences,          0)

How does this piece of code initialize a? The starting point for ' Sam ' cannot be ' Mary read a story to Sam and Isla. ' Initial accumulation and specified by the third parameter. This allows the types of elements in the collection to be different from the accumulator.
Why is map and reduce better?

First, they are mostly one line of code.

The most important part of the iteration: the collection, operation, and return values, which are always in the same position in all maps and reduce.

The code in the loop may change the variables previously defined or the variables to be used later. As usual, map and reduce are function-type.

Four, map and reduce are element operations. Each time someone reads a for loop, they read the logic line-wise. There are few regular structures that can help understand the code. Instead, both map and reduce create blocks of code to organize complex algorithms, and readers can quickly understand elements and abstract them in their minds. "Well, the code is converting every element in the collection. Then combine the processed data into one output. ”

Map and reduce have many "good friends" that are convenient, and they are revisions to the basic behavior. such as Filter,all,any and find.

Exercise 2. Try rewriting the following code with Map,reduce and filter. Filter accepts a method and a collection. Returns the element in the collection that causes the method to return true.

People = [{' Name ': ' Mary ', ' height ': +},     {' name ': ' Isla ', ' height ': +},     {' name ': ' Sam '}] height_total = 0height _count = 0for people:  if ' height ' in person:    height_total + = person[' height ']    height_count + = 1 if h Eight_count > 0:  average_height = height_total/height_count   print average_height  # = 120

If this is tricky, try not to think about data manipulation. Consider the state that the data will go through, from the People Dictionary list to the average height. Do not attempt to bundle multiple transitions together. Put each one on a separate line and save the results in a well-named variable. Code can be run immediately after the condensed.

My plan:

People = [{' Name ': ' Mary ', ' height ': +},     {' name ': ' Isla ', ' height ': +},     {' name ': ' Sam '}] Heights = map (lambda x : x[' height ',       filter (lambda x: ' Height ' in x, people)) if Len (heights) > 0: From  operator import add  av Erage_height = reduce (add, heights)/Len (Heights)

Write declarative code, rather than command-style

The following program shows three car races. Each time the vehicle moves, each car may move or not move. Each move time program will print to the path of all cars so far. Five times after the game is over.

Here is an output for one time:

----- ------- -------- ----------- -------------

This is the program:

From random import random time = 5car_positions = [1, 1, 1] while time:  # Decrease Time-  = 1   print ' 
   for i in range (len (car_positions)):    # Move Car    if random () > 0.3:      car_positions[i] + = 1     # draw car
    print '-' * car_positions[i]

The code is command-style. A functional version should be declarative. Should describe what to do, not how to do it.
How to use

By binding code snippets into a method, you can make the program more declarative in flavor.

From random import random def move_cars ():  for I, _ in Enumerate (car_positions):    if random () > 0.3:      car_pos Itions[i] + = 1 def draw_car (car_position):  print '-' * car_position def run_step_of_race ():  Global  Time time -= 1  move_cars () def draw ():  print "for car_position in  car_positions:    draw_car (car_position) time = 5car_positions = [1, 1, 1] while time:  run_step_of_race ()  Draw ()

To understand this code, the reader only has to look at the main loop. "If time is not 0, run the next run_step_of_race and draw, under check time." "If the reader wants to understand more about the run_step_of_race or draw in this code, you can read the code in the method."

The comment is gone. The code is self-describing.

Refining code into methods is a very good and simple way to improve the readability of your code.

This technique is used in a method, but only as a normal sub-method, simply to package the code. According to the instructions, these codes are not functional. The method in the code uses the state, not the incoming parameter. The method affects the nearby code by altering the external variables, rather than passing the return value. In order to figure out what the method does, the reader must read each line carefully. If you find an external variable, you must find the source of it and find out which methods have changed it.
Removal status

The following is a functional version:

From random import random def move_cars (car_positions):  return Map (lambda x:x + 1 if random () > 0.3 else x,        CA r_positions) def output_car (car_position):  return '-' * car_position def run_step_of_race (state):  return {' Time ': state[' time '-1,      ' car_positions ': Move_cars (state[' car_positions ')} def draw (state):  print '  print ' n '. Join (Map (Output_car, state[' Car_positions ')) def race (state):  Draw (state)  if state[' time ']:    Race (Run_step_of_race (state)) race ({' Time ': 5,   ' car_positions ': [1, 1, 1]})

The code is still in the segmentation refinement method, but this method is functional. The functional approach has three flags. First, there are no shared variables. Time and Car_positions are passed directly into the method race. Second, the method accepts the parameter. Thirdly, there is no instantiation of the variable in the method. All data changes are done in the return value. Rece () uses the results of run_step_of_race () for recursion. One step at a time results in a state that is passed directly into the next.

Now there are two ways to zero () and one ():

def zero (s):  if s[0] = = "0":    return S[1:] def one (s):  if s[0] = = "1":    return S[1:]

Zero () takes a string s as a parameter, and if the first character is ' 0′ ', the method returns other parts of the string. If not, returns the default return value for None,python. One () does the same thing except that the first character requirement is ' 1′.

Imagine a method called Rule_sequence (). Accepts a string and a list that holds the rule method for zero () and one () mode. Invokes the first rule on a string. Unless none is returned, it will continue to accept the return value and invoke the second rule on string. Unless none is returned, it will accept the return value and call the third rule. Wait a minute. If one of the rules returns the None,rule_sequence () method stops, and returns none. Otherwise, returns the return value of the last rule method.

The following is an example output:

Print rule_sequence (' 0101 ', [Zero, one, zero]) # = + 1 Print rule_sequence (' 0101 ', [Zero, Zero]) # = None

This is the imperative version of Rule_sequence ():
This is a command-style version:

def rule_sequence (S, rules): For  rule in rules:    s = rule (s)    if s = = None: Break   return s

Exercise 3. The above code uses loops to complete the function. Use recursive rewriting to make it more declarative flavor.

My plan:

def rule_sequence (S, rules):  if s = = None or not rules:    return s  else    : Return Rule_sequence (Rules[0] (s), Rules[1:])

Using pipelining

In the previous chapters, some of the imperative loops were rewritten as recursive forms and used to invoke helper methods. In this section, another type of imperative loop is rewritten with pipline technology.

Below is a list of three sub-typical data, each of which holds three key-value pairs related to the band: name, inaccurate nationality, and activation status. The Format_bands method loops through the list.

Bands = [{' Name ': ' Sunset rubdown ', ' Country ': ' UK ', ' Active ': False},     {' name ': ' Women ', ' country ': ' Germany ', ' active  ': False},     {' name ': ' A silver Mt. Zion ', ' Country ': ' Spain ', ' Active ': True}] def format_bands (bands): For  band in Bands:    band[' country '] = ' Canada '    band[' name '] = band[' name '].replace ('. ', ') ' band[    ' name '] = band[' name ' ].title () format_bands (bands) Print bands# = = [{' Name ': ' Sunset rubdown ', ' active ': False, ' country ': ' Canada '},#   {' name ': ' Women ', ' active ': False, ' country ': ' Canada '},#   {' name ': ' A Silver Mt Zion ', ' active ': True, ' country ': ' Ca Nada '}]

Worried about the name of the method. "Format" is a very vague word. When you look at the code, these fears become crazy. Do three things in a loop. The value of the key value ' country ' is set to ' Canada '. The punctuation in the name was removed. The first letter of the name is changed to uppercase. But it's hard to see what the purpose of this code is and whether it does what it looks like. And the code is difficult to reuse, difficult to test and parallel.

Compare this with the following code:

Print Pipeline_each (bands, [Set_canada_as_country,              Strip_punctuation_from_name,              Capitalize_names])

This piece of code is easy to understand. It removes the side effects and the helper methods are functional because they appear to be chained together. The last output forms the input of the next method. If these methods are functional, then it is easy to verify. They are easy to reuse, test and easily parallel.

The work of Pipeline_each () is to pass bands, one at a time, to a conversion method such as Set_cannada_as_country (). After all bands have called This method, Pipeline_each () collects the converted bands. Then pass in the next method in turn.

Let's take a look at the conversion method.

Def Assoc (_d, key, value): From  copy import deepcopy  d = deepcopy (_d)  D[key] = value  return d def Set_cana Da_as_country (band):  return Assoc (band, ' Country ', "Canada") def strip_punctuation_from_name (band):  return Assoc (band, ' name ', band[' name '].replace ('. ', ')) def capitalize_names (band):  return Assoc (band, ' name ', band[' Name '].title ())

Each of these links a key to band to a new value. It is difficult to do without changing the original value. Assoc () solves this problem by using deepcopy () to generate a copy of the incoming dictionary. Each conversion method modifies the copy and then returns the copy.

It seems to be a good thing. The original band dictionary is no longer concerned because a key value needs to be associated with a new value to be changed. But the code above has two potential side effects. In Method Strip_punctuation_from_name (), the non-punctuation name is generated by calling the Replace () method on the original value. In the Capitalize_names () method, the first letter of the name is capitalized by calling title () on the original value. If replace () and title () are not functional, strip_punctuation_from_name () and capitalize_names () are not functional.

Fortunately, replace () and title () do not change the string they manipulate. Because the strings in Python is immutable. For example, when the replace () operation band The name string, the original string is copied, and the copied string is modified. Gee.

The variability of string and dictionaries in Python illustrates the appeal of languages such as Clojure. Programmers never have to worry about whether data is mutable. The data is immutable.

Exercise 4. Try rewriting the Pipeline_each method. Consider the order of operations. Each time a bands is taken out of the array, it is passed to the first conversion method. Then a similar one is passed to the second method. Wait a minute.

My Solution:
My plan:

def pipeline_each (data, FNS):  return reduce (lambda A, x:map (x, a),         FNS,         data)

All three conversion methods are due to changes to the specific fields of the incoming band. Call () can be used to extract this function. Call accepts a method that takes a parameter to invoke, as well as a value for the key to use when this method parameters.

Set_canada_as_country = Call (lambda x: ' Canada ', ' country ') Strip_punctuation_from_name = call (lambda x:x.replace ('. '), ' ' name ') Capitalize_names = call (Str.title, ' name ') print Pipeline_each (bands, [Set_canada_as_country,              Strip_ Punctuation_from_name,              Capitalize_names])

Or, if we want to be able to satisfy the simplicity of readability, then:

Print Pipeline_each (bands, [Call (lambda x: ' Canada ', ' country '), "call" (Lambda x:x.replace ('. ', '), ' name '), call ( Str.title, ' name ')])

Code for Call ():

Def Assoc (_d, key, value): From  copy import deepcopy  d = deepcopy (_d)  D[key] = value  return d def call (FN, Key):  def APPLY_FN (record):    return Assoc (record, Key, FN (Record.get (key)))  return APPLY_FN

There is a lot going in here. Let's take it piece by piece.

This piece of code does a lot of things. Let's see it at 1.1 o ' watch.

Call () is a high-order function. The higher order function takes a function as a parameter, or returns a function. or like call (), both have.

Second, APPLY_FN () looks much like those three conversion functions. It takes a record (a band), finds the value at the Record[key] position, calls FN with this value, specifies that the result of FN returns to the copy of the record, and returns the copy.

Third, call () did not do any practical work. When call is called, APPLY_FN () will do the actual work. In the example above using Pipeline_each (), an instance of APPLY_FN () changes the country value of the incoming band to "Canada". Another instance capitalizes the name of the incoming band in the first letter.

Iv. when a apply_fn () instance is running, FN and key will no longer be in scope. They are neither parameters of the APPLY_FN () nor local variables. But they can still be accessed. When a method is defined, the method saves a reference to the variables that the method contains: variables that are defined outside the scope of the method but used in the method. When the method runs and the code references a variable, Python looks for variables in the local and parameter. If you do not find it, you will find the variables stored in the closure package. That's where FN and key are found.

V. Bands is not mentioned in the call () code. Because no matter what the subject is, call () can generate pipeline for any program. Functional programming part of the purpose is to build a common, reusable, composable function library.

It's pretty dry. Closures, higher-order functions and variable scopes are included in the paragraph. Have a glass of lemon water.

You also need to do a bit of processing on the band. is to remove anything other than name and country on band. Extract_name_and_country () can pull out such information.

def extract_name_and_country (band):  Plucked_band = {}  plucked_band[' name '] = band[' name ']  plucked_band[ ' Country '] = band[' country ']  return plucked_band print Pipeline_each (bands, [Call (lambda x: ' Canada ', ' country '), C4/>call (Lambda x:x.replace ('. '), ' name '), call              (Str.title, ' name '),              extract_name_and_country]) # = = [{ ' Name ': ' Sunset rubdown ', ' Country ': ' Canada '},#   {' name ': ' Women ', ' country ': ' Canada '},#   {' name ': ' A Silver Mt Z Ion ', ' Country ': ' Canada '}]

Extract_name_and_country () can be written as a general function called pluck (). Pluck () can be used in this way:

Print Pipeline_each (bands, [Call (lambda x: ' Canada ', ' country '), call              (Lambda x:x.replace ('. ', '), ' name '),              Call (Str.title, ' name '),              pluck ([' name ', ' Country '])]

Exercise 5. Pluck () accepts a series of key values to extract data from the record according to these key values. Try to write. Higher-order functions are required.

My plan:

def pluck (keys):  def PLUCK_FN (record):    return reduce (lambda A, X:ASSOC (A, x, Record[x]),           keys,           {})  return PLUCK_FN

What's now?
Is there anything else you want to do?

Functional code works well with other styles of code. The converter in the article can be implemented in any language. Try experimenting with your code to implement it.

Think about Mary,isla and Sam. Turn the iteration of the list into maps and reduces operations.

Think about the car race. Break the code down into methods. Change those methods into functional ones. Turn the loop processing into recursion.

Think about the band. Rewrite a series of operations into pipeline.


1, a piece of immutable data refers to data that cannot be changed. Some languages are like Clojure languages, and the default values are all immutable. Any mutable operation is a copy value, and the copied value is modified and returned. This eliminates the bugs caused by incomplete state access in the program.

2. The language that supports the class-a function allows the function to be treated as if it were other types of values. means that methods can be created, passed to other methods, returned from methods, and stored in other data structures.

3, tail call optimization is a programming language feature. Each time the method is recursive, a stack is created. The stack is used to store the parameters and local values that the current method needs to use. If a method is recursively recursive, it is likely that the compiler or interpreter will consume all of the memory. A language with a tail call is optimized to support the sequence of the entire recursive call by reusing the same stack. Languages like Python do not support tail-call optimizations, which usually limit the number of recursive methods to the thousands of levels. In the race () method, it is only 5 times, so it is safe.

4. Currying means to decompose a method that accepts multiple parameters into a method that accepts only the first argument and returns a method that accepts the next argument until all parameters are accepted.

5, parallel meaning that in the case of different steps to run the same code at the same time. These concurrency operations are often run on different processors.

6, lazy Computing is the technology of the compiler, in order to avoid the need to run the code before the result.

7, only when each repetition can produce the same result, can say processing is deterministic.

  • Related Article

    Contact Us

    The content source of this page is from Internet, which doesn't represent Alibaba Cloud's opinion; products and services mentioned on that page don't have any relationship with Alibaba Cloud. If the content of the page makes you feel confusing, please write us an email, we will handle the problem within 5 days after receiving your email.

    If you find any instances of plagiarism from the community, please send an email to: info-contact@alibabacloud.com and provide relevant evidence. A staff member will contact you within 5 working days.

    A Free Trial That Lets You Build Big!

    Start building with 50+ products and up to 12 months usage for Elastic Compute Service

    • Sales Support

      1 on 1 presale consultation

    • After-Sales Support

      24/7 Technical Support 6 Free Tickets per Quarter Faster Response

    • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.