August 12, 2022

Robotic Notes

All technology News

FOREVER FUNCTIONAL–Working with functions… but partially!

3 min read


In our previous article on currying we discussed an important functional programming technique that lets you fix parameters of a function, in a specific way. Now, we will consider a more generic transformation, Partial Applicationthat gives you even more power.

What is Partial Application?

Partial Application is having a function with a certain number of parameters, and fixing the values ​​of some of them, so you are left with a function with fewer parameters. Just so you know if you happen to come into it, the “official” name for the process of fixing some parameters and producing a function of the unfixed ones is called projection; you would have projected the original function onto the three unfixed parameters.

To clarify, let’s suppose we have a function with five parameters; you could fix any of them —say, the second and the fifth— producing a new function that now expects the still-unbound other three. If you call that with some three arguments, it will produce precisely the same result as the original function if it had been called with the two fixed values ​​plus the recent three. The following diagram (from page 197 of my MASTERING JAVASCRIPT FUNCTIONAL PROGRAMMING book) shows the concept.

Partial Application

The original function needs five arguments, and produces some result. With partial application, we would get a new function (f1) that, when called with two values ​​(b and e), produces another function (f2). When that function gets called with the other three parameters (a, c, and d), it produces the same result as the original function.

How can we use this? Let’s have some examples before moving on to implementing this feature.

Why / when use partial application?

Let’s revisit some examples of my previous article. For instance, we saw that with an isOlderThan(a,b) function that tells you if b >= ayou could then derive a isAdult() function that checks if someone is 21 or more, by currying:

1const isAdult = curried_version_of_isOlderThan(21);

2

3if (isAdult(student.age)) { ... }

4

5const adultAges = agesArray.filter(isAdult);

This is quite fine – but what would you do if you didn’t have isOlderThan(a,b) but rather had isNotLessThan(a,b) meaning a >= b? Currying wouldn’t help you, because here you’d want to fix the second parameter to 21, not the first one. With partial application, we can solve this.

1const isAdult = partialVersionOfIsNotLessThan(__, 21);

The __ argument means “this value is not known yet”, so your isAdult() function is a version of isNotLessThan() that has its second argument fixed at 21; cool!

Let’s see another example, also similar to an example from my currying article. Imagine you have a function that calculates the total amount to pay for an item, considering the local tax.

1const totalWithTax = (price, tax, units) = (price * units) * (1 + tax/100);

If the local tax for your region was 3%, you could then create a more specific version of the code.

1const totalWithLocalTax = partialVersionOfTotalWithTax(__, 3, __);

This new function receives just two parameters (price and units) and will apply a 3% tax.

You wouldn’t be limited to fixing just a single value; we want to be able to set any number of parameters and leave the others “open” to future calls; let’s do this!

Open Source Session Replay

OpenReplay is an open-source, session replay suite that lets you see what users do on your web app, helping you troubleshoot issues faster. OpenReplay is self-hosted for full control over your data.

replayer.png

Start enjoying your debugging experience – start using OpenReplay for free.

Implementing partial application with closures

What should a function that allows partial application look like? The function should keep track of the arguments it has already received, and when it’s called with more arguments, it should replace the pending ones (__) with the newly received ones. If at some point it happens that all the parameters to the original function have been provided, and there are no pending arguments to replace, then we can produce the result. The implementation is longish and uses recursion, so we’ll have to analyze it slowly and carefully.

1const partialByClosure = (fn, ...args) => {

2 const partialize =

3 (...args1) =>

4 (...args2) => {

5 for (let i = 0; i < args1.length && args2.length; i++) {

6 if (args1[i] === __) {

7 args1[i] = args2.shift();

8 }

9 }

10 const allParams = [...args1, ...args2];

11 return (

12 allParams.includes(__) || allParams.length < fn.length ? partialize : fn

13 )(...allParams);

14 };

15 return partialize(...args);

16};

The partialize() function is key: given a list of arguments (args1), it produces a function that will receive another list of arguments (args2):

  • First, it goes through args1and while there are arguments left to process in args2if it finds a pending argument, it replaces it with an argument from arg2; the result is an updated arg1 list.
  • Then, it appends whatever there may be left of arg2 to the list of arguments that we had (arg1), producing allParams.
  • Finally, if that list still has pending arguments or if not all the necessary arguments have been provided, the function recursively calls itself to produce a new, intermediate function.
  • Otherwise, when there are no pending arguments, and all the needed arguments are there, we call the original function.

