## 함수형 프로그래밍 #3 ### 모나드(monad) 앞선 글에서 안전한 데이터를 다루기 위해 함수자 라는 자료구조가 필요하며 함수자만으로 사용하기 보다는 추가적인 개념이 필요하다 까지 알아보았다. 모나드는 ***컨테이너 안으로 값을 승급하고 어떠한 규칙을 정해 통제하는 자료형을 생성하는 것***으로 함수자로 값을 보호하고 합성으로 데이터가 흘러갈때 안전하고 부수효과 없이 흘러가도록 한다. 모나드에는 중요한 개념 두가지가 있는데, - **모나드** : 모나드 연산을 추상한 인터페이스를 제공 - **모나드형** : 모나드 인터페이스를 실제로 구현한 형식 두가지가 필수로 구현되어야 한다. 이때 모나드형 구현시 준수해야하는 인터페이스 몇가지가 있다. 1. **형식생성자** : 모나드형을 생성 2. **단위 함수** : 어떤 형식의 값을 모나드에 삽입 3. **바인드 함수** : 연산을 서로 체이닝 4. ** 조인 연산** : 모나드 자료구조의 계층을 눌러 펴준다. 위의 개념들이 왜 필요한지는 코드를 보면서 직접 알아보도록 하겠다. 모나드는 함수자와 비슷하나 특정한 케이스를 특정한 로직에 위임하여 처리할 수 있다. ``` javascript const half = val => val / 2; Wrapper(2).fmap(half); // -> Wrapper(1) Wrapper(3).fmap(half); // -> Wrapper(1.5) ``` 어떤 임의의 숫자를 전달받아 짝수일 경우에만 특정한 로직을 적용하려면 모나드를 사용하지 않을 경우 한 함수에 모두 담아서 넣거나 짝수가 아닐 경우의 로직을 전부 다루어야 한다. 그러나 모나드를 사용하면 ``` javascript class Empty { map(f) { return this; } fmap(_) { return new Empty(); } } const empty = () => new Empty(); const isEven = n => Number.isFinite(n) && (n % 2 === 0); const half = val => isEven(val) ? wrap(val / 2) : empty(); half(4); // -> Wrapper(2); half(3); // -> Empty ``` 위와 같이 짝수가 넘어오면 수행하고 아닐경우 무시하게끔 만든다. 이때 중요한건 짝수이든 아니든 값을 받아와서 동일한 로직을 수행하더라도 에러가 나지않고 그냥 흘러가기만 한다. 모나드를 간단하게 직접 구현해 보도록 하겠다. ``` javascript // Wrapper 모나드 class Wrapper { // 형식 생성자 constructor(value) { this._value = value; } static of(a) { // 단위함수 return new Wrapper(a); } map(f) { // 바인드 함수(함수자) return Wrapper.of(f(this._value)); } join() { // 조인 연산: 중첩된 계층을 눌러 펴준다. if(!(this._value instanceof Wrapper)) { return this; } return this._value.join(); } get() { return this._value; } toString() { // 자료구조를 나타내는 문자열 반환 return `Wrapper (${this._value})`; } } ``` 위에서 언급한대로 필수로 구현해야할 인터페이스를 모두 구현하였고 추가로 값을 조회할 `get`과 `toString`을 구현해주었다. 여기서 바인드 함수인 `map`의 예시로는 ``` javascript Wrapper.of("Hello Monads!") .map(R.toUpper) .map(identity); // -> Wrapper("HELLO MONADS!") ``` 위와 같이 사용되며 리턴되는 값을 다시 Wrapper 모나드로 감싸서 리턴을 해준다. 여기서 다시 Wrapper 모나드로 리턴될 수 있던 이유는 `of`의 역활 때문인데, ``` javascript // DB :: db에서 데이터를 조회해오는 임의 함수 const DB = key => findKeyFromSomeWhere(key); // findObject :: DB -> String -> Wrapper const findObject = R.curry((db, id) => Wrapper.of(find(db, id))); // getAddress :: Student -> Wrapper const getAddress = student => Wrapper.of(student.map(R.prop("address"))); const studentAddress = R.compose(getAddress, findObject(DB("student"))); studentAddress("student-id").join().get(); // student address ``` 위와 같이 외부에서 데이터를 조회해서 `of`를 통해 모나드를 생성해준다. 이로서 데이터는 항상 동일한 형식으로 받게 된다. 여기서 한가지 문제가 있는데 값을 리턴할때 새로운 Wrapper를 생성해주게 되므로 깊이가 깊어질 수록 Wrapper의 뎁스가 높아지게 된다. 최종적으로 값을 얻으려면 Wrapper 의 Wrapper의 Wrapper 의 .... Wrapper의 값을 get 해와야하는 어려움이 있음으로 조인연산을 반드시 구현하여야 한다. ``` javascript // 조인 연산 Wrapper.of(Wrapper.of(Wrapper.of("Get Functional"))).join(); ``` ![](https://uzilog-upload.s3.ap-northeast-2.amazonaws.com/private/ap-northeast-2%3Ab6c10628-1f45-492c-a9eb-f54020bc8014/1582168457309-image.png) 모나드는 Wrapper 모나드 외에도 유명한 몇가지 모나드들이 있다. 이러한 모나드를 활용하면 좀더 유연하게 값을 핸들링 할 수 있다. ### Maybe 모나드 Maybe 모나드는 null 체크 로직을 효과적으로 통합하기 위한 모나드이다. 이전에 말한 null check를 함수형 프로그래밍에서 우아하게 다루기 위해 사용되는 모나드로 두가지 타입이 있다. 1. **Just(value)** : 존재하는 값을 감싼 컨테이너를 나타낸다. 2. **Nothing()** : 값이 없는 컨테이너 ``` javascript class Maybe { static just(a) { return new Just(a); } static nothing() { return new Nothing(); } static fromNullable(a) { return a !== null ? Maybe.just(a) : Maybe.nothing(); } static of(a) { return just(a); } get isNothing() { return false; } get isJust() { return false; } } // Just class Just extends Maybe { constructor(value) { super(); this._value = value; } get value() { return this._value; } map(f) { return Maybe.fromNullable(f(this._value)); } getOrElse() { return this._value; } filter(f) { Maybe.fromNullable(f(this._value) ? this._value : null); } chain(f) { return f(this._value); } } // Nothing class Nothing extends Maybe { map(f) { return this; } get value() { throw new TypeError("Nothing 값은 가져올 수 없습니다."); } getOrElse(other) { return other; } filter(f) { return this._value; } chain(f) { return this; } } ``` 이전에 알아본 모나드를 사용하지 않았을때 null을 처리하는 방법과 모나드를 사용하여 null을 처리하는 방법을 비교해 보도록 하겠다. ``` javascript // 모나드 사용 전 function getComponeyInfo(emloyee) { let componey = employee.getComponey(); if (componey && componey !== null) { const addr = componey.getAddress(); if(addr && addr !== null) { const country = addr.getCountry(); return country; } return null; } throw new Error("There is no componey"); } // 모나드 사용 후 const getComponeyInfo = employee => employee .map(getComponey) .map(getAddress) .map(getCountry) .getOrElse("There is no componey"); ``` 모나드를 사용하기 전과 후를 비교하면 모나드가 수행하는 역활들의 우아함을 확연히 확인할 수 있다! 모나드 사용이 적용된 함수 단계에서 하나라도 결과값이 Nothing일 경우 이후 연산은 전부 건너뛰게 된다. 위의 Nothing에서 그냥 흘려버리기 때문이다. 여기서 중요한것은 Nothing 과 Just 둘다 필수 인터페이스를 구현하여 값이 없더라도 에러가 나지 않도록 하는 것이 중요하다. `getOrElse`에서는 실제 값이 없을 경우 리턴되는 값이 들어오게 되는데, 디폴트 값이 있다면 파라미터로 전달해주면 된다. ### Either 모나드 Either 모나드는 동시에 발생하지 않는 두 값 a, b를 논리적으로 구분한 자료구조 이다. Either 모나드는 두가지로 구현되는데, 1. **Left(a)** : 에러 메시지 또는 예외 객체를 담는다. 2. **Right(b)** : 성공한 값을 담는다. ``` javascript class Either { constructor(value) { this._value = value; } get value() { return this._value; } static left(a) { return new Left(a); } static right(b) { return new Right(b); } static fromNullable(val) { return val !== null && val !== undefined ? Either.right(val) : Either.left(val); } static of(a) { return Either.right(a); } } // Left class Left extends Either { map(_) { return this; // 아무것도 하지 않는다. } get value() { throw new TypeError("Left 값은 가져올 수 없습니다."); } getOrElse(other) { return other; } orElse(f) { return f(this._value); } chain(f) { return this; } getOrElseThrow(a) { throw new Error(a); } filter(f) { return this; } } // Right class Right extends Either { map(f) { return Either.of(f(this._value)); } getOrElse(other) { return this._value; } orElse() { return this; // 아무것도 하지 않는다. } chain(f) { return f(this._value); } getOrElseThrow(_) { return this._value; } filter(f) { return Either.fromNullable(f(this._value) ? this._value : null); } } ``` ### IO 모나드 IO 모나드는 Input Output을 쉽게 다룰 수 있는 모나드로 이전에 말한 자바스크립트에서 필수적으로 사용되어야하는 부수효과들 (HTML에 접근하여 값을 사용 등)에 사용되는 모나드이다. ``` javascript class IO { constructor(effect) { if(!_isFunction(effect)) { throw "IO 사용법: 함수는 필수"; } this.effect = effect; } static of(a) { return new IO(() => a); } static from(fn) { return new IO(fn); } map(fn) { const self = this; return new IO(() => fn(self.effect())); } chain(fn) { return fn(this.effect()); } run() { return this.effect(); } } ``` ``` javascript // IO 적용 전 const read = (document, selector) => document.querySelector(selector).innerHTML; const write = (document, selector, val) => { document.querySelector(selector).innerHTML = val; } // IO 적용 후 const read = (document, selector) => { () => document.querySelector(selector).innerHTML; } const write = (document, selector) => { return (val) => { document.querySelector(selector).innerHTML = val; return val; } } const readDom = _.partial(read, document); const writeDom = _.partial(write, document); // HTML 예시
abcd
const changeToStartCase = IO.from(readDom("#student-name")) .map(_.startCase) .map(writeDom("#student-name")); changeToStartCase(); ``` 좀 더 복잡한 예시를 들어, 학생 데이터를 조회해서 해당 학생의 ID 가 있을 경우 학생의 정보를 보여주고 아닐 경우 를 다루는 함수를 모나드를 사용하는 것과 아닌것을 비교해 보도록 하겠다. ``` javascript // 모나드 적용 전 function showStudent(id, elementId) { let studentId = id; if (id !== null) { studentId = id.replace(/^\s*|\-|\s*$/g, ''); if(studentId.length !== 9) { throw new Error("Wrong ID"); } let student = db.get(studentId); if(student) { document.querySelector(`#${elementId}`).innerHTML = `${studnet.name}`; } else { throw new Error("학생을 찾을 수 없습니다."); } } else { throw new Error("잘못된 id 입니다."); } } ``` 학생 아이디와 HTML Element Id를 받아 DB에서 학생 정보를 조회 후 있을 경우 HTML에 학생 정보를 보여주는 단순한 기능을 하는 함수이다. 여기서 필요힌 것은 매번 값 체크를 해야하며 innerHTML 등 HTML에 직접 접근하여 부수효과를 일으켜야 하므로 이 모든걸 하나의 함수에 담을 경우 코드가 복잡해질 가능성이 크다. 위와 같은 역활을 모나드로 구현하면 ``` javascript const showStudent = R.compose( map(append("#student-info")), liftIO, getOrElse("학생을 찾을 수 없습니다."), map(csv), map(R.props(["name"])), chain(findStudent), chain(checkIDLength), lift(cleanInput) ) showStudent(studentId).run(); ``` 위와 같이 깔끔하게 구현할 수 있다. 함수를 분리 시킨거랑 뭐가 다르냐라고 할 수 있지만 구조적으로도 분리가 되어 로직의 흐름을 한눈에 볼 수 있으며 중간에 값이 없어 에러가 날 걱정을 하지 않아도 된다. 언제 어디서 함수를 돌리던 항상 같은 결과값을 기대할 수 있는 함수형 코드가 되었다. ### 비동기 코드의 복잡성과 에러 가능성을 높여주는 것 중 한가지를 꼽자면 이 비동기 코드 일 것이다. 비동기 코드는 함수간의 일시적 의존 관계가 형성되고 콜백 피라미드나 동기 코드와 호환되지 않는다는 문제로 인해 코드의 복잡성을 높여준다. 이 비동기 코드로 인해 코드 리뷰가 힘들어지며 내부 동작이 어떻게 흘러가는지 판단을 어렵게 만드는 주요 요인이다. 자바스크립트와 프론트엔드에서 비동기를 다루는 경우는 대부분 서버로 데이터를 요청한 후 데이터를 받을 때까지 기다리고 데이터가 들어오면 처리를 할때 일것이다. ``` javascript const getJSON = function(url, success, error) { const req = new XMLHttpRequest(); req.responseType = "json"; req.open("GET", url); req.onload = function () { if(ref.status === 200) { const data = JSON.parse(req.responseText); success(data); } else { req.onerror(); } } req.onerror = function() { if(error) { error(new Error(req.statusText)); } }; req.send(); } ``` ``` javascript // 비동기 코드 활용 getJSON("/student", function (students) { showStudents(students); }, function (error) { console.log(error.message); } // 좀 더 복잡한 비동기 코드 활용 getJSON("/students", function(students) { students.sort(function (a, b) { if(a.id < b.id) return -1; if(a.id > b.id) return 1; return 0; }); for(let i = 0; i < students.length; i++) { const student = students[i]; if(student.address.country === "KOR") { const success = grades => showStudent(student, average(grades)); const error = error => console.error(error); getJSON(`/students/${student.id}/grades`, success, error); } } }, function(error) { console.log(error.message); }) ``` 두번째 예와 같이 비동기로 받아온 값을 체크 후 다시 비동기 요청을 보내는 걸 반복하다보면 뎁스가 깊어지게 되고 이것이 곧 **콜백 지옥**이 된다. ``` javascript // 콜백 지옥 function callback() { function callback() { function callback() { function callback() { .... } } } } ``` 이것을 해결하기 위해 자바스크립트는 `Promise`를 도입했다. Promise 는 총 Pending, Fulfilled, Rejected, Settled이라는 네가지 상태를 가지고 있다. ![](https://uzilog-upload.s3.ap-northeast-2.amazonaws.com/private/ap-northeast-2%3Ab6c10628-1f45-492c-a9eb-f54020bc8014/1582255268755-image.png) 성공을 나타내는 Fulfilled 와 실패를 나타내는 Rejected를 묶어 Settled이라 하며 각각 resolve 와 reject를 호출한다. ![](https://uzilog-upload.s3.ap-northeast-2.amazonaws.com/private/ap-northeast-2%3Ab6c10628-1f45-492c-a9eb-f54020bc8014/1582255337085-image.png) 위 코드를 프라미스로 변경하면 ``` javascript const getJSON = function (url) { return new Promise(function(resolve, reject) { const req = new XMLHttpRequest(); req.responseType = "json"; req.open("GET", url); req.onload = function() { if(req.status === 200) { const data = JSON.parse(req.responseText); resolve(data); } else { reject(new Error(req.statusText)); } }; req.onerror = function() { if(reject) { reject(new Error("XML Request Error")); } }; req.send(); }); } getJSON("/students") .then(hide("spinner")) .then(R.filter(s => s.address.country === "KOR")) .then(R.sortBy(R.props("id")) .then(R.map(student => { return getJSON(`/grades?id=${student.id}`) .then(R.compose(Math.ceil, fork(R.divide, R.sum, R.length))) .then(grade => IO.of(R.merge(student, { "grade": grade }))) }))) .catch(error => cosole.error(error)); ``` 위와 같이 성공후 then 으로 다음 코드를 실행 시킬 수 있으며 에러는 맨 마지막 catch에서 잡아서 처리하게 된다. 그러나 Promise로도 콜백 지옥 문제를 완벽히 해결할 수 는 없으며 ES7+ 에서는 `async await`을 사용하여 비동기를 처리하기도 한다. Promise에 대한 더 자세한 설명은 [여기](https://uzihoon.com/post/b0bd9910-42b6-11ed-8b2b-635e2af2f788)에서 다루고 있다. 여기까지 세편에 걸쳐 함수형 프로그래밍에 대해서 알아보았다. 위와 같은 함수형의 다양한 장점에도 불구하고 함수형이 무조건적인 정답은 아닐 것이다. 간혹 객체지향형이 더 우월한지 함수형이 더 우월한지에 대한 논쟁도 많이 일어나는데 (물론 부질없는 짓이다) 개발에 있어서 정답은 과연 비즈니스 적으로 타당한가를 논한는 것일꺼다. 개인 프로젝트나 연구 목적의 개발이 아닌 이상 대부분의 개발은 기업의 비즈니스를 위해 수행된다. 그 말은 즉 아무리 우아하다고 주장하며 완벽하게 짜놓은 코드도 비즈니스 관점에서 바라볼때 합당하지 않다면 그 코드는 쓸모없는 코드가 되고 말 것이다. 혼자 함수형으로 개발했다 하더라도 팀에서 따라 올 수 없거나 유지보수 하는 사람이 코드가 읽기 어려워 추가 시간과 비용을 들여야 한다면 함수형은 정답이 아닐 수 있다. 또한 객체지향이 가지고 있는 상속 개념등 인간이 이해하기 쉬운 구조를 가져와 함수형으로 자료구조와 데이터를 다루는 로직을 작성하는 것 또한 가능하다. 최근 리액트도 `Hook`으로 옮겨가며 흐름이 함수형 프로그래밍으로 옮겨지고 있는 것 처럼 보인다. 제일 좋은건 이런 흐름을 놓치지 않고 비즈니스에도 부합한 프로그래밍을 한다면 어디서든 환영받는 코드가 되지 않을까 싶다. **참고** 함수형 사고, 한빛미디어 2016 함수형 자바스크립트, 한빛미디어 2018