Currying in js

Yoseob Shin·2022년 4월 20일
0

javascript

목록 보기
18/24
post-thumbnail
post-custom-banner

Currying is functional programming technique we can use to write code that is modular, easy to test, and highly reusable. Functional programming is a declarative paradigm that emphasizes immutability and pure functions — meaning the function is side-effect free and for any given input it will always return the same output.

How currying works

Converting regular function into a series of nested functions that will each take a single argument.

This makes the function calls more modular. With curried functions, calling the outer function returns the next function in the chain and so on until the innermost function is called, which then returns a value.

// traditional function
function add(a,b) {
// Could return NaN if either one argument is missing as number.
    return a + b;
}

//Curried
function curried_add(a) {
    // has access to the argument for a
    return function nested_add(b) {
        // has access to the arguments for a and b
        return a + b;
    }
}
 
// creates a local variable a and assigns it the value 1
let add_one = curried_add(1); 
 
// add_one() still has access to the argument from curried_add()
add_one(10);  // 11

The argument from calling curried_add() is available to the nested functions due to closure. A closure means that the nested function retains the scope of parent functions based on where the function is defined, even if the nested function is executed outside of that lexical scope.

A lexical scope in JavaScript means that a variable defined outside a function can be accessible inside another function defined after the variable declaration.

Lexical vs Closure

The lexical scope allows a function scope to access statically the variables from the outer scopes. Finally, a closure is a function that captures variables from its lexical scope.

You may recall that a function can access variables both in its inner and outer scope. That behavior is an example of lexical scoping rules, which means the scoping is based on the structure of the code.

Taking that one step further, when a function is invoked, lexical scoping is retained. So nested functions will continue to have access to any variables that are declared in the outer scope of parent functions. This is true even if that parent function is done executing.

let add_one = curried_add(1); 
 
// add_one() still has access to the argument from curried_add()
add_one(10);  // 11

Overall, that means that when you run the line let add_one = curried_add(1); , add_one() will retain the scope from curried_add() and therefore have access to the variable created for the argument a as 1, which you can see explained in the code snippet line by line:

Arrow function with currying

The same curried_add() function from earlier can be rewritten much more concisely using ES6 arrow function syntax:

let curried_add = a => b => a + b;
  1. let curried_add is a variable assignment to the outer arrow function, a => ....
  2. Calling curried_add takes an argument a and returns b => a + b.
  3. Invoking the second arrow function returns the sum, or a + b.

Currying in Context(커링의 문맥)

예를 들어 players 어레이에 우리가 나이나, 스포츠 종목, 거주 도시, 날짜에 따라 필터링을 해야 하는 경우가 생긴다고 보자.

const players = [
    { age: 5, sport: "soccer", city: "Chicago", dateJoined: new Date('2021-01-20') },
    { age: 6, sport: "baseball", city: "Boulder", dateJoined: new Date('2019-12-30') },
    { age: 10, sport: "soccer", city: "Chicago", dateJoined: new Date('2020-11-12') },
    { age: 11, sport: "handball", city: "San Francisco", dateJoined: new Date('2020-08-21') },
    { age: 6, sport: "soccer", city: "Chicago", dateJoined: new Date('2021-07-06') },
    { age: 8, sport: "softball", city: "Boulder", dateJoined: new Date('2019-02-27') },
    { age: 7, sport: "tennis", city: "San Francisco", dateJoined: new Date('2019-05-31') },
    { age: 4, sport: "handball", city: "San Francisco", dateJoined: new Date('2018-03-10') }
]

아래 함수는 1) 플레이어 어레이를 매개변수 city에 따라 필터링하고 그다음 나이값에 따라 sorting 할수 있다.

const sortPlayersByValueFromCity = (playersArr, city, sortKey) => {
    return playersArr.filter(player => {
        return player.city === city;
    }).sort((a,b) => {
        return a[sortKey] - b[sortKey]
    });
}
 
console.log(sortPlayersByValueFromCity(players, "San Francisco", "age"));

또 여기 아래 함수는 플레이어 어레이에 주어진 매개변수 도시로 먼저 필터링하고 그 도시에 key value 속성에 따라 필터링을 한번더 할수 있다.

const filterPlayersByValueFromCity = (playersArr, city, filterKey, filterValue) => {
 return playersArr.filter(player => {
   return player.city === city;
 }).filter(playersFromCity => playersFromCity[filterKey] === filterValue)
}
 
console.log(filterPlayersByValueFromCity(players, "San Francisco", "sport", "handball"));

앞에 두 예문에서 볼수 있듯 두함수는 크기도 크고 반복적이며 디버깅 할떄 시간이 쓸데없이 많이 들수도 있다.

커링 기술을 사용하면 보다 같은 함수를 선언형으로 세부적으로 분해하고 재사용에 용이하며 읽기도 쉽다.

