⬆️ ⬇️

Practical application of currying in js on the example of the module http requests

Hello! It is not a secret for anyone that in the programming world there are many techniques, practices and programming patterns (design), but often, having learned something new, it is not at all clear where and how to apply this new one.



Today, using the example of creating a small module wrapper for working with http requests, we will analyze the real benefit of currying - the reception of functional programming.



All beginners and those interested in using functional programming in practice - welcome, those who are well aware of what currying is - are waiting for your comments on the code, because as they say - there is no limit to perfection.



So, let's begin



But not from the concept of currying, but from the formulation of the problem, where we can apply it.



We have a kind of blog API that works according to the following principle (all matches with real APIs are random):





As you can see, in order to access the API, we need to access the api of a specific version of v1 (if it grows a little and the new version comes out) and then proceed to construct the data request.



Therefore, in js code, to get data, for example, one article with id 222, we have to write (for maximum simplification of the example, we use the native js fetch method):



 fetch('/api/v1/article/222/') .then(/* success */) .catch(/* error */) 


To edit the same article, we request this:



 fetch('/api/v1/article/edit/222/') .then(/* success */) .catch(/* error */) 


Surely you have already noticed that in our requests there are a lot of repetitive ways. For example, the path and version to our API /api/v1/ , and work with one article /api/v1/article/ and /api/v1/article/edit/ .



