자바 스크립트의 함수형 프로그래밍 원리

오랜 시간 동안 객체 지향 프로그래밍을 배우고 작업 한 후 시스템 복잡성에 대해 생각하기 위해 한 걸음 물러서 게되었습니다.

“Complexity is anything that makes software hard to understand or to modify."— John Outerhout

연구를하면서 불변성 및 순수 함수와 같은 함수형 프로그래밍 개념을 발견했습니다. 이러한 개념을 통해 부작용없는 기능을 구축 할 수 있으므로 다른 이점과 함께 시스템을 유지 관리하기가 더 쉽습니다.

이 게시물에서는 JavaScript의 많은 코드 예제와 함께 함수형 프로그래밍과 몇 가지 중요한 개념에 대해 자세히 설명합니다.

함수형 프로그래밍이란 무엇입니까?

함수형 프로그래밍은 프로그래밍 패러다임 — 컴퓨터 프로그램의 구조와 요소를 구축하는 스타일 — 계산을 수학적 함수의 평가로 취급하고 상태 변경 및 변경 가능한 데이터를 방지합니다 — Wikipedia

순수한 기능

함수형 프로그래밍을 이해하고 싶을 때 배운 첫 번째 기본 개념은 순수 함수 입니다. 그러나 이것이 정말로 무엇을 의미합니까? 함수를 순수하게 만드는 것은 무엇입니까?

그렇다면 함수가 있는지 여부를 어떻게 알 수 pure있습니까? 다음은 순도에 대한 매우 엄격한 정의입니다.

  • 동일한 인수가 주어지면 동일한 결과를 반환합니다 (라고도 함 deterministic).
  • 관찰 가능한 부작용을 일으키지 않습니다.

동일한 인수가 주어지면 동일한 결과를 반환합니다.

원의 면적을 계산하는 함수를 구현한다고 상상해보십시오. 불순한 함수는 radius매개 변수로 수신 한 다음 다음을 계산합니다 radius * radius * PI.

let PI = 3.14; const calculateArea = (radius) => radius * radius * PI; calculateArea(10); // returns 314.0

이것이 왜 불순한 기능입니까? 단순히 함수에 매개 변수로 전달되지 않은 전역 개체를 사용하기 때문입니다.

이제 일부 수학자들이 PI값이 실제로 있다고 주장 42하고 전역 객체의 값을 변경 한다고 상상해보십시오 .

우리의 불순한 함수는 이제 10 * 10 * 42= 4200. 동일한 매개 변수 ( radius = 10)에 대해 결과가 다릅니다.

고쳐 봅시다!

let PI = 3.14; const calculateArea = (radius, pi) => radius * radius * pi; calculateArea(10, PI); // returns 314.0

이제 우리는 항상의 값을 PI매개 변수로 함수에 전달합니다. 이제 우리는 함수에 전달 된 매개 변수에 액세스하고 있습니다. 아니 external object.

  • 매개 변수 radius = 10및의 PI = 3.14경우 항상 결과가 동일합니다.314.0
  • 매개 변수 radius = 10및의 PI = 42경우 항상 결과가 동일합니다.4200

파일 읽기

함수가 외부 파일을 읽는다면 순수한 함수가 아닙니다. 파일의 내용이 변경 될 수 있습니다.

const charactersCounter = (text) => `Character count: ${text.length}`; function analyzeFile(filename) { let fileContent = open(filename); return charactersCounter(fileContent); }

난수 생성

난수 생성기에 의존하는 함수는 순수 할 수 없습니다.

function yearEndEvaluation() { if (Math.random() > 0.5) { return "You get a raise!"; } else { return "Better luck next year!"; } }

관찰 가능한 부작용을 일으키지 않습니다.

관찰 가능한 부작용의 예로는 전역 개체 또는 참조로 전달 된 매개 변수 수정이 있습니다.

이제 정수 값을 받고 1만큼 증가 된 값을 반환하는 함수를 구현하려고합니다.

let counter = 1; function increaseCounter(value) { counter = value + 1; } increaseCounter(counter); console.log(counter); // 2

우리에게는 counter가치가 있습니다. 우리의 불순한 함수는 그 값을 받고 1 씩 증가 된 값으로 카운터를 재 할당합니다.

let counter = 1; const increaseCounter = (value) => value + 1; increaseCounter(counter); // 2 console.log(counter); // 1