무엇보다 함수에 인자를 넣는 순서에 헷갈려 잘못된 인자를 함수에 전달 할수 있어 에러가 생길 경우가 쉽다.

const filterPlayersByValueFromCity = (playersArr, city, filterKey, filterValue) => {
 return playersArr.filter(player => {
   return player.city === city;
 }).filter(playersFromCity => playersFromCity[filterKey] === filterValue)
}
 
console.log(filterPlayersByValueFromCity(players, "San Francisco", "sport", "handball"));

// *** curried ***
const setFilter = arr => key => val => arr.filter(obj => obj[key] === obj[val]);

const filterPlayers = setFiler(players);
const filterPlayersByCity = filterPlayers('city');
const filterPlayersBySanFrancisco = filterPlayersByCity('San francisco');

// 리사이클링 예 
const playersInSanFrancisco = setFilter(filterPlayersBySanFrancisco);
const handballPlayersInSanFran = playersInSanFrancisco('sport')('handball');

막상 보기에는 코드량이 일반 filterPlayersByValueFromCity()에 비해 많아 보이나 각 함수변수들은 하나의 일만 핸들링 하기에 읽기도 쉽고 미래에 재활용이 정말 좋다.
위 예문의 setFilter를 이용해 다른 속성 키-값에 따라 필터링을 반복적으로 해야 할때 각기 다른 변수들에 필터링된 값을 지정하고 재사용 할수 있다.

//SanFrancisco 말고 Chicago 도시로 필터링 하고 싶음 이렇게 한줄만 더 넣음 된다. 인자를 실수로 인해 잘못 넣을일이 없다.
const playersInChicago = filterPlayersByCity('Chicago');

With currying, we can write functions that handle one task, and are therefore not only easier to read and understand, but more reusable. For example, we can create a curried function that filters an array of objects by a provided key and value.

In this post, we took a look at how currying works under the hood thanks to closures in JavaScript, and how you can use different syntax techniques to curry your functions.

Overall, thanks to the modularity of curried functions, you can now use currying in your code to make your functions have a single purpose and therefore be easier to test, debug, maintain, and read.

currying 사용예시

아래 코드 예시는 frontsom.tistory 퍼온건데 사용예시로 리퍼런스 하기 좋은듯.

let sentence =
    greeting => seperator =>  end => name =>
        console.log(greeting + seperator + name + end);

let introduce = sentence('안녕하세요')('. ')('입니다.');
introduce('다솜'); // 안녕하세요. 다솜입니다.

let greet = sentence('안녕하세요')('? ')('씨.');
greet('다솜'); // 안녕하세요? 다솜씨.

let bye = sentence('안녕히가세요')(', ')('씨.');
bye('다솜'); // 안녕히가세요, 다솜씨.

위와 같이 sentence 라는 커링 함수 하나를 가지고 다양한 함수를 정의하고 사용할 수 있으며, 부분적으로 정의한 함수를 다시 정의해서 사용하는 패턴도 가능해 중복을 최소화할 수 있다.

전통적인 사용법
위에서는 ES6 문법을 사용해 쉽게 표현을 했지만 전통적으로 JavaScript는 기본적으로 커리함수를 지원하지 않는다. 그래서 화살표 함수 없이 커링을 구현하려면 코드가 매우 길어지게 된다. 그래서 전통적인 방법으로 구현하려면 Function.prototype 에 curry 메소드를 추가하는 방식으로 사용할 수 있다.

Function.prototype.curry = function() {
    var slice = Array.prototype.slice;
    var args = slice.apply(arguments);
    var that = this;
    return function() {
        return that.apply(null, args.concat(slice.apply(arguments)));
    };
}

var sum1 = sum.curry(1);
console.log(sum1(5));        // 6
console.log(sum1(3));        // 4

The arguments object:
arguments is an Array-like object accessible inside functions that contains the values of the arguments passed to that function. - 그치만 es6를 쓰고있음 ...rest 문법을쓰자.
Note: "Array-like" means that arguments has a length property and properties indexed from zero, but it doesn't have Array's built-in methods like forEach() or map(). See §Description for details.

function func1(a, b, c) {
  console.log(arguments[0]);
  // expected output: 1

  console.log(arguments[1]);
  // expected output: 2

  console.log(arguments[2]);
  // expected output: 3
}

func1(1, 2, 3);

The apply() method calls a function with a given this value, and arguments provided as an array (or an array-like object).

apply(thisArg)
apply(thisArg, argsArray)

thisArg -> if the method is a function in non-strict mode code, null and undefined will be replaced with the global object,

ref:
https://frontsom.tistory.com/m/10#:~:text=%EC%9E%A0%EC%8B%9C%20%EC%99%9C%20%ED%95%A8%EC%88%98%ED%98%95%20JavaScript%EB%A5%BC,%EC%9E%91%EC%84%B1
and codecademy.com

profile
coder for web development + noodler at programming synthesizers for sound design as hobbyist.
post-custom-banner

0개의 댓글