FP-21 functional programming

Posted by lexx on Tue, 08 Feb 2022 10:54:54 +0100

Lecture 21 functional programming

imperative programming

Procedural and object-oriented programming belong to the imperative programming paradigm

  • "How to do" needs to be described in detail, including operation steps and state changes.

  • They are consistent with von Neumann architecture. They are widely used programming paradigms and are suitable for solving most practical application problems.

declarative programming

Besides the declarative programming paradigm, there is also a kind of declarative programming paradigm

  • It only needs to describe "what to do", and how to do it is automatically completed by the implementation system.

  • With good mathematical theory support, it is easy to ensure the correctness of the program, and the designed program is relatively refined and has potential parallelism.

Typical representative of declarative programming paradigm:

  • Functional programming
  • Logical programming

What is functional programming

functional programming refers to organizing the program into a group of mathematical functions, and the calculation process is embodied in the evaluation of expressions based on a series of function applications (functions acting on data).

Functions are also treated as values (data), that is, the parameters and return values of functions can also be functions.

The theory based on is recursive function theory and lambda calculus.

Basic features of functional programming

  • "Pure" function (transparent reference): calling a function with the same parameters always gets the same value. (no side effects)

  • No state (data immutable): the calculation does not change the existing data, but generates new data. (no assignment operation)

  • A function is also a first class citizen: the parameter and return value of a function can be a function. (higher order function)

  • Recursion is the main control structure: repetition does not take the form of iteration (loop).

  • Lazy evaluation of expressions: only evaluate when necessary.

  • Potential parallelism.

Functional programming language

Pure functional language:

  • Common Lisp, Scheme, Clojure, Racket, Erlang, OCaml, Haskell, F#

Non functional languages that support functional programming:

  • Perl, PHP, C# 3.0, Java 8, Python, C++(11)

Examples of functional programming

calculation:

a * b + c / d
  • Imperative programming:
t1 = a * b;
t2 = c / d;
r = t1 + t2;
  • Functional programming:
multiply(...) { ... }
divide(...) { ... }
add(...) { ... }
... add(multiply(a,b),divide(c,d)) ...

Calculate the sum of a series of numbers:

a1 + a2 + a3 + ... + aN
  • Imperative programming:
int a[N] = {a1, a2, a3,..., aN},sum=0;
for (int i=0; i<N; i++) sum += a[i];
cout << sum;
  • Functional programming:
int sum(int x[], int n)
{ if (n==1) return x[0];
     else return x[0]+sum(x+1,n-1);
}
int a[N] = {a1, a2, a3,..., aN};
cout << sum(a,N);

Reverse order of calculation string:

"abcd" --> "dcba"
  • Imperative programming:
void reverse(char str[])
{ int len=strlen(str);
   for (int i=0; i<len/2; i++)
   {  char t=str[i];
      str[i] = str[len-i-1]; str[len-i-1] = t;
   }
}
  • Functional programming:
string reverse(string str) 
{ if (len(str) == 1) 
  	return str;
  else 
	 return concat(reverse(substr(str,1,len(str)-1)),substr(str,0,1));
}

Note: either written as a function or functional programming!

Calculate the number of even numbers in a series.

  • Imperative programming:
int a[N] = {a1, a2, a3,..., aN},count=0;
for (int i=0; i<N; i++) 
   if (a[i]%2 == 0) count++;
cout << count;
  • Functional programming
bool f(int x) { return x%2 == 0; } 
vector<int> v={a1, a2, a3,..., aN};
cout << count_if(v.begin(),v.end(),f); 

Basic means of functional programming

Recursion: realize repeated operation without iteration (loop).

Tail recursion: recursive call is the last operation of recursive function. (optimized)

filter/map/reduce

  • Filtering: select the elements that meet certain conditions in a set to form a new set.

  • Mapping: perform certain operations on each element in a set, and put the results into a new set.

  • Specification: get a value after performing an operation on all elements in a set.

bind: specify fixed values for some parameters of a function and return a function composed of parameters with unspecified values. (partial function)

currying: transform a function that accepts multiple parameters into a function that accepts a single parameter (the first parameter of the original function). This function returns a function that receives the remaining parameters.

recursion

For example, find the nth fibonacci number

  • Imperative scheme (iteration)
int fib_1=1,fib_2=1,n=10;
for (int i=3; i<=n; i++)
{	int temp=fib_1+fib_2;
	fib_1 = fib_2; fib_2 = temp;
}
cout << fib_2 << endl;
  • Functional scheme 1 (recursion)
int fib(int n)
{	if (n == 1 || n == 2)  return 1;
	else  return fib(n-2)+fib(n-1);
}
cout << fib(10) << endl;

Tail recursion

Functional scheme 2 (tail recursion)

int fib(int n, int a, int b)
{	if (n == 1)  return a;
	else  return fib(n-1,b,a+b); 
}
cout << fib(10,1,1) << endl;
  • Easy compiler optimization:
    • Since recursive call is the last step of this call, the stack space of this call can be reused during recursive call.

    • It can be automatically converted to iteration.

Tail recursion automatically turns to iteration

