Haskell brings a unique and powerful approach to handling functions: it treats all functions as curried functions by default. This concept, coupled with partial application, allows for more modular, reusable, and flexible code. In this article, we’ll explore what curried functions and partial application are, why they matter, and how they can be used effectively in Haskell.

What Are Curried Functions?

In Haskell, all functions are naturally curried, meaning they take one argument at a time and return a new function that takes the next argument. Currying is named after the logician Haskell Curry and allows us to split a multi-argument function into a series of single-argument functions.

Example of Currying

Consider a simple addition function that takes two integers and returns their sum:

add :: Int -> Int -> Int
add x y = x + y

While this may look like a function that takes two arguments, it’s actually curried. In Haskell, Int -> Int -> Int means that add is a function that takes an Int and returns another function, which in turn takes an Int and returns the sum as an Int. Conceptually, this is the same as writing:

add :: Int -> (Int -> Int)

So, if you call add with just one argument, you’ll get a new function that waits for the second argument:

addFive :: Int -> Int
addFive = add 5

Here, addFive is a partially applied function: a version of add that takes one argument and adds five to it. This leads us to the concept of partial application.

Partial Application Explained

Partial application refers to calling a function with fewer arguments than it expects, resulting in a new function that takes the remaining arguments. In Haskell, this is made possible by currying.

Example of Partial Application

Let’s consider an example where partial application makes our code simpler and more reusable. Imagine we want to add a fixed number to each element in a list. We can partially apply add to create a new function that increments each value by 10:

incrementByTen :: [Int] -> [Int]
incrementByTen = map (add 10)

incrementByTen [1, 2, 3]  
-- Result: [11, 12, 13]

In this case, add 10 is a partially applied function that creates a function which adds 10 to any number it’s given. Then map applies this function to each element in the list, resulting in [11, 12, 13].

Why Are Curried Functions and Partial Application Useful?

Currying and partial application offer several benefits, making Haskell code more modular, expressive, and reusable.

1. Modularity and Reusability

Curried functions enable modular code. By breaking down functions into smaller pieces, we can reuse them in different contexts. In the example above, add can be partially applied in multiple ways, such as add 10 or add 5, each creating a new, specific function from the same core function.

2. Higher-Order Functions and Function Composition

Currying plays a crucial role in higher-order functions, where functions are passed as arguments or returned as results. Partial application allows us to quickly define specific behaviors without writing entirely new functions.

3. Cleaner and More Expressive Code

Curried functions make it easier to express transformations and behaviors. With partial application, we can create more readable code that communicates intent more clearly.

Examples of Curried Functions and Partial Application

Let’s go through some examples that illustrate the power and flexibility of currying and partial application in Haskell.

Example 1: Filtering with Partial Application

Suppose we want to filter a list of numbers, keeping only those greater than 10. We can create a curried greaterThan function:

greaterThan :: Int -> Int -> Bool
greaterThan x y = y > x

We can then partially apply greaterThan to create a new function that checks if a number is greater than 10:

greaterThanTen :: Int -> Bool
greaterThanTen = greaterThan 10

filter greaterThanTen [5, 15, 8, 20]  
-- Result: [15, 20]

Example 2: Using foldl with Partial Application

The foldl function takes a binary function, an initial accumulator, and a list to process. Currying allows us to partially apply foldl to create functions like sum:

sum :: [Int] -> Int
sum = foldl (+) 0

sum [1, 2, 3, 4]  -- Result: 10

Here, we’ve partially applied foldl by fixing (+ 0) as the initial accumulator, creating a new sum function that sums all elements in a list.

Example 3: Function Composition with Curried Functions

Haskell’s function composition operator (.) works seamlessly with curried functions, allowing us to chain operations concisely. Here’s an example that composes multiple functions to process a list:

process :: [Int] -> [Int]
process = map (* 2) . filter (> 10)

process [5, 15, 8, 20]  -- Result: [30, 40]

In this example, process first filters the list to keep numbers greater than 10, then doubles each element in the filtered list.

Curried Functions in Action: A Real-World Example

Imagine we’re building a small application to calculate discounts based on customer membership levels. We could start by defining a curried applyDiscount function:

applyDiscount :: Double -> Double -> Double
applyDiscount rate price = price * (1 - rate)

Now, suppose we have different discount levels for regular and premium members:

regularDiscount :: Double -> Double
regularDiscount = applyDiscount 0.10

premiumDiscount :: Double -> Double
premiumDiscount = applyDiscount 0.20

Using partial application, we can create specific discount functions for regular and premium members:

regularPrice = regularDiscount 100.0   
-- Result: 90.0
premiumPrice = premiumDiscount 100.0   
-- Result: 80.0

In this way, currying and partial application help us to reuse applyDiscount flexibly, avoiding redundancy and making it easy to adjust the discount rates in one place.

Explain Curried Functions & Partial Application Like I’m Five Years Old (ELI5)

Imagine functions in Haskell as machines that need ingredients to make something. A normal machine might need all the ingredients at once to work, but a curried machine is different. It’s a machine that takes one ingredient at a time, then transforms into a new machine that waits for the next ingredient, and so on, until it has everything it needs.

Curried Functions

Let’s say you have a magic machine called add that makes numbers bigger by adding two ingredients together, like this:

