-
-
Notifications
You must be signed in to change notification settings - Fork 422
Thinking Functionally: Partial application
In the previous post on currying, we looked at breaking multiple parameter functions into smaller one parameter functions. It is the mathematically correct way of doing it, but that is not the only reason it is done -- it also leads to a very powerful technique called partial function application. This is a very widely used style in functional programming, and it is important to understand it.
The idea of partial application is that if you fix the first N parameters of the function, you get a function of the remaining parameters. From the discussion on currying, you can probably see how this comes about naturally.
Here are some simple examples that demonstrate this:
using LanguageExt;
using static LanguageExt.Prelude;
using static LanguageExt.List;
using LanguageExt.ClassInstances;
Func<int, int, int> add = (x, y) => x + y;
Func<int, int> add42 = par(add, 42); // partial application
int x = add42(1); // x == 43
int y = add42(3); // y == 45
// create a new list by applying the add42 function to each element
// This uses `lpar` which partially applies from the left
var res = lpar(map, add42)(List(1,2,3)); // [43, 44, 45]
// create a "tester" by partial application of "less than"
Func<int, int, bool> isLessThan = (x, y) => x < y;
Func<int, bool> twoIsLessThan = par(isLessThan, 2); // partial application
bool x = twoIsLessThan(1); // false
bool y = twoIsLessThan(3); // true
// filter each element with the twoIsLessThan function
var res = lpar(filter, twoIsLessThan)(List(1,2,3)); // [3]
The following more complex example shows how the same approach can be used to create "plug in" behaviour that is transparent.
- We create a function that adds two numbers, but in addition takes a logging function that will log the two numbers and the result.
- The logging function has two parameters:
string name
andgeneric value
, so it has signature(string * A) -> Unit
, which as aFunc
looks like so:Func<string, A, Unit>
. - We then create various implementations of the logging function, such as a console logger or a popup logger.
- And finally we partially apply the main function to create new functions that have a particular logger baked into them.
// create an adder that supports a pluggable logging function
static A AdderWithPluggableLogger<MonoidA, A>(Func<string, A, Unit> logger, A x, A y)
where MonoidA : struct, Monoid<A>
{
logger("x", x);
logger("y", y);
var result = default(MonoidA).Append(x, y);
logger("x+y", result);
return result;
}
Note: Don't worry too much about the
Monoid
stuff. That will be covered in a later discussion, just accept for now that it allows any values of type ofA
that supports adding to be added.
// create a logging function that writes to the console
static Unit ConsoleLogger<A>(string argName, A argValue)
{
Console.WriteLine($"{argName}={argValue}");
return unit;
}
// create an adder with the console logger partially applied
static Func<A, A, A> AddWithConsoleLogger<MonoidA, A>()
where MonoidA : struct, Monoid<A> =>
par(AdderWithPluggableLogger<MonoidA, A>, ConsoleLogger);
// create an adder that works with ints
var addIntsWithConsoleLogger = AddWithConsoleLogger<TInt, int>();
// Test
addIntsWithConsoleLogger(1, 2);
addIntsWithConsoleLogger(42, 99);
// create a logging function that creates popup windows
static Unit PopupLogger<A>(string argName, A argValue)
{
var message = $"{argName}={argValue}";
System.Windows.Forms.MessageBox.Show(text: message, caption: "Logger");
return unit;
}
// create an adder with the popup logger partially applied
static Func<A, A, A> AddWithPopupLogger<MonoidA, A>()
where MonoidA : struct, Monoid<A> =>
par(AdderWithPluggableLogger<MonoidA, A>, PopupLogger);
// create an adder that works with strings
var addStringsWithPopupLogger = AddWithPopupLogger<TString, string>();
// Test
addStringsWithPopupLogger("Hello, ", "World");
addStringsWithPopupLogger("Really ", "Generic");
These functions with the logger baked in can in turn be used like any other function. For example, we can create a partial application to add 42, and then pass that into a list function, just like we did for the simple "add42" function.
// create a another adder with 42 baked in
var add42WithConsoleLogger = par(addIntsWithConsoleLogger, 42);
var res1 = map(List(1, 2, 3), add42WithConsoleLogger);
var res2 = map(List(1, 2, 3), add42);
These partially applied functions are a very useful tool. We can create library functions which are flexible (but complicated), yet make it easy to create reusable defaults so that callers don't have to be exposed to the complexity all the time.
NOTE: When using a method as an argument to
par
andcurry
you will need to provide the generic arguments that represent the arguments in the method being partially-applied or curried. I have omitted them from the examples above for clarity. No such limitation exists when usingFunc
however.