Hello! Welcome to our third lesson in the "Higher-Order Functional Patterns" module.
In our last session, we explored currying—the transformation of a multi-argument function into a chain of single-argument functions. We saw that in Haskell, this is the default behavior, which makes partial application a natural and seamless operation. For example, map (*2) works because map is curried, and applying it to just one argument, (*2), returns a new function.
Today, we'll dive deeper into this consequence. Our goal is to use partial application to create specialized functions, comparing explicit approaches in Python's functools with implicit currying in Haskell. We'll see how different language philosophies lead to different ways of achieving the same powerful result: creating new functions on the fly by "pre-filling" arguments.
1. Partial Application vs. Currying: A Quick Refresher
Before we proceed, let's solidify the distinction between currying and partial application, as the terms are often used interchangeably but describe different things.
- Currying is a transformation. It takes a function
f(a, b, c)and restructures it intog(a)(b)(c). The curried function always processes arguments one at a time. - Partial Application is an action. It involves taking a function (curried or not) and applying it to some of its arguments, which creates a new function that takes the remaining arguments.
This article, 'Introduction to Currying', provides a very clear and concise section that distinguishes between these two concepts.
Please read the section titled 'Currying vs. Partial Application'. Focus on how it defines each term and the example it uses to contrast them: currying add(x, y) gives add(x)(y), while partially applying it might give addFive = add(5, _).
In essence, currying is a specific way to structure functions that makes partial application particularly easy. You can partially apply a non-curried function, but it often requires a helper mechanism. Let's see how this plays out in Haskell and Python.
2. Implicit Partial Application in Haskell
As we saw last time, Haskell's "curried by default" design means partial application requires no special syntax. If a function is defined as f :: a -> b -> c, applying it to just an argument of type a implicitly returns a function of type b -> c.
This is not just a theoretical curiosity; it's a cornerstone of idiomatic Haskell programming. Let's revisit a practical example.
Haskell for Imperative Programmers #7 - Partial Function Application & Currying
We watched parts of this video in our last lesson. Now, let's focus on how partial application is used to build a new, specialized function from map.
Please watch from 01:15 to 03:28. Pay close attention to two key ideas: How calling add 1 doesn't produce an error but instead returns a new function (01:15 - 02:49). How map is partially applied to create the doubleList function without needing a lambda or a new function definition (02:34 - 03:28).
The expression map (*2) is a powerful demonstration of this principle. We created a new function, doubleList, simply by providing the first argument to map. The language handles the creation of the specialized function for us. This is the "implicit" approach.
3. Explicit Partial Application in Python
Python's philosophy, often summarized by "explicit is better than implicit," leads to a different design. Functions in Python are not curried by default. Calling a function with fewer arguments than it expects results in a TypeError.
def add(a, b):
return a + b
# This will raise a TypeError: add() missing 1 required positional argument: 'b'
# add_5 = add(5)
To achieve partial application, we must explicitly ask for it. There are two main ways to do this.
3.1. The Manual Approach: Lambdas and Closures
You can manually create a partially applied function using a lambda or a nested function. This leverages closures, where the inner function "remembers" the variables from the enclosing scope.
Python is the Haskell You Never Knew You Had: Currying
The article 'Python is the Haskell You Never Knew You Had' demonstrates how to manually curry functions, which is effectively a manual way to enable partial application.
Read the first two sections, 'What is Currying?' and 'Why Currying?'. Notice how the nested function add_a and the lambda a: lambda b: a + b examples are used to create functions that can be partially applied, like add3 = add(3).
For our add example, we could create an add_5 function like this:
def add(a, b):
return a + b
# Manual partial application using a lambda
add_5 = lambda y: add(5, y)
print(add_5(10)) # Output: 15
This works, but it's verbose and requires you to write a new function definition for each partial application.
3.2. The Idiomatic Approach: functools.partial
The standard, more robust way to perform partial application in Python is by using the partial function from the functools module. It automates the process of creating a new function with some arguments "frozen."
This next video provides an excellent walkthrough, starting with the manual lambda approach and then introducing functools.partial as the superior solution.
Partial Functions in Python - Functools Tutorial
This tutorial from NeuralNine clearly explains and demonstrates partial application in Python, culminating in a practical, real-world example.
Please watch from 02:07 to 07:07. Focus on: The manual implementation using a lambda inside a helper function (02:07 - 04:45). The introduction of functools.partial and how it simplifies the process, including how to fix arguments by position or by keyword (04:45 - 07:07).
Using functools.partial, our add_5 example becomes much cleaner:
from functools import partial
def add(a, b):
return a + b
add_5 = partial(add, 5)
print(add_5(10)) # Output: 15
partial(add, 5) returns a new callable object that behaves like add, but with the first argument permanently set to 5.
4. A Practical Example: Specializing API Calls
The power of partial application shines when you have a general, complex function that you repeatedly use in more specific contexts. Your background in finance and experience building trading platforms makes this a relevant scenario. Imagine a generic function for fetching market data.
The video you just watched continues with an excellent example of this, creating specialized functions to fetch stock data.
Partial Functions in Python - Functools Tutorial
Let's continue with the same video to see a more realistic use case for partial application.
Watch the final section of the video from 07:07 to 13:35. Notice how partial is used to create specialized functions like get_apple_data or get_stock_data_from_2018 from a single generic get_stock_data function.
Here's a summary of the pattern shown in the video:
This pattern is extremely common:
- Define a general, highly configurable function (
get_stock_data(ticker, start_date, end_date)). - Use
partialto create simpler, specialized versions by fixing some parameters (get_apple_data = partial(get_stock_data, ticker='AAPL')).
This improves code readability and reduces the chance of errors from repeatedly passing the same configuration arguments.
5. Comparison and Design Trade-offs
We've seen two different approaches to the same goal. The choice between them reflects a fundamental difference in language design philosophy.
| Feature | Haskell (Implicit) | Python (Explicit with functools.partial) |
|---|---|---|
| Syntax | f arg1 | partial(f, arg1) |
| Mechanism | A natural result of the default curried evaluation model. | A utility function that creates a new callable object with "frozen" arguments. |
| Verbosity | Minimal. Syntactically lightweight. | More verbose. Requires import and an explicit function call. |
| Clarity | Can feel "magical" or implicit to newcomers. Intent is part of the language fabric. | The intent is very clear and explicit: "I am creating a partial function." |
| Flexibility | Arguments are applied in a fixed order. | Highly flexible. Can fix positional arguments, keyword arguments, or both. |
| Core Philosophy | Functions are fundamentally single-argument transformations. Composition is key. | "Explicit is better than implicit." Practicality over mathematical purity. |
Neither approach is inherently "better"; they are idiomatic to their respective languages. Haskell's approach makes higher-order function composition, like map . map, incredibly fluid. Python's approach provides a clear, readable, and practical tool that fits well within its multi-paradigm nature without forcing a purely functional style.
Conclusion
In this lesson, we've focused on partial application as a powerful technique for creating specialized functions from more general ones.
Key Takeaways:
- Partial application creates a new, less-generic function by fixing one or more arguments of an existing function.
- In Haskell, this is an implicit and natural consequence of its curried design. Applying a function to fewer arguments than it expects simply yields a new function.
- In Python, this is an explicit action. While it can be done manually with lambdas, the idiomatic tool is
functools.partial. functools.partialis a flexible utility that can fix arguments by position or keyword, improving code reuse and readability by reducing boilerplate.- The difference between these approaches highlights a core design trade-off: Haskell's mathematical elegance and seamless composition versus Python's emphasis on explicit clarity and practicality.
Preview of the Next Lesson:
We've now seen how to create and pass around specialized functions using map and partial application. Next, we will explore another fundamental higher-order function: fold (also known as reduce). We will analyze its power as a universal list-processing tool, tracing its origins from Lisp and seeing how it can be used to implement map, filter, and many other operations.
Can't find a good explanation? Sign up and we'll make it for you
Sign up