add x y = x + y

This machine is curried, so if you only give it the first number, x, it doesn’t get confused. Instead, it turns into a new machine that’s just waiting for the second number, y. When you give it that second number, it will finally do the adding and give you the result.

So if you say add 3, Haskell doesn’t complain that you missed something. Instead, it just turns into a new machine that is ready to add 3 to whatever number you give it next. You can then call it like this:

addThree = add 3
addThree 5  -- This gives you 8, since 3 + 5 = 8

Partial Application

Now, this “give me one ingredient at a time” feature has a bonus: it allows you to make smaller machines out of bigger machines. This is called partial application.

With add, if you only give it 3, you get a smaller machine, addThree, that always adds 3 to whatever you give it next. You can reuse addThree wherever you want without typing add 3 each time! This lets you build up specialized little machines without rewriting the instructions.

So, with curried functions and partial application, you can start with a big machine, give it one ingredient, and end up with a smaller machine ready to take the next one. It’s like turning a sandwich-making machine into a “peanut butter spreader” if you give it only peanut butter first!

In Haskell, every function works like this, making it easy to build new functions by giving them just some of their ingredients and saving the rest for later.

Summary

Curried functions and partial application are core concepts in Haskell, offering a powerful approach to building modular, expressive, and reusable code. Here’s a quick recap:

  1. Curried Functions: All functions in Haskell are curried by default, meaning they take one argument at a time and return a new function for each additional argument.
  2. Partial Application: This allows you to supply fewer arguments than a function expects, resulting in a new function that takes the remaining arguments.
  3. Use Cases: Curried functions and partial application enhance higher-order functions, function composition, and the overall modularity of code.

By mastering curried functions and partial application, you’ll gain a deeper understanding of functional programming in Haskell, unlocking new ways to write clean, efficient, and expressive code.

FAQs about Currying & Partial Application

1. What exactly is currying?

Currying is a way of structuring functions so that they take one argument at a time. In Haskell, a function with multiple arguments is actually a series of single-argument functions that each return a new function, waiting for the next argument.

For example, add :: Int -> Int -> Int in Haskell is actually a function that takes an Int and returns another function, which then takes the second Int and finally returns the result.

2. What’s the difference between currying and partial application?

  • Currying is the process of transforming a function to take one argument at a time.
  • Partial application is using a curried function by giving it fewer arguments than it needs, creating a new function that takes the remaining arguments.

For example, if add 3 5 gives 8, then add 3 is a partially applied function that takes one argument and adds 3 to it.

3. Why are all functions in Haskell curried by default?

Currying allows for more flexible and reusable functions by enabling partial application and function composition. This default structure in Haskell simplifies the syntax and makes it easier to build complex functions from simpler ones.

4. What’s an example of partial application in Haskell?

Let’s say you have a function multiply :: Int -> Int -> Int that multiplies two numbers. By partially applying it with one argument, you can create a new function:

double = multiply 2
double 10  -- Result: 20, since 2 * 10 = 20

Here, double is a partially applied version of multiply that always multiplies its input by 2.

5. Can you partially apply any function in Haskell?

Yes, you can partially apply any function because all functions in Haskell are curried. If a function takes more than one argument, you can supply just one (or more) and get back a new function waiting for the remaining arguments.

6. What does the type Int -> Int -> Int mean?

The type Int -> Int -> Int means a function that takes an Int and returns another function of type Int -> Int. This second function, in turn, takes an Int and finally returns an Int. So, Int -> Int -> Int can also be written as Int -> (Int -> Int).

7. What’s the benefit of currying over regular multi-argument functions?

Currying makes it easy to:

  • Reuse functions by creating specialized versions (like using add 3 to make a function that always adds 3).
  • Compose functions by chaining them together in flexible ways.
  • Work with higher-order functions, where functions are passed as arguments or returned as results.

8. Does currying affect performance?

Currying does introduce a small overhead since each argument creates a new function. However, Haskell’s optimizations usually make this impact negligible, especially compared to the benefits in modularity and code clarity.

9. Is currying only available in Haskell?

No, currying is available in other languages, especially functional ones like ML, Scala, and F#. In some languages, like JavaScript, currying isn’t the default but can be implemented using closures.

10. How can I make a function that isn’t curried in Haskell?

If you really need a function that takes multiple arguments all at once, you can use tuples. For instance, add :: (Int, Int) -> Int would take a single argument that’s a pair of Ints. This can be useful if currying doesn’t suit the specific function you’re writing.

11. Are curried functions always used with partial application?

Not necessarily. Curried functions support partial application, but you can still apply all arguments at once if needed. Partial application is just one possible way to use curried functions.

12. Is there any difference between a curried function and a higher-order function?

Yes. While all curried functions can be partially applied, higher-order functions refer to functions that either take other functions as arguments or return functions as results. Currying helps make higher-order functions easier to work with, but they are distinct concepts.

13. What’s a good real-world analogy for curried functions?

Imagine ordering a multi-step process, like a sandwich. You might start by picking the bread (first step), then add your protein (second step), and finally add toppings (third step). Each step of the process is like supplying an argument to a curried function, and each step returns a new function waiting for the next ingredient.


Comments

Leave a Reply

Your email address will not be published. Required fields are marked *