관찰 : 함수형 프로그래밍에서 가변성은 권장되지 않습니다.

전역 개체를 수정하고 있습니다. 하지만 우리는 어떻게 만들 pure까요? 1만큼 증가 된 값을 반환하면됩니다.

순수 함수 increaseCounter가 2를 반환하지만 counter값은 여전히 ​​동일합니다. 이 함수는 변수 값을 변경하지 않고 증가 된 값을 반환합니다.

이 두 가지 간단한 규칙을 따르면 프로그램을 더 쉽게 이해할 수 있습니다. 이제 모든 기능이 격리되어 시스템의 다른 부분에 영향을 미칠 수 없습니다.

순수 함수는 안정적이고 일관 적이며 예측 가능합니다. 동일한 매개 변수가 주어지면 순수 함수는 항상 동일한 결과를 반환합니다. 동일한 매개 변수가 다른 결과를 갖는 상황을 생각할 필요가 없습니다. 이는 결코 발생하지 않기 때문입니다.

순수한 기능 이점

코드는 확실히 테스트하기 쉽습니다. 우리는 아무것도 조롱 할 필요가 없습니다. 따라서 다른 컨텍스트로 순수 함수를 단위 테스트 할 수 있습니다.

  • 매개 변수가 주어지면 A함수가 값을 반환 할 것으로 예상합니다.B
  • 매개 변수가 주어지면 C함수가 값을 반환 할 것으로 예상합니다.D

간단한 예는 숫자 모음을 수신하고이 모음의 각 요소를 증가시킬 것으로 예상하는 함수입니다.

let list = [1, 2, 3, 4, 5]; const incrementNumbers = (list) => list.map(number => number + 1);

numbers배열을 받고 map각 숫자를 증가시키는 데 사용 하고 증가 된 숫자의 새 목록을 반환합니다.

incrementNumbers(list); // [2, 3, 4, 5, 6]

를 들어 input[1, 2, 3, 4, 5], 예상되는 output[2, 3, 4, 5, 6].

불변성

시간이 지남에 따라 변하지 않거나 변경할 수 없습니다.

데이터가 변경 불가능한 경우상태는 변경할 수 없습니다생성 된 후.변경 불가능한 개체를 변경하려면 변경할 수 없습니다. 대신새 값으로 새 개체를 만듭니다.

JavaScript에서 우리는 일반적으로 for루프를 사용합니다 . 이 다음 for문에는 몇 가지 가변 변수가 있습니다.

var values = [1, 2, 3, 4, 5]; var sumOfValues = 0; for (var i = 0; i < values.length; i++) { sumOfValues += values[i]; } sumOfValues // 15

반복 할 때마다 isumOfValue상태를 변경합니다 . 하지만 반복에서 변경을 어떻게 처리할까요? 재귀.

 let list = [1, 2, 3, 4, 5]; let accumulator = 0; function sum(list, accumulator) { if (list.length == 0) { return accumulator; } return sum(list.slice(1), accumulator + list[0]); } sum(list, accumulator); // 15 list; // [1, 2, 3, 4, 5] accumulator; // 0

So here we have the sum function that receives a vector of numerical values. The function calls itself until we get the list empty (our recursion base case). For each "iteration" we will add the value to the total accumulator.

With recursion, we keep our variablesimmutable. The list and the accumulator variables are not changed. It keeps the same value.

Observation: We can use reduce to implement this function. We will cover this in the higher order functions topic.

It is also very common to build up the final state of an object. Imagine we have a string, and we want to transform this string into a url slug.

In Object Oriented Programming in Ruby, we would create a class, let’s say, UrlSlugify. And this class will have a slugify method to transform the string input into a url slug.

class UrlSlugify attr_reader :text def initialize(text) @text = text end def slugify! text.downcase! text.strip! text.gsub!(' ', '-') end end UrlSlugify.new(' I will be a url slug ').slugify! # "i-will-be-a-url-slug"

It’s implemented!

Here we have imperative programming saying exactly what we want to do in each slugify process — first lower-case, then remove useless white spaces and, finally, replace remaining white spaces with hyphens.

But we are mutating the input state in this process.

We can handle this mutation by doing function composition, or function chaining. In other words, the result of a function will be used as an input for the next function, without modifying the original input string.