Following our favorite DRY rule (Don't Repeat Yourself), how to optimize API request code?



We can add parts of queries to constants, for example:



 const API = '/api' const VERSION = '/v1' const ARTICLE = `${API}${VERSION}/article` 


And now we can rewrite the examples above in this way:



Request article



 fetch(`${ARTICLE}/222/`) 


Article Editing Request



 fetch(`${ARTICLE}/edit/222/`) 


The code seems to be smaller, there are constants related to the API, but we all know what can be done much more conveniently.



I believe that there are more options for solving the problem, but our task is to consider a solution using currying.



The principle of building requests based on http services



The strategy is to create a certain function, causing that we will construct requests to the API.



How it should work



We construct the request by calling the wrapper function over the native fetch (let's call it http. Below is the full code of this function), in the arguments of which we pass the request parameters:



 cosnt httpToArticleId222 = http({ url: '/api/v1/article/222/', method: 'POST' }) 


Please note that the result of executing this http function will be a function, which contains the url and method request settings.



Now, by calling httpToArticleId222() we actually send a request to the API.



You can do more cunning, and gradually build queries. Thus, we can create a set of ready-made functions with "wired" paths to the API. We call them http services.



So, first, we construct a service for accessing the API (simultaneously adding query parameters that do not change for all subsequent queries, for example, a method)



 const httpAPI = http({ url: '/api', method: 'POST' }) 


Now we create the service for accessing the API of the first version. In the future, we will be able to create a separate branch of requests to another version of the API from the httpAPI service.



 const httpAPIv1 = httpAPI({ url: '/v1' }) 


Service access to the first version of the API is ready. Now we will create services for the rest of the data from it (recall the impromptu list at the beginning of the article)



Homepage data



 const httpAPIv1Main = httpAPIv1({ url: '/index' }) 


News page data



 const httpAPIv1News = httpAPIv1({ url: '/news' }) 


Article List Information



 const httpAPIv1Articles = httpAPIv1({ url: '/articles' }) 


Finally come to our main example, the data for the material



 const httpAPIv1Article = httpAPIv1({ url: '/article' }) 


How to get a way to edit an article? Of course you guessed it, we load the data, the httpAPIv1Article function created earlier



 const httpAPIv1ArticleEdit = httpAPIv1({ url: '/edit' }) 


Small logical result



So, we have a beautiful list of services, which, for example, are in a separate file that does not bother us at all. If something needs to be changed in the request, I know exactly where to edit.



 export { httpAPIv1Main, httpAPIv1News, httpAPIv1Articles, httpAPIv1Article, httpAPIv1ArticleEdit } 


I do import service with a certain function



 import { httpAPIv1Article } from 'services' 


And I execute the request, first completing it, adding the material id, and immediately calling the function to send the request (as they say: "izi")



 httpAPIv1Article({ url: ArticleID // id  -   })() .then(/* success */) .catch(/* error */) 


Clean, beautiful, clear (not advertising)



How it works



We can add the data to the function thanks to currying.



A bit of theory.

Currying is a way of constructing a function with the possibility of gradually applying its arguments. It is achieved by returning the function after it is called.



A classic example is addition.

We have a sum function, the first time calling which, we pass the first number for later folding. After calling it, we get a new function, waiting for the second number to calculate the sum. Here is its code (ES6 syntax)



 const sum = a => b => a + b 


Call the first time (partial application) and save the result in a variable, for example sum13



 const sum13 = sum(13) 


Now sum13 we can also call with the missing number in the argument, the result of which will be called 13 + the second argument



 sum13(7) // =>  20 


Well, how to apply it to our problem?



We create the http function, which will be a wrapper over fetch



 function http (paramUser) {} 


where paramUser is the request parameters passed at the time of the function call



Let's start to complement our function with logic



Add query parameters specified by default.



 function http (paramUser) { /** *  -,    * @type {string} */ let param = { method: 'GET', credentials: 'same-origin' } } 


And then the paramGen function, which generates the request parameters from those that are set by default and user-defined (in fact, just the merging of two objects)



 function http (paramUser) { /** *  -,    * @type {string} */ let param = { method: 'GET', credentials: 'same-origin' } /** *   , *  url  ,          * * @param {object} param   * @param {object} paramUser ,    * * @return {object}     */ function paramGen (param, paramUser) { let url = param.url || '' let newParam = Object.assign({}, param, paramUser) url += paramUser.url || '' newParam.url = url return newParam } } 


We turn to the most important thing, we describe currying



It will help us in this function, called, for example, fabric and returned by the function http



 function http (paramUser) { /** *  -,    * @type {string} */ let param = { method: 'GET', credentials: 'same-origin' } /** *   , *  url  ,          * * @param {object} param   * @param {object} paramUser ,    * * @return {object}     */ function paramGen (param, paramUser) { let url = param.url || '' url += paramUser.url || '' let newParam = Object.assign({}, param, paramUser); newParam.url = url return newParam } /** *  ,     *  ,      * *  : * * -    ,        ,     * -   ,           * -   ,      * * @param {object} param ,       * @param {object} paramUser ,   * * @return {function || promise}   ,    (fetch),     */ function fabric (param, paramUser) { if (paramUser) { if (typeof paramUser === 'string') { return fabric.bind(null, paramGen(param, { url: paramUser })) } return fabric.bind(null, paramGen(param, paramUser)) } else { //  ,   ,   param    url, //       :) return fetch(param.url, param) } } return fabric.bind(null, paramGen(param, paramUser)) } 


The first time the http function is called, the fabric function is returned, with the param parameters passed to it (and configured by the paramGen function), which will wait for its o'clock call further.



For example, we configure the query



 let httpGift = http({ url: '//omozon.ru/givemegift/' }) 


And calling httpGift , the passed parameters are applied, as a result we return fetch , if we want to preconfigure the request, we simply transfer the new parameters to the generated httpGift function and wait for it to be called without arguments



 httpGift() .then(/* success */) .catch(/* error */) 


Results



Through the use of currying in the development of various modules, we can achieve high flexibility in the use of modules and ease of testing. As, for example, when organizing an architecture of services for working with an API.



It’s as if we are creating a mini-library, using the tools of which we create a unified infrastructure of our application.



I hope the information was useful, do not hit hard, this is my first article in my life :)



All the compiled code, see you there!



')

Source: https://habr.com/ru/post/422661/



All Articles