int fib(int n, int a, int b)
{	 while (true)
   { if (n == 1)  return a;
	    else  //return fib(n-1,b,a+b);
      { int t1=n-1,t2=b,t3=a+b;
         n = t1; a = t2; b = t3;
      }
   }
}

General form:

T f(T1 x1, T2 x2, ...)
{  ......
    ... return f(m1,m2,...);
    ......
    ... return f(n1,n2,...);
    ......
}

Convert to:

T f(T1 x1, T2 x2, ...)
{  while (true)
    { ......
       ... { T1 t1=m1; T2 t2=m2; ... 
              x1 = t1; x2 = t2; ... continue;} 
       ......
       ... { T1 t1=n1; T2 t2=n2; ... 
              x1 = t1; x2 = t2; ... continue;}
       ......
    }
}

Filter operation

For example, find all positive integers in a set of integers:

vector <int> nums={1,-2,7,0,3}, positives;
copy_if(nums.begin(),nums.end(),
	back_inserter(positives),[](int x){return x>0;});
for_each(positives.begin(),positives.end(),
	[](int x){ cout << x <<','; });

Map operation

For example, calculate the square of each integer in a set of integers;

vector <int> nums={1,-2,7,0,3}, squares;
transform(nums.begin(),nums.end(), 
	back_inserter(squares), [](int x){return x*x;});
for_each(squares.begin(), squares.end(),
              [](int x){cout<<x<<',';});

Reduce operation

vector <int> nums={1,-2,7,0,3};
int sum = accumulate(nums.begin(), nums.end(),
			0, [](int a,int x) {return a+x;});
cout << sum;

Use filtering / mapping / convention operation to realize: output the names and average ages of all girls in the students:

vector<Student> students,students_2;
vector<string> names;
copy_if(students.begin(),students.end(),
	     back_inserter(students_2),
   	[](Student &st) { return st.get_sex()==FEMALE;});

transform(students_2.begin(),students_2.end(),
		back_inserter(names),
	[](Student &st) { return st.get_name(); });

sort(names.begin(),names.end());
for_each(names.begin(),names.end(),
	[](string& s) { cout << s << endl; });

cout << accumulate(students_2.begin(),students_2.end(),0,
[](int a,Student &st) {return a+st.get_age();})/students_2.size();

bind operation

For a multi parameter function, in some application scenarios, some of its parameters often take fixed values.

You can generate a new function for such a function, which does not contain the parameters with fixed values specified in the original function. (partial function application)

Partial function can narrow the scope of application of a function and improve the pertinence of the function.

For example, for the following print function:

void print(int n,int base); //Output n by base specified

Since it is often used for decimal output, a new function print10 can be generated based on print. It only accepts one parameter n and the base is fixed to 10:

#include <functional>
using namespace std;
using namespace std::placeholders;
function<void(int)> print10=bind(print,_1,10);
print10(23); //print(23,10);
  • Fty2 bind(Fty1 fn, T1 t1, T2 t2, ..., TN tN);
    • fn is a function (or function object) with N parameters;

    • t1 ~ tN are the parameter values specified for function fn, where ti can be bound or unbound.

    • Use for unbound values_ 1,_ 2. ... and so on, in which the number represents the position of the parameter with unbound value in the new function parameter table;

    • bind returns a new function consisting of all parameters with unbound values.

For example:

#include <functional>
using namespace std; 
using namespace std::placeholders; 
                              //_ 1,_ 2,... The namespace in which the definition of is located
void product(double x, double y, double z)
{  cout << x << "*" << y << "*" << z << endl;
}
......
function<void(int,int)> f2=
bind(product,_1,4,_2);//bind returns a function with two arguments
f2(3,5); //Apply the function returned by bind to (3,5), output: 3 * 4 * 5
#include <functional>
using namespace std; 
using namespace std::placeholders; 
                              //_ 1,_ 2,... The namespace in which the definition of is located
void product(double x, double y, double z)
{  cout << x << "*" << y << "*" << z << endl;
}
......
function<void(int,int)> f2=
bind(product,_2,4,_1);//bind returns a function with two parameters
f2(3,5); //Apply the function returned by bind to (3,5), output: 5 * 4 * 3

For example:

#include <functional>
using namespace std; 
using namespace std::placeholders; 
bool greater2(double x, double y) { return x > y; }
auto is_greater_than_42=bind(greater2, _1, 42);
auto is_less_than_42=bind(greater2, 42, _1);
cout << is_greater_than_42(45); //true
cout << is_less_than_42(45); //false

Currying operation

Turn the function f(x,y) into a single parameter function h(x), h returns a function g(y):

#include <functional>
using namespace std;
int f(int x,int y) 
{ return x+y; 
}
function<int (int)>  h(int x) //The return value is a function object
{ return [x](int y)->int { return x+y; };
   //Equivalent to: return bind (F, x, _, 1);
}
......
cout << f(1,2);
cout << h(1)(2);

For example, using the above function h, a specific function can be generated for function f:

function<int (int)> add5=h(5); //add5(y)=5+y
......
int a,b,c;
......
... add5(a) ... //Return to a+5
... add5(b) ... //Return to b+5
... add5(c) ... //Return to c+5

Topics: C++ OOP Functional Programming