const string = " I will be a url slug "; const slugify = string => string .toLowerCase() .trim() .split(" ") .join("-"); slugify(string); // i-will-be-a-url-slug

Here we have:

  • toLowerCase: converts the string to all lower case
  • trim: removes white-space from both ends of a string
  • split and join: replaces all instances of match with replacement in a given string

We combine all these 4 functions and we can "slugify" our string.

Referential transparency

Let’s implement a square function:

const square = (n) => n * n;

This pure function will always have the same output, given the same input.

square(2); // 4 square(2); // 4 square(2); // 4 // ...

Passing 2 as a parameter of the square function will always returns 4. So now we can replace the square(2) with 4. Our function is referentially transparent.

Basically, if a function consistently yields the same result for the same input, it is referentially transparent.

pure functions + immutable data = referential transparency

With this concept, a cool thing we can do is to memoize the function. Imagine we have this function:

const sum = (a, b) => a + b;

And we call it with these parameters:

sum(3, sum(5, 8));

The sum(5, 8) equals 13. This function will always result in 13. So we can do this:

sum(3, 13);

And this expression will always result in 16. We can replace the entire expression with a numerical constant and memoize it.

Functions as first-class entities

The idea of functions as first-class entities is that functions are also treated as values and used as data.

Functions as first-class entities can:

  • refer to it from constants and variables
  • pass it as a parameter to other functions
  • return it as result from other functions

The idea is to treat functions as values and pass functions like data. This way we can combine different functions to create new functions with new behavior.

Imagine we have a function that sums two values and then doubles the value. Something like this:

const doubleSum = (a, b) => (a + b) * 2;

Now a function that subtracts values and the returns the double:

const doubleSubtraction = (a, b) => (a - b) * 2;

These functions have similar logic, but the difference is the operators functions. If we can treat functions as values and pass these as arguments, we can build a function that receives the operator function and use it inside our function.

const sum = (a, b) => a + b; const subtraction = (a, b) => a - b; const doubleOperator = (f, a, b) => f(a, b) * 2; doubleOperator(sum, 3, 1); // 8 doubleOperator(subtraction, 3, 1); // 4

Now we have an f argument, and use it to process a and b. We passed the sum and subtraction functions to compose with the doubleOperator function and create a new behavior.

Higher-order functions

When we talk about higher-order functions, we mean a function that either:

  • takes one or more functions as arguments, or
  • returns a function as its result

The doubleOperator function we implemented above is a higher-order function because it takes an operator function as an argument and uses it.

You’ve probably already heard about filter, map, and reduce. Let's take a look at these.

Filter

Given a collection, we want to filter by an attribute. The filter function expects a true or false value to determine if the element should or should not be included in the result collection. Basically, if the callback expression is true, the filter function will include the element in the result collection. Otherwise, it will not.

A simple example is when we have a collection of integers and we want only the even numbers.

Imperative approach

An imperative way to do it with JavaScript is to:

  • create an empty array evenNumbers
  • iterate over the numbers array
  • push the even numbers to the evenNumbers array
var numbers = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10]; var evenNumbers = []; for (var i = 0; i < numbers.length; i++) { if (numbers[i] % 2 == 0) { evenNumbers.push(numbers[i]); } } console.log(evenNumbers); // (6) [0, 2, 4, 6, 8, 10]

We can also use the filter higher order function to receive the even function, and return a list of even numbers:

const even = n => n % 2 == 0; const listOfNumbers = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10]; listOfNumbers.filter(even); // [0, 2, 4, 6, 8, 10]

One interesting problem I solved on Hacker Rank FP Path was the Filter Array problem. The problem idea is to filter a given array of integers and output only those values that are less than a specified value X.

An imperative JavaScript solution to this problem is something like:

var filterArray = function(x, coll) { var resultArray = []; for (var i = 0; i < coll.length; i++) { if (coll[i] < x) { resultArray.push(coll[i]); } } return resultArray; } console.log(filterArray(3, [10, 9, 8, 2, 7, 5, 1, 3, 0])); // (3) [2, 1, 0]

We say exactly what our function needs to do — iterate over the collection, compare the collection current item with x, and push this element to the resultArray if it pass the condition.

Declarative approach

But we want a more declarative way to solve this problem, and using the filter higher order function as well.

A declarative JavaScript solution would be something like this:

