In a language like F# (the functional language most closely related to C#) a function may be called without all of its required parameters, and it will return a new function that takes the remaining required parameters. Here is an example from Fsharp for Fun and Profit where the binding addWithConsoleLogger
refers to a partial application of adderWithPluggableLogger
.
[F#]
// create an adder that supports a pluggable logging function
let adderWithPluggableLogger logger x y =
logger "x" x
logger "y" y
let result = x + y
logger "x+y" result
result
// create a logging function that writes to the console
let consoleLogger argName argValue =
printfn "%s=%A" argName argValue
//create an adder with the console logger partially applied
let addWithConsoleLogger = adderWithPluggableLogger consoleLogger
addWithConsoleLogger 1 2
addWithConsoleLogger 42 99
This is one case where I think the zealous type inference of F# hurts the examples ability to teach, so lets try to write out the example in C#.
In the simplest case without partial application the two functions might look like this,
[C#]
int adderWithPluggableLogger (Action<string, int> logger, int x, int y)
{
logger("x", x);
logger("y", y);
var result = x + y;
logger("x+y", result);
return result;
}
void consoleLogger(string argName, int argValue) => WriteLine($"{argName}={argValue}");
The trouble arises on the next line of the F# example, where the adderWithPluggableLogger
method is called without all of its parameters, which in C# would produce a compilation error.
var addWithConsoleLogger = adderWithPluggableLogger(consoleLogger);
addWithConsoleLogger(1, 2);
addWithConsoleLogger(42, 99);
We can get an idea here what we expect the return type of adderWithPluggableLogger(consoleLogger)
might be if it were to compile and execute - it would be a function that takes two integers and returns their addition, so a Func<int, int, int>
or a delegate int binop(int a, int b);
. I'll use the delegate here for clarity, and update the adderWithPluggableLogger
to return that type.
In local method syntax,
delegate int binop(int a, int b);
binop adderWithPluggableLogger(Action<string, int> logger)
{
int addWithLogger(int x, int y)
{
logger("x", x);
logger("y", y);
var result = x + y;
logger("x+y", result);
return result;
}
return addWithLogger;
}
Or with an anonymous lambda,
delegate int binop(int a, int b);
binop adderWithPluggableLogger(Action<string, int> logger)
{
return (int x, int y) =>
{
logger("x", x);
logger("y", y);
var result = x + y;
logger("x+y", result);
return result;
};
}
In both cases, a closure is being created and the returned function holds on to a reference to the passed in logger. With one more tweak to use expression bodied members the example can be brought much closer visually to how a functional developer might describe the signature of the function.
// int -> int -> int
delegate int binop(int a, int b);
// (string -> int -> ()) -> int -> int -> int
binop adderWithPluggableLogger(Action<string, int> logger) => (int x, int y) =>
{
// . . .
return result;
};
Because we want the returned type to a binary operation on two integers I've kept the x and y params together, but a more faithful interpretation of the F# example would look like this:
Func<int, int> adderWithPluggableLogger(Action<string, int> logger) => (int x) => (int y) =>
{
// . . .
return result;
};
This final rendition results in the creation of an additional closure and is not particularly useful in the example outside of satisfying my inner purist.
Where this methodology and syntax becomes particularly powerful is when you combine it with map, filter, and fold - which in C# comes in the form of the System.Linq extension methods Select, Where, and Aggregate.
Lets say we want to explore a course catalog from Washington State University, like wsu.xml from this example xml repository. I opened up LinqPad and start with something like this
var courses = XElement.Load(@"Path\To\wsu.xml").Elements();
Func<T, bool> Not<T>(Func<T, bool> predicate) => (T value) => !predicate(value);
This Not function behaves a lot like the adderWithPluggableLogger
above, but in this case it's a generic inverterWithPluggablePredicate
of sorts. After exploring the data a little more I added some more utility functions in this same style.
// for selecting days from a course element
IEnumerable<string> Days(XElement course) => course.Element("days")?.Value?.Split(",") ?? Enumerable.Empty<string>();
// Check if course occurs on a day
Func<XElement, bool> IsOnDay(string day) => (XElement course) => Days(course).Contains(day);
// functions for checking course availability
int LimitOf(XElement course) => int.Parse(course.Element("limit").Value);
int EnrolledIn(XElement course) => int.Parse(course.Element("enrolled").Value);
bool IsOpen(XElement course) => EnrolledIn(course) < LimitOf(course);
// map an element to full course code
string CourseCode(XElement course) => $"{course.Element("prefix").Value} {course.Element("crs").Value}";
// map an element to its instructor
string Instructor(XElement course) => course.Element("instructor")?.Value;
// check if a course is by a specific instructor
Func<XElement, bool> IsByInstructor(string instructor) => (XElement course) => course.Element("instructor")?.Value == instructor;
All of these allow me to write a complex query like "all open courses taught by Miller that aren't on a monday" extremely similarly to how I'd say it in English.
courses
.Where(IsOpen)
.Where(IsByInstructor("MILLER"))
.Where(Not(IsOnDay("M")))
.Select(CourseCode)
.Dump();
The .Dump()
is a LinqPad method for displaying the results in rich text, which with the linked exmaple data comes out to
IEnumerable<string> (12 items)
- GEOL 426
- GEOL 428
- GEOL 526
- PSYCH 312
- PSYCH 401
- STAT 428
- T & L 521
- ZOOL 224
- ZOOL 225
- ZOOL 498
- CH E 432
- CH E 432
Posted as part of the 2019 C# Advent
https://crosscuttingconcerns.com/The-Third-Annual-csharp-Advent
Top comments (2)
Very good point. Great explanation.
Only one note : shouldn't you have written like this:
in the last example?
Thanks for this article. I've been using C# for a few years, but I'm new to F#, and I think this is the most helpful explanation of partial application that I've encountered thus far.