원문
Greg Weng, https://hacks.mozilla.org/2015/01/from-mapreduce-to-javascript-functional-programming/
ECMASript 5.1버전 이후로 Array.prototype.map
과 Array.prototype.reduce
는 주요 브라우저에서 도입하게 되었다. 이 두 함수는 개발자가 계산을 더 명확하게 설명하게 하는 것 뿐만 아니라 배열 탐색을 위한 반복문 작성을 단순화 시키는 역할을 한다. 이는 특히, 반복 코드가 실제로 새로운 배열에 배열을 맵핑(mapping) 하는 경우나 수집(accumulation), 검사(checksum) 그리고 다른 유사한 리듀스(축소, reducing) 연산의 반복문 작성에서 더욱 그러하다.
좌측: 일반적인 반복문 사용; 우측: map, reduce 사용
Map은 실제로 결과물을 위해 구조적인 변경 없이 원래의 배열로 계산하는 것을 의미한다. 예를들면, map이 배열을 전달 받았을 때, 결과물은 또 다른 배열이 되는 것을 확인 할 수 있는데, 기존 배열과의 유일한 차이는 내부요소가 원래의 값이나 타입에서 다른 값이나 타입으로 변형될 것이라는 점이다. 그래서 우리는 위의 예시로 부터 doMap 함수를 다음과 같은 타입 시그니처(type signature)로 표현할 수 있다.
위의 시그니처는 [Number]
가 숫자 배열을 의미한다는 것을 나타낸다. 그래서 우리는 시그니처를 아래와 같이 읽을 수 있다.
doMap
은 숫자 배열을 불린형 배열로 바꿔주는 함수다.
반면에, 리듀스(reducing) 연산은 새로운 하나를 위해 입력 데이터 형식의 구조를 변경할 수 있다는 것을 의미한다. 예를 들면, doReduce
의 시그니처는 다음과 같다.
여기서는 [Number]
배열이 사라졌다. 이를 통해 우리는 map
과 reduce
사이의 주요한 차이를 볼 수 있다.
사실, map
과 reduce
의 개념은 자바스크립트 보다 더 오래됐고, 리스프(Lisp)나 하스켈(Haskell)과 같은 다른 함수형 개발 언어에서 널리 사용되고 있다. 이에 대한 논평은 더글라스 크락포드가 작성한 유명한 자바스크립트 아티클인 ‘JavaScript: The World’s Most Misunderstood Programming Language’에 기록되어있다.
중괄호와 투박한 for 문이 포함된 자바스크립트의 C와 같은 문법은, 자바스크립트가 일반적인 프로시저형 언어로 보이게 한다. 이는 잘못된 오해인데, 자바스크립트가 C나 자바(Java)보다는 리스프나 스킴(Scheme)과 같은 함수형 언어와 더 유사점이 많이 때문이다.
이것은 자바스크립트가 함수형 언어와 같은(다른 직교 OOP언어가 할 수 없거나 하지 않는) 일을 할 수 있는 이유중에 하나다. 예를 들면, 자바8 이전의 자바에서 자바스크립트에서 일반적인 ‘콜백(callback)’과 같은 것을 원했다면, 불필요한 ‘익명 클래스’를 생성해야만 했다.
Button button = (Button) findViewById(R.id.button);
button.setOnClickListener(
new OnClickListener() {
public void onClick(View v) {
// do something
}
}
);
물론 자바스크립트에서 익명 콜백을 사용하는 것에 대해서는 항상 논란이 많다. 특히 구성요소가 계속해서 성장 할 때에는 콜백 지옥을 경험 하기도 한다. 그러나 일급 함수(first-class function)는 콜백의 넘어서는 많은 것을 할 수 있다. 하스켈에서는 함수만으로 퀘이크(Quake) 같은 게임과 유사한 전체 GUI 프로그램을 구성 할 수 있다. 즉, 심지어 일반적으로 프로그램이 구성될 때 필요할 것으로 예상되는 클래스, 메소드, 인터페이스, 탬플릿 및 기타 요소들 없이도 만들 수 있다.
하스켈로 개발한 퀘이크와 유사한 게임의 일부분
따라서, 자바스크립트 세계에서는 ‘클래스'와 ‘클래스 시스템'을 황급히 구현하는 것이 아닌(개발자가 문제에서 시작할 때 종종 행하는), 프로그램 구성을 하기 위한 유사한 패턴을 따라하는 것이 가능하다. 자바스크립트에서 함수형 스타일로 추가하는 것은 전체적으로(특히 네이티브 API에서 지원하는 map, reduce와 같은 기능을 사용할 때) 그렇게 나쁘지 않다. 이 방법을 받아들이는 것은 기능을 재정의 하는 대신 그들을 결합하여 보다 간결한 코드를 작성할 수 있다는 것을 의미한다. 유일한 한계는 언어 자체가 아직 충분히 함수형스럽지 않다는 것이다. 그러기에 우리가 올바른 라이브러리로 해결할 수 있을 지라도 너무 많은 트릭을 사용하게 된다면 아마도 문제가 생길 것이다.
map
과 reduce
는 인자나 그에 대한 결과물로 다른 함수를 전달 받는다. 이러한 방법들은 함수형 세계의 연산을 구성하는 기본 개념을 제시하고, 유연성 및 확장성과 함께 작은 조각을 붙일 수 있기 때문에 중요하다. 예를들어, 위에서 언급한 map
의 표현 시그니처를 살펴보자.
두번째 인수가 “Number -> Boolean’유형의 함수를 나타내는 것을 알수 있다. 실은, ‘a -> b’ 유형이라면 어떤 함수든 다 가능하다. 이것은 아마도 자바스크립트 세계에서는 그리 특이한 일은 아닐 것이다(우리는 일상 개발업무에서 다수의 콜백을 사용한다). 그러나 요점은 고차 함수 역시 함수라는 것이다. 일급함수와 id, reduce, curry, uncurry, arrow, bind와 같은 몇몇 강력한 고차함수만을 가진 완전한 프로그램을 생성 할 때 까지 그들은 더 큰 것들로 구성될 수 있다.
언어의 한계가 발생할 수 있기에, 자바스크립트 코드를 완전한 함수형 스타일로 작성할 수는 없다. 그러나 유형의 발상과 더 많은 것을 하기 위한 구성은 차용 할 수 있다. 예를 들면, 유형에 대해 생각할때, map
은 단지 데이터 처리를 위해서만 사용되는게 아니라는 것을 발견 할 것이다.
이 map
과 reduce
타입 시그니처는 하스켈인 것 처럼 보인다. 어떤 것으로든 a
와 b
를 대신할 수 있다. 그래서 a
가 SQL
이 되고 b
가 IO x
가 된다면 어떨까? 유형으로 생각하고 있으면, IO x
는 정수(Int)나 URL
과 같은 일반 적인 형식에 불과하다는 것을 기억해야한다.
-- Let's construct queries from SQL statements.
makeQueries strs = map str prepare conn str
doQuery qrys = foldl (results query results >> query) (return ()) qrys
-- Do query and get results.
let stmts = [ "INSERT INTO Articles ('Functional JavaScript')"
, "INSERT INTO Gecko VALUES ('30.a1')"
, "DELETE FROM Articles WHERE version='deprecated'"
]
main = execute (doQuery (makeQuery stmts))`
(이 단순한 하스켈 예제는 오직 데모를 위해서 작성었다. 실제로 실행되지 않을 수 있다.)
예제에서는 map
을 사용하여 IO()
로 SQL
를 바꾸는 makeQueries
함수를 생성했는데, 이는 또한 몇몇 실행 가능한 행동들을 생성했음을 의미한다.
그리고 doQuery
함수는 실제 리듀스 연산이며, 쿼리를 실행할 것이다.
리듀스 연산은 특정 모나드(Monad) bind
함수(>>
)의 도움으로 IO 행위가 수행됨을 알아야 한다. 이 주제는 이번 아티클에서 다루지는 않지만, 독자는 프라미스(Promise)를 하는 것 처럼 한단계씩 실행하며 함수를 결합하는 방법을 상상해야 한다.
이 기술은 하스켈에서 뿐만이 아니라 자바스크립트에서도 유용하다. 유사한 계산을 구성하기 위해 프라미스, ES6 에로우(arrow)함수와 함께 이 개념을 사용할 수 있다.
// Use a Promise-based library to do IO.
var http = require("q-io/http")
,noop = new Promise(()=>{})
,prepare =
(str)=> http.read('http://www.example.com/' + str)
.then((res)=> res.body.toString())
// the 'then' is equal to the '>>'
,makeQuery =
(strs)=> strs.map((str)=> prepare(str))
,doQuery =
(qrys)=> qrys.reduce((results, qry)=> results.then(qry), noop)
,stmts = [ "articles/FunctionalJavaScript"
, "blob/c745ef73-ece9-46da-8f66-ebes574789b1"
, "books/language/Haskell"
]
,main = doQuery(makeQuery(stmts));
(Node.js에서, map/reduce와 프라미스로 개발한 유사 쿼리 코드는 늦은(Lazy) 프라미스와 늦은 평가 때문에 하스켈 버전 처럼 실행되지는 않을 것이다.)
함수로 계산을 정의하고 나중에 수행하기 위해 그들을 결합하는 것에서, 우리가 원하는 것에 상당부분 접근했다고 볼 수 있다(자바스크립트에서는 늦은 평가를 하지 않기 때문에, 실제로 나중에 수행되지는 않는다). 이것은 프라미스 미완료를 유지하는 트릭을 사용한다면 수행되어질 수 있다(resolve
함수는 오직 우리가 원할 때에만 수행할 수 있다). 그러나 트릭을 사용한다 해도 몇몇 해결되지 않는 이슈는 여전히 남아있다.
주목해야 할 또 다른 것은 우리 프로그램은 가변적인 변수들을 필요로 하지 않지만, 몇가지 계산 결과는 변경되었고 프로그램의 모든 단계에 전달 된다는 것이다. 사실상, 이것은 함수형 언어가 순수하게 머물 수 있는 유일한 이유다. 그에 따라 함수형 언어들은 최적화와 얘기치 않은 부작용(side-effects)의 제거에서 득을 볼 수 있다.
Map / reduce는 자바스크립트에서 가장 일반적인 함수형 기능이다. 프라미스 같은 다른 함수형이 아닌 기능들과 함께, 모나드-스타일(Monad-style) 계산 제어와 같은 트릭을 사용할 수 있고 ES6의 에로우 함수로 커리함수를 쉽게 정의할 수 있으며 기타 등등의 것들을 할 수 있다. 또한, 좋은 함수형 기능을 제공하는 몇몇 훌륭한 라이브러리도 있다. 그리고 몇몇 특정 도메인 언어(DSLs, Domain Specific Languages)는 심지어 함수형 스피릿을 갖고 있다. 물론, 함수형 개발을 이해하는 제일 좋은 방법은 하스켈, ML, 오카멜(OCamel)과 같은 함수형 개발을 위해 디자인된 언어를 배우는 것이다. 스칼라(Scala), F#, 얼랭(Erlang) 또한 좋은 선택이다.