A bit of Railway Oriented Programming with Coconut
Using Coconut/Python to build a pipeline with error handling
Why am I doing this?
I recently did a talk on Coconut at the Tucson Python Meetup (aka TuPLE). I mostly demoed the examples in the tutorial and the documentation, but I also came up with a way to do error handling like I do in F# with Railway Oriented Programming that I learned from F# for Fun and Profit.
You can find my source code on Github.
What is Coconut, anyway?
Coconut is functional programming for Python. All valid Python 3 is valid Coconut. Using the coconut command you will compile your Coconut code into Python that works on both 2.x and 3.x, which is really cool.
What is Railway Oriented Programming, anyway?
This is a common functional pattern for handling errors. This is a pattern that helps you write functions like puzzle pieces that you can fit together while handling exceptions at the same time. We will see more on this later.
In F# the pattern is to output from a function a Result union type that is either the result value of the execution or the error that happened during execution. It looks something like this (using Chessie):
//Result union type
type Result<'TSuccess, 'TMessage> =
// Represents the result of a successful computation.
| Ok of 'TSuccess * 'TMessage list
// Represents the result of a failed computation.
| Bad of 'TMessage list
let doSomething doThisThing =
try
match doThisThing with
| TheValueIExpected -> () |> ok
| _ -> fail "The command didnt work for some reason."
with ex -> fail (sprintf "ERROR [%s]" ex.Message)
Getting started
The user guide takes you through all of this, but here are the basic steps.
You'll need to install Python 3 and then you can
pip install coconut
You can now use coconut
from the Python prompt.
You'll also need an editor. I like using VS Code, but you can use any one that you like. I really like VS Code. I get a terminal right in the editor so I can compile and run my code on the command line directly in the editor. If you do use VS Code, you can associate coco files with Python highlighting and that works pretty good.
Making the Result type the Python way
All credit goes to my husband for the idea behind this.
First I needed a way to represent the Result type. My husband suggested using a namedtuple, so I did this:
from collections import namedtuple
ErrorResult = namedtuple("ErrorResult", "Message")
# instantiate like this.
test = ErrorResult(Message = "Something went wrong. This could be a message or a an exception object.")
We can use this in a function and return ErrorResult if an error occurred like this:
def calc_with_exception():
try:
# do a thing
return something
except Exception as e:
return ErrorResult(Message = "SOMETHING WENT WRONG")
Creating an execution pipeline
Doing the above is good for creating a program with consistent error handling, but you can take it a step further. We can use the ErrorResult pattern to write functions that can fit together like puzzle pieces and then build an execution pipeline in a functional way.
Let's say you needed to execute a number of functions one after the other but you needed to stop executing any functions if an error occurred. With ErrorResult, with any function you need in this pipeline, you can pass it as an argument to those functions and check the type to see if it is an error or a value to do something with, like this.
def calc_1(result):
if isinstance(result, ErrorResult):
#there was an error. do not evaluate and just return the ErrorResult
return result
else:
# do something with the value passed in
return result * 100
We can then write a bunch of functions like this and execute them in a pipeline.
from collections import namedtuple
ErrorResult = namedtuple("ErrorResult", "Message")
test = ErrorResult(Message = "Something went wrong. This could be a message or a an exception object.")
def calc_1(result):
if isinstance(result, ErrorResult):
print ("error calc_1")
return result
else:
# do something with the result
print ("calculating calc_1")
return result * 100
def calc_2(result):
if isinstance(result, ErrorResult):
print ("error calc_2")
return result
else:
# do something with the result
print ("calculating calc_2")
return result * 200
def calc_error(result):
# FORCE AN ERROR FOR THIS EXAMPLE
print ("returning an error")
return test
Then using Coconut's pipe syntax we can pass the results of a function into the next function with |>
1 # initial value
|> calc_1 # calculates value time 100
|> calc_error # passed 100 but result is an ErrorResult
|> calc_2 # passed an ErrorResult. does not do calculation
|> print # prints ErrorResult
If you follow the comments, the calculation stopped and the error that caused the short circuit is caught and passed all the way to the end. You can then react to that as needed.
Using the Coconut pattern matching decorator
Coconut provides ways to do pattern matching and it lets you take advantage of Python decorators so you can easily add pattern matching functions to any function.
This is my favorite feature of Coconut. While Coconut provides all of the usual functional features I expect to see, pattern matching decorators are a unique feature, taking advantage of the power of Python's decorators.
We can do the pipeline from the example above but instead of explicitly checking the type of the result argument, we can create a pattern matching function and apply it as a decorator to the functions in the pipeline.
Here is the pattern matching function:
match def checkResult(result, if isinstance(result, ErrorResult)) = result
This is a function that checks if the result argument is an ErrorResult and returns the ErrorResult if it gets a match. Otherwise it does nothing.
And then you can use Coconut's @addpattern
decorator like this:
@addpattern(checkResult)
def calc_100(result):
print ("calculating calc_1")
return result * 100
@addpattern(checkResult)
def calc_200(result):
print ("calculating calc_2")
return result * 200
@addpattern(checkResult)
def calc_error(result):
# FORCE AN ERROR FOR THIS EXAMPLE
print ("returning an error")
return ErrorResult(Message = "Sorry that didn't quite work out like we planned. This could be a message or an exception object.")
Notice the function doesn't check the ErrorResult. That is handled by the pattern matching decorator. This function will only execute if checkResult does not return. It will not return because the decorator function found a match with ErrorResult.
This eliminates a ton of code. There are other pure Python ways to simplify this, but this is more easily reusable and very readable. Plus if you have Coconut you can then build a pipeline.
1 |> calc_100 |> calc_error |> calc_200 |> print
What will happen in this pipeline? I leave that as a exercise for the reader.
Thoughts
There are tons of features and this is only one example. This is an elegant way to handle errors and build execution pipelines without tons of if statements. And if you can take advantage of the other functional features, Coconut will make your Python better.
If you have any questions or comments, please tweet me.
Full Stack .NET Programmer and Ham