Functional Programming In Js With The Example Of The Http Request Module

F Programming Main Logo

Functional Programming In Js With The Example Of The Http Request Module

Hello! It’s no secret that in the programming world there are many techniques, practices, and patterns of programming (design), but often, learning something new, it’s completely unclear where and how to use this new one.

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

To all newcomers and interested in the application of functional programming in practice – welcome, those who perfectly understand what is currying – I’m waiting for your comments on the code, for as they say – there is no limit to perfection.

So, let’s begin

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

We have some kind of blog API, working on the following principle (all coincidences with real APIs are random):

  • the request to /api/v1/index/ will return data for the main page
  • request to /api/v1/news/ return data for news page
  • request to /api/v1/articles/ return data for article list
  • request to /api/v1/article/222/ returns the article page with id 222
  • request to /api/v1/article/edit/222/ will return the form of editing an article with id 222
    … and so on, further, further

As you can see, in order to access the API we need to refer to api of a certain version of v1 (it will grow a little and a new version will be released) and then further construct the data query.

Therefore, in js code, to get data, for example, one article with id 222, we have to write (to simplify the example as much as possible, we use the native fs fetch method):


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

To edit the same article, we will ask this:


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

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

Following our favorite rule of DRY (Do not Repeat Yourself), how to optimize the API request code?

We can add query parts 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.

Article request:


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

Article editing request:


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

The code became like-would be less, there were constants, related to API, but we know that it is possible to make much more conveniently.

I believe that there are still options for solving the problem, but our task is to consider the solution by means of carrying.

The principle of building queries based on http services

The strategy is to create a function that causes us to construct queries against the API.

How it should work

We construct the query by calling the wrapper function over the native fetch (call it http. The full code of this function is laid out below), in the arguments of which we pass query parameters:


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

Note that the result of this http function will be a function that contains the settings for the url and method request.

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

You can act trickier, and stage-by-stage design queries. Thus, we can create a set of ready-made functions with “wired” paths to the API. We will call them http services.

So, first, we construct a service for accessing the API (simultaneously adding query parameters that are unchanged for all subsequent requests, for example, a method)


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

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


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

The service for accessing the API of the first version is ready. Now from it, we will create services for other data (remember the improvised list at the beginning of the article)

Main page data:


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

News page data:


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

Article list data:


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

Finally, we approach our main example, the data for the material


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

How to get the path before editing the article? Of course, you guessed it, uploading the data created earlier function httpAPIv1Artikle


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

A small logical result

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


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

I’m importing a service with a specific function


import { httpAPIv1Article } from 'services'

And I execute the query, first by pre-constructing it, by adding the id of the material, and then I call the function to send the request.


httpAPIv1Article ({
url: ArticleID // id is received somewhere in the code
}) ()
.then (/ * success * /)
.catch (/ * error * /)

Pure, beautiful, clear (not advertising)

How it works

We can “load” the given function with the help of carrying.

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 its call.

A classic example is an addition.

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


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

We call the first time (partial application) and store 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 13 + the second argument.


sum13 (7) // => result 20

Well, how does this apply to our task?

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


function http (paramUser) {}

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

Let’s begin to complement our function with logic.

Add the query parameters specified by default.


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

And then the function paramGen, which generates query parameters from those that are specified by default and custom (in fact, just the measurement of two objects).


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

/ **
* Query parameter generator,
* the url field adds up, so we can supplement it with different calls
*
* @param {object} param initial parameters
* @param {object} paramUser parameters by which we initialize
*
* @ return {object} return a new parameter object
* /
function paramGen (param, paramUser) {
let url = param.url || ''

let newParam = Object.assign ({}, param, paramUser)
url + = paramUser.url || ''
newParam.url = url

return newParam
}
}

We pass to the most important, describe the currying

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


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

/ **
* Query parameter generator,
* the url field adds up, so we can supplement it with different calls
*
* @param {object} param initial parameters
* @param {object} paramUser parameters by which we initialize
*
* @ return {object} return a new parameter object
* /
function paramGen (param, paramUser) {
let url = param.url || ''
url + = paramUser.url || ''

let newParam = Object.assign ({}, param, paramUser);
newParam.url = url

return newParam
}

/ **
* Factory function returning a new module instance
* calling it, you can thus configure the requests
*
* Note:
*
* - if you do not pass arguments, then the function will start querying data on the parameters that it has
* - if you pass a string, it will be automatically interpreted as adding to the query string
* - if you transfer an object, it will configure the request
*
* @param {object} param Parameters saved previously and passed through the currying
* @param {object} paramUser parameters passed by the user
*
* @return {function || promise} return the function, or query promo (fetch), depending on the argument
* /
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 {
// I know that you will say that in the param goes and useless url,
// let the solution be your homework :)
return fetch (param.url, param)
}
}

return fabric.bind (null, paramGen (param, paramUser))
}

When the http function is called for the first time, the fabric function is returned, with param parameters passed to it (and configured paramGen), which will wait for its call in the future.

For example, configure the query:


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

And invoking httpGift, the passed parameters are applied, as a result, we return fetch, if we want to configure the request, we simply transfer the new parameters to the generated httpGift function and wait for its call without arguments.


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

Conclusion

Thanks to 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 the architecture services for the API.

We like creating a mini-library, using the tools with which we create a single infrastructure for our application.

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