function smaller(number) { return number < this; } function filterArray(x, listOfNumbers) { return listOfNumbers.filter(smaller, x); } let numbers = [10, 9, 8, 2, 7, 5, 1, 3, 0]; filterArray(3, numbers); // [2, 1, 0]

Using this in the smaller function seems a bit strange in the first place, but is easy to understand.

this will be the second parameter in the filter function. In this case, 3 (the x) is represented by this. That's it.

We can also do this with maps. Imagine we have a map of people with their name and age.

let people = [ { name: "TK", age: 26 }, { name: "Kaio", age: 10 }, { name: "Kazumi", age: 30 } ];

And we want to filter only people over a specified value of age, in this example people who are more than 21 years old.

const olderThan21 = person => person.age > 21; const overAge = people => people.filter(olderThan21); overAge(people); // [{ name: 'TK', age: 26 }, { name: 'Kazumi', age: 30 }]

Summary of code:

  • we have a list of people (with name and age).
  • we have a function olderThan21. In this case, for each person in people array, we want to access the age and see if it is older than 21.
  • we filter all people based on this function.

Map

The idea of map is to transform a collection.

map메서드는 모든 요소에 함수를 적용하고 반환 된 값에서 새 컬렉션을 빌드하여 컬렉션을 변환합니다.

people위 의 동일한 컬렉션을 가져옵니다 . 지금은 "연령 초과"로 필터링하고 싶지 않습니다. 우리는 단지 TK is 26 years old. 최종 문자열이 될 수 그래서 :name is :age years old어디 :name:age의 각 요소의 속성입니다 people수집.

명령형 JavaScript 방식에서는 다음과 같습니다.

var people = [ { name: "TK", age: 26 }, { name: "Kaio", age: 10 }, { name: "Kazumi", age: 30 } ]; var peopleSentences = []; for (var i = 0; i < people.length; i++) { var sentence = people[i].name + " is " + people[i].age + " years old"; peopleSentences.push(sentence); } console.log(peopleSentences); // ['TK is 26 years old', 'Kaio is 10 years old', 'Kazumi is 30 years old'] 

선언적 JavaScript 방식에서는 다음과 같습니다.

const makeSentence = (person) => `${person.name} is ${person.age} years old`; const peopleSentences = (people) => people.map(makeSentence); peopleSentences(people); // ['TK is 26 years old', 'Kaio is 10 years old', 'Kazumi is 30 years old']

전체 아이디어는 주어진 배열을 새 배열로 변환하는 것입니다.

또 다른 흥미로운 해커 순위 문제는 업데이트 목록 문제였습니다. 우리는 주어진 배열의 값을 절대 값으로 업데이트하려고합니다.

For example, the input [1, 2, 3, -4, 5]needs the output to be [1, 2, 3, 4, 5]. The absolute value of -4 is 4.

A simple solution would be an in-place update for each collection value.

var values = [1, 2, 3, -4, 5]; for (var i = 0; i < values.length; i++) { values[i] = Math.abs(values[i]); } console.log(values); // [1, 2, 3, 4, 5]

We use the Math.abs function to transform the value into its absolute value, and do the in-place update.

This is not a functional way to implement this solution.

First, we learned about immutability. We know how immutability is important to make our functions more consistent and predictable. The idea is to build a new collection with all absolute values.

Second, why not use map here to "transform" all data?

My first idea was to test the Math.abs function to handle only one value.

Math.abs(-1); // 1 Math.abs(1); // 1 Math.abs(-2); // 2 Math.abs(2); // 2

We want to transform each value into a positive value (the absolute value).

Now that we know how to do absolute for one value, we can use this function to pass as an argument to the map function. Do you remember that a higher order function can receive a function as an argument and use it? Yes, map can do it!

let values = [1, 2, 3, -4, 5]; const updateListMap = (values) => values.map(Math.abs); updateListMap(values); // [1, 2, 3, 4, 5]

Wow. So beautiful!

Reduce

The idea of reduce is to receive a function and a collection, and return a value created by combining the items.

A common example people talk about is to get the total amount of an order. Imagine you were at a shopping website. You’ve added Product 1, Product 2, Product 3, and Product 4 to your shopping cart (order). Now we want to calculate the total amount of the shopping cart.

In imperative way, we would iterate the order list and sum each product amount to the total amount.

