<SYSTEM_TASK:>
Given the following text description, write Python code to implement the functionality described below step by step
<END_TASK>
<USER_TASK:>
Description:
 Expecting the unexpected
To handle errors properly deserves a chapter on its own in any programming book. Python gives us many ways do deal with errors fatal and otherwise
Step1:  This shows how exceptions are raised and caught, but this approach is somewhat limited. Suppose now, that we weren't expecting this expected unexpected behaviour and we wanted to compute everything before displaying our results.
Step2:  Ooops! Let's fix that.
Step3:  That's also not what we want. We wasted all this time computing nice reciprocals of numbers, only to find all of our results being thrown away because of one stupid zero in the input list. We can fix this.
Step4:  That's better! We skipped right over the error and continued to more interesting results. So how are we going to make this solution more generic? Subsequent functions may not know how to handle that little nan in our list.
Step5:  Hmmmpf. There we go again.
Step7:  This seems Ok, but there are two problems here. For one, it feels like we're doing too much work! We have a repeating code pattern here. That's always a moment to go back and consider making parts of our code more generic. At the same time, this is when we need some more advanced Python concepts to get us out of trouble. We're going to define a function in a function!
Step8:  Consider what happens here. The function secure_function takes a function something_dangerous as an argument and returns a new function something_safe. This new function executes something_dangerous within a try-except block to deal with the possibility of failure. Let's see how this works.
Step11:  Ok, so that works! However, the documentation of safe_sqrt is not yet very useful. There is a nice library routine that may help us here
Step12:  Now it is very easy to also rewrite our function computing the reciprocals safely
Step15:  There is a second problem to this approach, which is a bit more subtle. How do we know where the error occured? We got two values of nan and are desperate to find out what went wrong. We'll need a little class to capture all aspects of failure.
Step18:  We will adapt our earlier design for secure_function. If the given argument is a Fail, we don't even attempt to run the next function. In stead, we extend the trace of the failure, so that we can see what happened later on.
Step19:  Now we can rewrite our little program entirely from scratch
Step20:  See how we retain a trace of the functions that were involved in creating the failed state, even though the execution of that produced those values is entirely decoupled. This is exactly what we need to trace errors in Noodles.
Handling errors in Noodles
Noodles has the functionality of secure_function build in by the name of maybe. The following code implements the above example in terms of noodles.maybe
Step21:  The maybe decorator works well together with schedule. The following workflow is full of errors!
Step22:  Both the reciprocal and the square root functions will fail. Noodles is smart enough to report on both errors.`
Step23:  Example
Step24:  If a file does note exist, stat returns an error-code of 1.
Step25:  We can wrap the execution of the stat command in a helper function.
Step26:  The run function runs the given command and returns a CompletedProcess object. The check=True argument enables checking for return value of the child process. If the return value is any other then 0, a CalledProcessError is raised. Because we decorated our function with noodles.maybe, such an error will be caught and a Fail object will be returned.
Step27:  We can now run this workflow and print the output in a table.
<ASSISTANT_TASK:>
Python Code:
import sys
def something_dangerous(x):
    print("computing reciprocal of", x)
    return 1 / x
try:
    for x in [2, 1, 0, -1]:
        print("1/{} = {}".format(x, something_dangerous(x)))
        
except ArithmeticError as error:
    print("Something went terribly wrong:", error)
Explanation: Expecting the unexpected
To handle errors properly deserves a chapter on its own in any programming book. Python gives us many ways do deal with errors fatal and otherwise: try, except, assert, if ...
Using these mechanisms in a naive way may lead to code that is littered with safety if statements and try-except blocks, just because we need to account for errors at every level in a program.
In this tutorial we'll see how we can use exceptions in a more effective way. As an added bonus we learn how to use exceptions in a manner that is compatible with the Noodles programming model. Let's try something dangerous! We'll compute the reciprocal of a list of numbers. To see what is happening, the function something_dangerous contains a print statement.
End of explanation
input_list = [2, 1, 0, -1]
reciprocals = [something_dangerous(item)
               for item in input_list]
print("The reciprocal of", input_list, "is", reciprocals)
Explanation: This shows how exceptions are raised and caught, but this approach is somewhat limited. Suppose now, that we weren't expecting this expected unexpected behaviour and we wanted to compute everything before displaying our results.
End of explanation
try:
    reciprocals = [something_dangerous(item)
                   for item in input_list]
except ArithmeticError as error:
    print("Something went terribly wrong:", error)
else:
    print("The reciprocal of\n\t", input_list,
          "\nis\n\t", reciprocals)
Explanation: Ooops! Let's fix that.
End of explanation
import math
def something_safe(x):
    try:
        return something_dangerous(x)
    except ArithmeticError as error:
        return math.nan
reciprocals = [something_safe(item)
               for item in input_list]
print("The reciprocal of\n\t", input_list,
      "\nis\n\t", reciprocals)
Explanation: That's also not what we want. We wasted all this time computing nice reciprocals of numbers, only to find all of our results being thrown away because of one stupid zero in the input list. We can fix this.
End of explanation
square_roots = [math.sqrt(item) for item in reciprocals]
Explanation: That's better! We skipped right over the error and continued to more interesting results. So how are we going to make this solution more generic? Subsequent functions may not know how to handle that little nan in our list.
End of explanation
def safe_sqrt(x):
    try:
        return math.sqrt(x)
    except ValueError as error:
        return math.nan
[safe_sqrt(item) for item in reciprocals]
Explanation: Hmmmpf. There we go again.
End of explanation
def secure_function(dangerous_function):
    def something_safe(x):
        A safer version of something dangerous.
        try:
            return dangerous_function(x)
        except (ArithmeticError, ValueError):
            return math.nan
    
    return something_safe
Explanation: This seems Ok, but there are two problems here. For one, it feels like we're doing too much work! We have a repeating code pattern here. That's always a moment to go back and consider making parts of our code more generic. At the same time, this is when we need some more advanced Python concepts to get us out of trouble. We're going to define a function in a function!
End of explanation
safe_sqrt = secure_function(math.sqrt)
print("⎷2 =", safe_sqrt(2))
print("⎷-1 =", safe_sqrt(-1))
print()
help(safe_sqrt)
Explanation: Consider what happens here. The function secure_function takes a function something_dangerous as an argument and returns a new function something_safe. This new function executes something_dangerous within a try-except block to deal with the possibility of failure. Let's see how this works.
End of explanation
import functools
def secure_function(dangerous_function):
    Create a function that doesn't raise ValueErrors.
    @functools.wraps(dangerous_function)
    def something_safe(x):
        A safer version of something dangerous.
        try:
            return dangerous_function(x)
        except (ArithmeticError, ValueError):
            return math.nan
    
    return something_safe
safe_sqrt = secure_function(math.sqrt)
help(safe_sqrt)
Explanation: Ok, so that works! However, the documentation of safe_sqrt is not yet very useful. There is a nice library routine that may help us here: functools.wraps; this utility function sets the correct name and doc-string to our new function.
End of explanation
something_safe = secure_function(something_dangerous)
[safe_sqrt(something_safe(item)) for item in input_list]
Explanation: Now it is very easy to also rewrite our function computing the reciprocals safely:
End of explanation
class Fail:
    Keep track of failures.
    def __init__(self, exception, trace):
        self.exception = exception
        self.trace = trace
        
    def extend_trace(self, f):
        Grow a stack trace.
        self.trace.append(f)
        return self
        
    def __str__(self):
        return "Fail in " + " -> ".join(
            f.__name__ for f in reversed(self.trace)) \
            + ":\n\t" + type(self.exception).__name__ \
            + ": " + str(self.exception)
Explanation: There is a second problem to this approach, which is a bit more subtle. How do we know where the error occured? We got two values of nan and are desperate to find out what went wrong. We'll need a little class to capture all aspects of failure.
End of explanation
def secure_function(dangerous_function):
    Create a function that doesn't raise ValueErrors.
    @functools.wraps(dangerous_function)
    def something_safe(x):
        A safer version of something dangerous.
        if isinstance(x, Fail):
            return x.extend_trace(dangerous_function)
        try:
            return dangerous_function(x)
        except Exception as error:
            return Fail(error, [dangerous_function])
    
    return something_safe
Explanation: We will adapt our earlier design for secure_function. If the given argument is a Fail, we don't even attempt to run the next function. In stead, we extend the trace of the failure, so that we can see what happened later on.
End of explanation
@secure_function
def reciprocal(x):
    return 1 / x
@secure_function
def square_root(x):
    return math.sqrt(x)
reciprocals = map(reciprocal, input_list)
square_roots = map(square_root, reciprocals)
for x, result in zip(input_list, square_roots):
    print("sqrt( 1 /", x, ") =", result)
Explanation: Now we can rewrite our little program entirely from scratch:
End of explanation
import noodles
import math
from noodles.tutorial import display_workflows
@noodles.maybe
def reciprocal(x):
    return 1 / x
@noodles.maybe
def square_root(x):
    return math.sqrt(x)
results = [square_root(reciprocal(x)) for x in [2, 1, 0, -1]]
for result in results:
    print(str(result))
Explanation: See how we retain a trace of the functions that were involved in creating the failed state, even though the execution of that produced those values is entirely decoupled. This is exactly what we need to trace errors in Noodles.
Handling errors in Noodles
Noodles has the functionality of secure_function build in by the name of maybe. The following code implements the above example in terms of noodles.maybe:
End of explanation
@noodles.schedule
@noodles.maybe
def add(a, b):
    return a + b
workflow = add(noodles.schedule(reciprocal)(0),
               noodles.schedule(square_root)(-1))
display_workflows(arithmetic=workflow, prefix='errors')
Explanation: The maybe decorator works well together with schedule. The following workflow is full of errors!
End of explanation
result = noodles.run_single(workflow)
print(result)
Explanation: Both the reciprocal and the square root functions will fail. Noodles is smart enough to report on both errors.`
End of explanation
!stat -t -c '%A %10s %n' /dev/null
Explanation: Example: parallel stat
Let's do an example that works with external processes. The UNIX command stat gives the status of a file
End of explanation
!stat -t -c '%A %10s %n' does-not-exist
Explanation: If a file does note exist, stat returns an error-code of 1.
End of explanation
from subprocess import run, PIPE, CalledProcessError
@noodles.schedule
@noodles.maybe
def stat_file(filename):
    p = run(['stat', '-t', '-c', '%A %10s %n', filename],
            check=True, stdout=PIPE, stderr=PIPE)
    return p.stdout.decode().strip()
Explanation: We can wrap the execution of the stat command in a helper function.
End of explanation
files = ['/dev/null', 'does-not-exist', '/home', '/usr/bin/python3']
workflow = noodles.gather_all(stat_file(f) for f in files)
display_workflows(stat=workflow, prefix='errors')
Explanation: The run function runs the given command and returns a CompletedProcess object. The check=True argument enables checking for return value of the child process. If the return value is any other then 0, a CalledProcessError is raised. Because we decorated our function with noodles.maybe, such an error will be caught and a Fail object will be returned.
End of explanation
result = noodles.run_parallel(workflow, n_threads=4)
for file, stat in zip(files, result):
    print('stat {:18} -> {}'.format(
        file, stat if not noodles.failed(stat)
        else 'failed: ' + stat.exception.stderr.decode().strip()))
Explanation: We can now run this workflow and print the output in a table.
End of explanation
<END_TASK>