Forever Functional – From methods to functions, and back

0
39
Forever Functional - From methods to functions, and back


Methods such as .map(...) or .filter(...) are available for arrays — but what about applying them to, for example, strings? You could want to test a string to ensure that all its characters were, say, vowels, but you couldn’t do that with .every(...) as you’d do with an array. (Yes, you could test the string with regular expressions, but that isn’t always the solution, and using regular expressions also brings problems and difficulties!) Yet another problem: we cannot use higher-order functions (as we saw in a previous article in the series) like not(...) to negate a method; it works on functions instead.

So, what’s to do? A good idea is to transform methods into equivalent functions, which we can then use in typical “functional programming ways”. In this article, we’ll see how to achieve such conversions, bringing some other advantages that we’ll comment upon. And, for the sake of balance, let’s also see how a function can be converted into a method — even if that’s not what we’ll prefer!

From methods to functions

Why would you want functions instead of methods? There are several answers to this — and we’ll even be seeing examples of “why” in this article itself. One case is passing functions as parameters; methods are linked to objects, so we cannot pass them easily. (We’ll see a case of this below.) Other limitations come when we want to do currying or partial application (excellent topics for future articles!) all of which apply to functions. Yet another case: we need functions if we want to use the (possibly forthcoming) pipeline operator, closely related to composition, which only works with functions.

Decoupling methods from the objects to which they apply helps because now everything is a function — and functions are what we want to deal with! We’ll call this conversion “demethodizing”. A “demethodized” method may even be applied to diverse data types, becoming more general and practical than the original method.

How may we “demethodize” a function? Let’s start by defining how the functions will be: we’ll go from myObject.someMethod(parameters) to someMethod(myObject, parameters). In our conversion, the first parameter to the new function will be the object to which it will apply. (This is akin to what happens in other languages: for instance, in Python the first parameter to any method is the self value, which points to the object itself.) We can now do the conversion in several alternative and equivalent ways. The .apply(…), .call(…) methods help.

1const demethodize1 =

2 (fn) =>

3 (arg0, ...args) =>

4 fn.apply(arg0, args);

5

6const demethodize2 =

7 (fn) =>

8 (arg0, ...args) =>

9 fn.call(arg0, ...args);

The .apply(...) method calls function fn using the first argument (arg0) as this, and the other arguments as an array. The .call(...) solution is similar but provides arguments individually, which we can do with spreading.

Using arrow functions provides one more way, using .bind(…) to achieve similar results to the first two ways.

1const demethodize3 =

2 (fn) =>

3 (arg0, ...args) =>

4 fn.bind(arg0, ...args)();

Binding produces a function with all its parameters set, and then we call it. You may ask: why did I single out arg0? Wouldn’t the code below work as well? The simple answer: I just wanted to show the same style of code.

1const demethodize3b =

2 (fn) =>

3 (...args) =>

4 fn.bind(...args)();

And, if you are curious, there’s yet one more way of demethodizing a function — but probably the hardest one to understand! The code below was written by Taylor Smith and explained by Leland Richardson in his blog; check it out! The explanation is well worth the read, and we won’t duplicate it here.

1const demethodize4 = Function.prototype.bind.bind(

2 Function.prototype.call

3);

Incidentally, we could observe that all solutions are, in fact, “one-liners” — though I opted for clearer formatting. Now that we’ve seen several ways of achieving the same result, let’s verify that they do indeed work!

Some examples

We mentioned earlier the idea of using a higher-order function like every(...) to check if a string was all vowels. We can do this the following way:

1const isVowel = (x) =>

2 ['a', 'e', 'i', 'o', 'u'].includes(x);

3

4const every = demethodize(Array.prototype.every);

5

6console.log(every('abcd', isVowel));

7console.log(every('eieio', isVowel));

The predicate function isVowel(...) checks if its parameter is a vowel (Duh!!). We produce the every(...) function by applying any of the demethodizing functions that we saw; take your pick! Finally, we can now check if strings are all vowels by using the newly demethodized function. Nice!

Let’s have one more example, now from my ”Mastering JavaScript Functional Programming” (2nd Edition) book. In the following way, we could convert an array of numbers into properly formatted strings with the correct thousands separator and decimal points.

1const toLocaleString = demethodize(

2 Number.prototype.toLocaleString

3);

4

5const numbers = [2209.6, 124.56, 1048576];

6

7const strings = numbers.map(toLocaleString);

8console.log(strings);

We are passing a demethodized .toLocaleString(...) function to a .map(...) call; subtle! We could even go one better, and demethodize .map(...) itself.

1const map = demethodize(Array.prototype.map);

2

3const strings2 = map(numbers, toLocaleString);

4

The last line is quintessentially functional; no methods anywhere, pure FP style throughout! This is one of the cases that we mentioned at the beginning of the article: by converting .toLocaleString(...) into a function, we can pass it around to other functions.

Open Source Session Replay

OpenReplay is an open-source alternative to FullStory and LogRocket. It gives you full observability by replaying everything your users do on your app and showing how your stack behaves for every issue. OpenReplay is self-hosted for full control over your data.

replayer.png

Happy debugging, for modern frontend teams – start monitoring your web app for free.

From functions to methods

We’ve seen how to go from methods to functions; just for the sake of fairness, let’s look at going the other way — though we don’t really recommend it! (Why not? Because adding methods to object prototypes is a global change and may have conflicts with other -equally not recommended!- such changes.) We can “methodize” a function by adding a new property to a prototype.

1const methodize = (obj, fn) => {

2 obj.prototype[fn.name] = function (...args) {

3 return fn(this, ...args);

4 };

5};

Note that we must use a function(...) declaration (not an arrow function) because of how this works; read more on that topic. To use it, we could have a function that takes a string and produces a new one separating the original string’s characters with some other string. The following shows an example.

1const separate = (str, x) => str.split('').join(x);

2

3console.log(separate('eieio', '-'));

We can now add a new .separate(...) method to every string.

1methodize(String, separate);

2

3console.log('slowly'.separate(' ... '));

4

We got it! There’s only one possible problem; depending on how you define a function, it may or may not have a name… and in the latter case, our methodize(...) function will simply crash!

In conclusion

We have seen how to use JavaScript to turn methods into independent functions and how to go the other way round to turn functions into methods. These techniques (mostly the former one, to be honest) are often used in FP. They provide useful tools by themselves, but they also let us get experience in working with less-frequently used methods, such as function application or binding. An exciting exercise in combining FP and learning!



Source link

Leave a reply

Please enter your comment!
Please enter your name here