var orders = [ { productTitle: "Product 1", amount: 10 }, { productTitle: "Product 2", amount: 30 }, { productTitle: "Product 3", amount: 20 }, { productTitle: "Product 4", amount: 60 } ]; var totalAmount = 0; for (var i = 0; i < orders.length; i++) { totalAmount += orders[i].amount; } console.log(totalAmount); // 120

Using reduce, we can build a function to handle the amount sum and pass it as an argument to the reduce function.

let shoppingCart = [ { productTitle: "Product 1", amount: 10 }, { productTitle: "Product 2", amount: 30 }, { productTitle: "Product 3", amount: 20 }, { productTitle: "Product 4", amount: 60 } ]; const sumAmount = (currentTotalAmount, order) => currentTotalAmount + order.amount; const getTotalAmount = (shoppingCart) => shoppingCart.reduce(sumAmount, 0); getTotalAmount(shoppingCart); // 120

Here we have shoppingCart, the function sumAmount that receives the current currentTotalAmount , and the order object to sum them.

The getTotalAmount function is used to reduce the shoppingCart by using the sumAmount and starting from 0.

Another way to get the total amount is to compose map and reduce. What do I mean by that? We can use map to transform the shoppingCart into a collection of amount values, and then just use the reduce function with sumAmount function.

const getAmount = (order) => order.amount; const sumAmount = (acc, amount) => acc + amount; function getTotalAmount(shoppingCart) { return shoppingCart .map(getAmount) .reduce(sumAmount, 0); } getTotalAmount(shoppingCart); // 120

The getAmount receives the product object and returns only the amount value. So what we have here is [10, 30, 20, 60]. And then the reduce combines all items by adding up. Beautiful!

We took a look at how each higher order function works. I want to show you an example of how we can compose all three functions in a simple example.

Talking about shopping cart, imagine we have this list of products in our order:

let shoppingCart = [ { productTitle: "Functional Programming", type: "books", amount: 10 }, { productTitle: "Kindle", type: "eletronics", amount: 30 }, { productTitle: "Shoes", type: "fashion", amount: 20 }, { productTitle: "Clean Code", type: "books", amount: 60 } ]

We want the total amount of all books in our shopping cart. Simple as that. The algorithm?

  • filter by book type
  • transform the shopping cart into a collection of amount using map
  • combine all items by adding them up with reduce
let shoppingCart = [ { productTitle: "Functional Programming", type: "books", amount: 10 }, { productTitle: "Kindle", type: "eletronics", amount: 30 }, { productTitle: "Shoes", type: "fashion", amount: 20 }, { productTitle: "Clean Code", type: "books", amount: 60 } ] const byBooks = (order) => order.type == "books"; const getAmount = (order) => order.amount; const sumAmount = (acc, amount) => acc + amount; function getTotalAmount(shoppingCart) { return shoppingCart .filter(byBooks) .map(getAmount) .reduce(sumAmount, 0); } getTotalAmount(shoppingCart); // 70

Done!

Resources

I’ve organised some resources I read and studied. I’m sharing the ones that I found really interesting. For more resources, visit my Functional Programming Github repository

  • EcmaScript 6 course by Wes Bos
  • JavaScript by OneMonth
  • Ruby specific resources
  • Javascript specific resources
  • Clojure specific resources
  • Learn React by building an App

Intros

  • Learning FP in JS
  • Intro do FP with Python
  • Overview of FP
  • A quick intro to functional JS
  • What is FP?
  • Functional Programming Jargon

Pure functions

  • What is a pure function?
  • Pure Functional Programming 1
  • Pure Functional Programming 2

Immutable data

  • Immutable DS for functional programming
  • Why shared mutable state is the root of all evil

Higher-order functions

  • Eloquent JS: Higher Order Functions
  • Fun fun function Filter
  • Fun fun function Map
  • Fun fun function Basic Reduce
  • Fun fun function Advanced Reduce
  • Clojure Higher Order Functions
  • Purely Function Filter
  • Purely Functional Map
  • Purely Functional Reduce

Declarative Programming

  • Declarative Programming vs Imperative

That’s it!

Hey people, I hope you had fun reading this post, and I hope you learned a lot here! This was my attempt to share what I’m learning.

Here is the repository with all codes from this article.

Come learn with me. I’m sharing resources and my code in this Learning Functional Programming repository.

I also wrote an FP post but using mainly Clojure

I hope you saw something useful to you here. And see you next time! :)

My Twitter & Github.

TK.