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.
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):
/api/v1/index/
will return data for the main page/api/v1/news/
will return data for the news page/api/v1/articles/
will return data for a list of articles/api/v1/article/222/
will return the page of the article with id 222/api/v1/article/edit/222/
will return the form for editing the article with id 222
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/
.
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 strategy is to create a certain function, causing that we will construct requests to the API.
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' })
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)
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
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 } }
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 */)
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/