Wow, a bit complex! We still got a minor detail; what can we use as the __ “Not set yet” value? An interesting way (not the only one!) Of doing this is by creating a Symbol.

1const __ = Symbol();

This is a unique value, so no matter what other libraries or pieces of code may do, there can be no confusion.

Let’s now see how our “partializing” works!

Testing our function

Let’s do a few tests with a nonsensical but straightforward function that just produces a string with the five arguments that it received.

1const fn5 = function (j, k, l, m, n) {

2 return `FIVE ${j}.${k}.${l}.${m}.${n}`;

3};

4

5console.log(fn5(1,2,3,4,5));

Then, all the following work – and note that we are making more and more complex usage down the line!

1console.log(partialByClosure(fn5)(__, 22, __, 44, __)(11, 33, 55));

2

3

4console.log(partialByClosure(fn5, __, 222, __, 444, __)(111, 333)(555));

5

6

7console.log(

8 partialByClosure(fn5, __, 2222, __)(__, 3333, __, 5555)(1111, 4444)

9);

10

Let’s analyze them.

  • In the first case, we take the original function, fix the 2nd and 4th parameters, and when we call it with the three remaining ones, it produces the result.
  • In the second case, we again fix the 2nd and 4th parameters, but we then pass only two more, so we get a function that expects the last one before producing the result.
  • In the last case, the most interesting one, we first fix the 2nd parameter; then we fix the 2nd and 4th of the new function, which work out to be the 3rd and 5th of the original one. After that, we get the expected result when we provide the two still missing values ​​(that will match the 1st and 4th of the original function).

In a way somewhat reminiscing of currying, when we call the function but not providing enough arguments, we get a new function. The mechanism isn’t at all the same as currying, but there’s a similarity. And, even if currying and partial application are different concepts, if you do partial application and always provide just the first argument to the function, the result is an identical sequence of calls to that of currying. Let’s check, using an example from the currying article:

1const fn3 = function (c, d, e) {

2 return `THREE... ${c}-${d}-${e}`;

3};

4

5const fn3C = curryByBind(fn3);

6const fn3P = partialByClosure(fn3);

7

8console.log(fn3C(1)(2)(3));

9console.log(fn3P(1)(2)(3));

Results are the same, although the mechanisms for fn3C() and fn3P() are totally different.

Doing Partial Application with the Function.prototype

To make work simpler and avoid having to call the partialByClosure() function ourselves, we may directly modify the prototype of all functions, so partial application will be available with no further ado. The needed change is the following.

As when we did currying, we may do this in two different ways. The first would be by adding a partial() method to the Function.prototype:

1Function.prototype.partial = function (...p) {

2 return partialByClosure(this)(...p);

3};

Alternatively we could use Object.defineProperty:

1Object.defineProperty(Function.prototype, "partial", {

2 get: function () {

3 return partialByClosure(this);

4 },

5});

Both solutions work, though I’d recommend the second because it disables any possible changes to the .partial() method by making it “non-writable”. Now, we can redo the first pair of examples in the article as follows:

1const isAdult = isNotLessThan.partial(__, 21);

2const totalWithLocalTax = totalWithTax.partial(__, 3, __);

Simple!

Summing up

We’re done; we have added a new powerful way of working with functions that will help you produce clearer, more understandable code. As with currying, we should remark that:

  • partial application depends on functions having a known number of parameters; it won’t work with functions that receive an undefined number of arguments. This said, you could implement a workaround by modifying partialByClosure() so if the original function doesn’t have a fixed number of arguments, as soon as there are no __ arguments present, it could produce a result. I’ll leave the change up to you!
  • partial application, as we have it, is meant for functions, not methods. If need be, you could first transform the method into a function (as we saw in a previous article), and then there would be no further problems.

Partial Application is another basic but fundamental technique in Functional Programming, and I hope you’ll add it to your bag of tools!

newsletter



Source link