Python is a multi-paradigm language that can follow different paradigms to solve a problem; this includes functional programming, which I enjoyed after learning Haskell and Elm. Python offers a lot of functional features like anonymous functions, high-order functions, decorators, lazy evaluation (through iterators), and functional helpers (through functools) and I can use the language's core feature to have custom operators for common operations (e.g. the piping operator). However, lambdas are too verbose, not all functions are curried, and I will need to create additional helpers or depend on external libraries to write functional code to maintain a single style for my application.
In this blog, I'm going to show how Coconut enhances the experience of writing functional Python and I'll build an application to show its features and how it can work fine in a Python environment.
NOTE: I want to strongly highlight that this blog is based on my personal experience with functional programming in Python. I'll list some inconveniences that I found in Python but (maybe) they're not enough reasons to look for a switch to another language. As always, use your own reasoning and analyze your situation to make a decision.
## Why I don't like Functional Programming in Python
Functional programming is my preferred paradigm for solving problems because I need to think about small functions that are easy to read, understand, test, and compose. Python has many features like lambda functions, decorators, high-order functions, lazy evaluation (e.g. through iterators), and so on that can enrich my functional programming experience. However, there are some small things that I (personally) don't like:
1. Lambda functions need the lambda
keyword which is a long word
for an anonymous function, especially when we need to use many of them.
For example:
queen_song_stars = map(
lambda song: song.stars,
filter(lambda song: song.author == 'Queen',
filter(lambda song: song.is_favorite, songs)))
In fact, lambda functions can be overused and in many cases, they may not be recommended.
2. Many common functions like compose
or pipe
are not defined
out-of-the-box so I need to define them manually:
def pipe(x, *fns):
pass
def compose(f, g):
pass
An option could be to use existing libraries that contain those functions (e.g. functools or PyToolz) but then I need to maintain that dependency (e.g. making sure it's up to date to avoid possible Common Vulnerabilities and Exposures).
3. Not all functions are curried so I need to curry them manually
def filter_by_artist(artist: str) -> Callable[[str], songs]:
def wrap(songs: Iterator[Song]) -> Iterator[Song]:
return filter(lambda song: song.author == 'Queen', songs)
return wrap
I could use functools.partial
but then I need to import that every time
I need to curry a function.
4. It's necessary to import utilities or other libraries every time I need to use these operators
from utils import compose
from toolz import pipe
import statistics
get_stars_of_artist = compose(filter_by_artist, get_stars)
pipe(
tenacious_d_songs,
get_stars_of_artist,
statistics.mean
)
This is a little bit of a consequence of the previous two points but it doesn't need to be an issue if I use some tooling to get the import automatically (e.g. using a Language Server Protocol and integrating it with my editor) and a code linter that tells me when I don't need a dependency anymore so that it can be removed.
These nuisances become great inconveniences for me when doing functional programming in Python but they may not be a big deal for most developers. Fortunately for me, I found Coconut which has the functionalities that I was looking for.
How did Coconut satisfy my needs?
Coconut is a variant of Python that adds new functional programming features (and others) on top of it. Coconut's built-in helpers solve the inconveniences that I mentioned before and more. In Coconut, I don't need to create helpers, import packages, add additional dependencies, wrap functions, create abstractions, overload operators, etc., in order to have the functional experience that I want. This is because the solutions are available out-of-the-box.
Let's take a look at this example as a sneak peak:
import statistics
tenacious_d_songs = [
{ "name": "Tribute", "stars": 4 },
{ "name": "Master Exploder", "stars": 5 },
{ "name": "Kickapoo", "stars": 4 }
]
result = (
tenacious_d_songs
|> map$((x) => x['stars'])
|> statistics.mean
|> (x) =>
if x >= 4
then "Cool music!"
else "Regular"
)
result |> print
We can note the following things:
-
We have a pipe operator (
|>
) out-of-the-box (in fact, we have many more) -
Arrow functions are simple and less verbose:
(x) => x['stars']
-
We can curry a function with
$
so no need to import other packages and wrap our functions. -
We have a different syntax for inline
if/else
statements that clarify what happens on both sides of a conditional
Let's be clear about something, though. Coconut is not the holy grail either; it has a lot of things that need to be improved to be used more broadly (e.g. a better developer environment) but it offers nice functional features for Python developers to enjoy functional programming.
Coconut used for an Application
I'll create a simple App that will crawl the website of Luciano Mellera to get a list of shows and a web app that summarizes the information obtained and allows me to know if there will be shows in my city. The project is structured in the following way:
crawler/
__main__.coco
web/
templates/index.html
__main__.coco
requirements.txt
Crawler
The crawler was made of 4 things:
- A data type that will hold the information from the Show
@dataclass
class Show:
date: str
place: str
def to_dict(self):
return { "date": self.date, "place": self.place }
-
A show parse given a
tag
will instance aShow
a. This function presents something very particular and that is the
else
statement that returns aShow
if the tags are found otherwiseNone
.
def parse_show(tag) -> Show | None:
match (tag.find('h2'), tag.find('h4')):
case (d `isinstance` bs4.element.Tag, place `isinstance` bs4.element.Tag):
return Show(place=place.get_text(), date=d.get_text())
case _:
return None
-
A function that parses all the shows filtering the
None
cases and getting a list out of it.a. I'm using the (
\>
) operator which is very neat because it's a monadic operator (like bind) for union types of the form:X | None
def parse_shows(html_doc: str) -> list[dict]:
soup = bs4.BeautifulSoup(html_doc, 'html.parser')
return (
soup.find_all('section', class_="elementor-section")
|> map$((x) => parse_show(x) |?> (x) => x.to_dict)
|> filter$(None)
|> list
)
-
A composition of all of the above
a. There is something in particular in this case and that is
safe_call
which converts the result ofrequest.get
into anExpected
data type that is similar toEither
in Haskell. This type represents a computation that could fail or succeed.
if __name__ == "__main__":
with open("shows.json", "w") as f:
match safe_call(requests.get, URL):
case Expected(result, error=None):
shows_json = (
result.content
|> parse_shows
|> json.dumps
)
f.write(shows_json)
print("Done!")
case Expected(_, error=error):
print(f"Sorry, there was an error: {error}")
Web App
The Web App is going to be something very simple as well. It will read the data from the crawler and summarize it to present it in a simple way. In fact, it's a simple Flask view function:
ffrom flask import Flask, render_template
import json
app = Flask(__name__)
@app.route("/", methods=["GET"])
def home():
with open("shows.json", 'r') as f:
shows = f.read() |> json.loads
return render_template(
"index.html",
cities=shows |> map$((x) => x['place']) |> set,
available_shows=shows |> filter$((x) => 'Agotado' not in x['place']),
show_in_quito=(
if any('Quito' in x['place'] for x in shows)
then 'Yes!!!'
else 'Nope :('
),
)
We're not doing anything extraordinary here. It's a simple view with three parameters that are passed to the template where they're processed to generate the final HTML:
<body>
<div class="cities">
<h1>All Cities</h1>
<ul>
{% for city in cities %}
<li>{{ city }}</li>
{% endfor %}
</ul>
<h1>Available</h1>
<ul>
{% for x in available_shows %}
<li>{{ x['place'] }} - {{ x['date'] }}</li>
{% endfor %}
</ul>
<h1>Will he be in Quito?</h1>
<p>{{ show_in_quito }}</p>
</div>
</body>
The main point of this part was to show that it's possible to integrate Coconut with the Python libraries (e.g. Flask, Jinja, requests, beautiful soup that were used in this post) with no hustle. All the code for this blog post is in GitHub. Take a look at it, try it out, play around with it, etc! For running the crawler and web projects you can use coconut-run:
coconut-run crawler
coconut-run web
Conclusion
In this post, I show how Coconut can help overcome some small inconveniences for functional programming in Python with out-of-the-box features. I presented some features like simple array functions, operators for piping data (|>
, |?>
) and curry functions ($
), pattern matching, operators and data types for optional and exceptional data types, and that is just the tip of the iceberg. Finally, I presented two simple and real applications of Coconut to show how it can be used to work with other Python packages without difficulties.
In this post, I presented code snippets to demonstrate Coconut’s functionality. In a future post, I will cover how to test, build and deploy a Coconut application using the coconut
compiler.