f1
, f2
and f3
, which accept a number and return it increased by 1, 2, and 3, respectively. Also, each function generates a message that represents a report on the operation performed. def f1(x): return (x + 1, str(x) + "+1") def f2(x): return (x + 2, str(x) + "+2") def f3(x): return (x + 3, str(x) + "+3")
x
, in other words, we would like to calculate x+1+2+3
. In addition, we need to get a human-readable explanation of what each function has done. log = "Ops:" res, log1 = f1(x) log += log1 + ";" res, log2 = f2(res) log += log2 + ";" res, log3 = f3(res) log += log3 + ";" print(res, log)
res
and log
affect the readability of the code, making it difficult to follow the main logic of the program.f3(f2(f1(x)))
. Unfortunately, the data types returned by f1
and f2
do not correspond to the types of parameters f2
and f3
. But we can add new functions to the chain: def unit(x): return (x, "Ops:") def bind(t, f): res = f(t[0]) return (res[0], t[1] + res[1] + ";")
print(bind(bind(bind(unit(x), f1), f2), f3))
x=0
. Here, v1
, v2
and v3
are the values derived from the calls unit
and bind
.unit
function converts the input parameter x
to a tuple of number and string. The bind
function calls the function passed to it as a parameter and accumulates the result in the intermediate variable t
.bind
function. Now, if we have a function f4
, we simply include it in the chain: bind(f4, bind(f3, ... ))
def f1(x): return x + 1 def f2(x): return x + 2 def f3(x): return x + 3
x+1+2+3
. We also need to obtain a list of all values obtained as a result of the operation of our functions, that is, x
, x+1
, x+1+2
and x+1+2+3
.f3(f2(f1(x)))
returns the final result. But in this case, we will lose intermediate values. lst = [x] res = f1(x) lst.append(res) res = f2(res) lst.append(res) res = f3(res) lst.append(res) print(res, lst)
f4
, we again have to repeat this code to get the correct list of intermediate values. def unit(x): return (x, [x]) def bind(t, f): res = f(t[0]) return (res, t[1] + [res])
print(bind(bind(bind(unit(x), f1), f2), f3))
x=0
. Again, v1
, v2
and v3
denote the values resulting from the calls unit
and bind
.Employee
class with two methods: class Employee: def get_boss(self): # Return the employee's boss def get_wage(self): # Compute the wage
Employee
has a manager (another object of class Employee
) and a salary, which can be accessed through appropriate methods. Both methods can also return None
(the employee has no supervisor, salary is unknown).None
. print(john.get_boss().get_wage())
None
, our program will end with an error. result = None if john is not None and john.get_boss() is not None and john.get_boss().get_wage() is not None: result = john.get_boss().get_wage() print(result)
get_boss
and get_wage
. If these methods are hard enough (for example, accessing a database), our solution is no good. Therefore, change it: result = None if john is not None: boss = john.get_boss() if boss is not None: wage = boss.get_wage() if wage is not None: result = wage print(result)
if
. Therefore, we will try to use the same trick as in the previous examples. We define two functions: def unit(e): return e def bind(e, f): return None if e is None else f(e)
print(bind(bind(unit(john), Employee.get_boss), Employee.get_wage))
unit
function: it simply returns the input parameter. But we will leave it so that it will be easier for us to summarize our experience later.Employee.get_boss(john)
instead of john.get_boss()
.john.get_boss()
returns None
.f1
, f2
, …
, fn
. If their input parameters coincide in type with the results, we can use a simple chain of the form fn(… f2(f1(x)) …)
. The following diagram shows a generalized calculation process with intermediate results, denoted as v1
, v2
, …
, vn
.f1
we did some initialization. In the first example, we initialized a variable to store the common log, in the second, for a list of intermediate values. After that, we interleaved the function calls with some kind of linking code: we calculated the aggregate values, checked the result to None
.unit
and bind
. This pattern is called a monad . The bind
function contains the glue code while the unit
performs initialization. This allows you to simplify the final decision to one line: bind(bind( ... bind(bind(unit(x), f1), f2) ... fn-1), fn)
unit(x)
generates an initial value of v1
. Then bind(v1, f1)
generates a new intermediate value v2
, which is used in the next call to bind(v2, f2)
. This process continues until the final result is obtained. By defining different unit
and bind
within this template, we can combine various functions into a single chain of calculations. Monad libraries ( for example, PyMonad or OSlash, - approx. Transl. ) Usually contain ready-to-use monads (pairs of functions unit
and bind
) to implement certain compositions of functions.unit
and bind
must be of the same type as the bind
input parameters. This type is called monadic . In terms of the above diagram, the type of variables v1
, v2
, …
, vn
must be a monadic type.bind
calls look inelegant. To avoid this, we define another external function: def pipeline(e, *functions): for f in functions: e = bind(e, f) return e
bind(bind(bind(bind(unit(x), f1), f2), f3), f4)
pipeline(unit(x), f1, f2, f3, f4)
Source: https://habr.com/ru/post/445800/
All Articles