Many functional articles describe abstract functional techniques such as composition, pipelining, and higher-order functions. Unlike this article, it shows the command-line, non functional code examples that people write every day, and converts these examples into functional styles.
The first part of the article rewrites some short data conversion loops into functional maps and reduces. The second section selects a longer loop, decomposes them into units, and then changes each cell to a functional type. The third part chooses a long continuous data conversion cycle, and then decomposes it into a functional assembly line.
Examples are written in Python because many people think Python is easy to read. To prove that functional techniques are the same for many languages, many examples avoid using Python-specific syntax: Map,reduce,pipeline.
Guide
When people talk about functional programming, they mention a lot of "functional" features. Refers to immutable data 1, the first Class object 2 and the tail call Optimization 3. These are the language features of the help-functional programming. refers to mapping (mapping), reducing (induction), piplining (piping), recursing (recursion), Currying4 (KLA), and the use of higher-order functions. These are the programming techniques used to write functional code. Refer to parallel 5, inert computing 6 and deterministic. These are attributes that are useful for functional programming.
Ignore all of this. You can use a sentence to describe the features of functional code: avoid side effects. It does not rely on or change data other than the current function. All the other "functional" things come from this. When you study, take it as a guide.
This is a non functional method:
A = 0
def increment1 ():
global a
+ = 1
This is a functional approach:
Def Increment2 (a): return
A + 1
Do not iterate over the lists. Use map and reduce.
Map (map)
Map accepts a method and a set as parameters. It creates a new empty collection, invokes the Passed-in method with the elements in each collection as parameters, and then inserts the return value into the newly created collection. Finally, the new collection is returned.
This is a simple map that accepts a list of names and returns a list of name lengths:
name_lengths = Map (len, ["Mary", "Isla", "Sam"])
print name_lengths
# => [4, 4, 3]
Next, this map will square 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 uses an anonymous and inline method that is defined with a lambda. The parameters of the lambda are defined on the left side 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 code number.
Import random
names = [' Mary ', ' Isla ', ' Sam ']
code_names = [' Mr. Pink ', ' Mr Orange ', ' Mr. Blonde '] for
i in R Ange (len (names)):
names[i] = Random.choice (code_names)
print names
# => [' Mr. Blonde ', ' Mr Blonde ', ' Mr Blonde ']
(as you can see, this algorithm may be a secret code for multiple spies.) Hopefully it won't be confusing in the task. )
This can be overridden 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 rewriting the following code with a map. It accepts a list of real names as arguments, and then uses a more stable strategy to generate a code name to replace the names.
names = [' Mary ', ' Isla ', ' Sam '] for
i in range (len (names)):
names[i] = hash (names[i))
print names
# = > [6306819796133686941, 8135353348168144921,-1228887169324443034]
(Hope that the spy has a good memory, do not 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 as 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 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 elements. Each iteration, executes 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 iteration 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 remembers how often the word ' Sam ' appears in the string list:
sentences = [' Mary read a story to Sam and Isla. ', '
Isla cuddled Sam. ', '
Sam chortled. ']
Sam_count = 0 for
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 the ' Sam ' cannot be ' Mary read a story to Sam and Isla. ' The initial summation 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 a line of code.
Second, the most important part of the iteration: collections, operations, and return values, always in the same place in all maps and reduce.
Third, the code in the loop may change the variable that you defined previously or the variable you want to use later. As usual, map and reduce are function-style.
Four, map and reduce are element operations. Each time someone reads a for loop, they read the logic line by row. There are few regular structures that can help understand code. Instead, both map and reduce create blocks of code to organize complex algorithms, and readers can quickly understand the elements and abstract them in their minds. "Well, the code is transforming every element in the collection. Then combine the processed data into one output. ”
Five, map and reduce have many convenient "good friends", they are revisions of 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 ': 160},
{' name ': ' Isla ', ' height ': ', '
{' name ': ' Sam '}]
height_total = 0< C4/>height_count = 0 for person in
people:
if ' height ' in person:
height_total + = person[' height ']
Height_count + + 1
if height_count > 0:
average_height = height_total/height_count
print Average_ Height
# => 120
If this is tricky, try not to think about the operation on the data. Consider the state of the data to go through, from the People Dictionary list to the average height. Do not attempt to bundle multiple transformations together. Put each one on a separate line and save the result in a named-good variable. The code can be run and condensed immediately.
My program:
People = [{' Name ': ' Mary ', ' Height ': 160},
{' name ': ' Isla ', ' height ': '
{' name ': ' Sam '}]
Heights = map (Lam BDA x:x[' height ',
filter (lambda x: ' Height ' in x, people))
if Len (heights) > 0: From
operator import add< C7/>average_height = reduce (add, heights)/Len (Heights)
Write declarative code instead of an imperative
The following program shows three car races. Each time each move, each vehicle may move or not move. Each move time program prints the path of all cars so far. Five times after the game is over.
The following is an output of one time:
-
--
--
--
--
---
---
--
---
----
---
----
----
----
-----
This is the program:
From random import random time
= 5
car_positions = [1, 1, 1] while time
:
# Decrease
Time 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 the code fragment into the method, you can make the program more declarative flavor.
From random import random
def move_cars ():
for I, _ in Enumerate (car_positions):
if Random () > 0.3:
car_positions[i] + 1
def draw_car (car_position):
print '-' * car_position
def run_step_of_race ():
Global Time-
= 1
move_cars ()
def draw ():
print ' for
car_position in Car_ Positions:
Draw_car (car_position) Time
= 5
car_positions = [1, 1, 1] while time
:
run_step_of _race ()
Draw ()
To understand this code, the reader only needs to look at the main loop. "If time is not 0, run the next run_step_of_race and draw and check the 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.
Decomposing code into methods is a very good and simple way to improve the readability of your code.
This technique uses the method, but only as a regular child method, but simply packs the code. According to the instructions, the code is not functional. The method in the code uses the state instead of passing in the argument. Method affects the nearby code by changing the external variable, rather than by returning the value. To figure out what the method does, the reader must read each line carefully. If you find an external variable, you must find out where it came from and find out how to modify it.
Remove state
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,
car_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)
race ({' Time ': 5,
' car_positions ': [1, 1, 1]})
The code is still in the split refinement method, but the method is functional. The functional method 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. Third, there is no instantiation of the variable in the method. All data changes are completed in the return value. Rece () uses the results of run_step_of_race () for recursion. One step at a time produces a state, and the state is passed directly into the next.
Now, there are two methods, 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 an argument, and if the first character is ' 0′, the method returns the other part 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 of rule methods for storing zero () and one () patterns. Invokes the first rule on a string. Unless you return none, it will continue to accept the return value and invoke the second rule on a string. Unless you return none, it will accept the return value and invoke the third rule. Wait a minute. If there is a rule that returns the None,rule_sequence () method to stop and returns none. Otherwise, returns the return value of the last rule method.
Here is a sample output:
Print rule_sequence (' 0101 ', [Zero, one, Zero])
# => 1
print rule_sequence (' 0101 ', [Zero, Zero])
# => N One
This is the imperative version of Rule_sequence ():
This is an imperative version:
def rule_sequence (S, rules): For rule in
rules:
s = rule (s)
if s = None: Break return
s
Exercise 3. The code above uses loops to complete the function. Rewrite it with recursion to make it a more declarative flavor.
My program:
def rule_sequence (S, rules):
if s = = None or not rules: return
s
else: return
rule_sequence (Rules[0] (s), Rules[1:])
Use the assembly line
In previous chapters, some of the command-line loops were rewritten as recursive forms and used to invoke helper methods. In this section, you use the pipline technique to rewrite a different type of command loop.
Below is a list of three sub typical data, each of which holds a band-related three key pairs: 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
Ba nd 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 ': ' Canada '}]
Worry about the name from the method. "Format" is a very vague word. Looking at the code carefully, these fears become maddening. Do three things in the loop. The value of ' country ' is set to ' Canada '. The punctuation is removed from the name. The first letter of the name changed to uppercase. But it's hard to see what the code is for and whether it does what it seems to do. And the code is difficult to reuse, hard 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 goes beyond the side effects, and the helper methods are functional, because they seem 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 can be easily reused, tested and easily parallel.
The work of Pipeline_each () is to pass bands, pass one at a time, to a transformation method such as Set_cannada_as_country (). After all bands have called This method, Pipeline_each () collects the converted bands. Then, in turn, pass in the next method.
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_ Canada_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 one links a key of band to a new value. It is hard to do without changing the original value. Assoc () solves this problem by using deepcopy () to produce 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 worried 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 (), names without punctuation are generated by invoking the Replace () method on the original value. In the Capitalize_names () method, the first letter of the name is capitalized by invoking title () on the original value. If replace () and title () are not functional, strip_punctuation_from_name () and capitalize_names () are not function-type.
Fortunately, replace () and title () do not change the string they are manipulating. Because the strings in Python is immutable. For example, when the replace () operation band name string, the original string is copied first, and then 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 data variability. The data is immutable.
Exercise 4. Try rewriting the Pipeline_each method. Consider the order of operations. Each time you take a bands from an array, pass it to the first conversion method. And then pass it on to the second method. Wait a minute.
My solution:
My program:
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 feature. Call accepts a method to make arguments to invoke, and a value key is used when the 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 satisfy the readability of simplicity, then:
Print Pipeline_each (bands, [Call (lambda x: ' Canada ', ' country '), call
(Lambda x:x.replace ('. ', '), ' name '),
Call (Str.title, ' name ')])
Call () code:
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 '.
A, call () is a higher order function. Higher-order functions take a function as an argument, or return a function. or like call (), both.
Second, APPLY_FN () looks like the three conversion functions. It takes a record (a band), looks for the value in the Record[key] position, invokes FN with this value as the parameter, specifies that the result of FN is returned to the copy of the record, and then returns the copy.
Call () did not do any actual work. When call is invoked, APPLY_FN () does 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 first letter of the incoming band name.
Iv. when a apply_fn () instance is run, the FN and key are no longer scoped. They are neither parameters of APPLY_FN () nor local variables in them. But they can still be accessed. When a method is defined, the method holds a reference to the variables contained by the method: those that are defined in the method, but which are used in the method. When a method runs and the code references a variable, Python looks for variables in the local and parameter. If you don't find it, you'll find the variables stored in the closure. That's where the FN and key are found.
There is no mention of bands in the Call () code. Because no matter what the topic is, call () can generate pipeline for any program. Part of the purpose of functional programming is to build a general-purpose, reusable, and reusable library of functions.
It's a beautiful job. Closures, higher-order functions, and variable scopes are included in the paragraph. Have a glass of lemon water.
Also need to do a little processing on the band. is to remove anything except name and country on the 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 '), 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 Zion ', ' Country ': ' Canada '}]
Extract_name_and_country () can be written as a common 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 program:
def pluck (keys):
def PLUCK_FN (record): Return
reduce (lambda A, X:ASSOC (A, x, record[x)),
keys,
{}) return
PLUCK_FN
What now?
is there anything else you want to do?
Functional code can work well with other styles of code. The converter in the article can be implemented in any language. Try your code to implement it.
Think about Mary,isla and Sam. The iteration of the list will be turned into maps and reduces operations.
Think about the car race. Decompose the code into methods. Change those methods into functional ones. Turn the loop process into recursion.
Think about the band. Rewrite a series of actions into pipeline.
Marking:
1, a piece of immutable data is the data can not be changed. Some languages are like Clojure, and by default all values are immutable. Any variable operation is a copy value, and the value of the copy is modified and returned. This eliminates the bugs caused by incomplete state access in the program.
2. Languages that support first-class functions allow functions to be processed like other types of values. means that the method can be created, passed to other methods, returned from the method, 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 recursive too many times, it is likely that the compiler or interpreter will consume all memory. The language with the tail call optimization will support the entire recursive invocation sequence by reusing the same stack. Languages such as Python do not support tail-call optimizations, which usually limit the number of recursive methods at thousands of levels. In the race () method, only 5 times, so it is safe.
4, currying means to decompose a method that takes multiple parameters into a method that accepts only the first parameter and returns a method that accepts the next parameter until all parameters are accepted.
5. Parallelism means running the same code at the same time in different steps. These concurrent operations are often run on different processors.
6. Lazy computing is a compiler technique to avoid running code before the results are needed.
7, only when each repetition can produce the same result, can say the processing is deterministic.