⚡️함수형 프로그래밍 입문

📈 Trend

React Hook

Reactive Programming

Java8+, C++ 11, NET 3.5

💡 Concept

  • 선언적 프로그래밍 
  • 순수함수 
  • 참조 투명성
  • 불변성

🚩Goal

부수효과를 방지하고 상태변이를 감소하기 위해 데이터 제어 흐름과 연산을 함수를 통해 추상화

🤖명령형 코드의 전환

// interactive(명령형)
// 배열에서 length 만큼 홀수만걸러서 제곱한 후 모두 더해서 출력
function test(list, length) {
    let i = 0;
    let acc = 0;

    for(const a of list) {
        if(a % 2) {
            acc = acc + a * a;
            if(++i === length) break;
        }
    }
    console.log(acc);
}

분리

합성

선언적 코드

// declaretive(선언적)
go(
   list,
   filter(v => v % 2),
   map(v => v * v),
   take(10),
   reduce(add),
   console.log
)

선언적 코드

  • 재사용 가능한 작은 함수로 코드 유연성 증가
  • 작은 단위 함수는 테스트하기 용이
  • 간결한 코드로 생산성가독성 향상

💸 Benefit

// declaretive(선언적)
go(
   list,
   filter(v => v % 2),
   map(v => v * v),
   take(10),
   reduce(add),
   console.log
)

JS에서의 순수 함수

  • No Side-effect
  • Same Input, Same Output
  • No this
// Bad
let counter = 0;
function increment() {
   return ++counter;
}

// Good
const increment = counter => counter + 1;

ex) Math.sin, pow....

고차 함수

  • 함수를 인자로 받는 함수 
  • 함수를 반환하는 함수

함수를 인자로 사용

런타임에 행위를 전달받아서

미리 정해진 제어흐름에 수행

(제어흐름 추상화)

const arr = [1,2,3,4,5];
// normal loop
for( let i = 0 ; i < 10 ; i ++ ) {
  console.log(arr[i]);
}

// abstract loop
const forEach = (f, iter) => {
  for(const a of iter) {
    f(a);
  }
}

forEach(arr, console.log);

함수를 반환하는 함수

Closure를 활용한 State 저장

const showToggle = (element) => {
    let show = false;
    return () => {
        show = !show;
        element.style.display = `${show ? 'block' : 'none'}`
    }
};

const h1 = document.querySelector('#heading');

const h1Toggle = showToggle(h1);

button.onclick = h1Toggle;

// or
// showToggle(h1)();

Array 고차함수

filter

map

reduce

every

some

forEach

filter

배열에 부분집합을 반환

map

배열에 요소를 사상(mapping)

reduce

리스트에 결과를 축약(결과)

const filter = (f, list) => {
    const arr = [];
    for(let i = 0 ; i < arr.length ; i ++) {
        if(f(arr[i]) arr.push(arr[i]);
    }
    return arr;
}

함수의 다형성 높이기

const filter = (f, list) => {
    const arr = [];
    for(let i = 0 ; i < arr.length ; i ++) {
        if(f(arr[i]) arr.push(arr[i]);
    }
    return arr;
}

함수의 다형성 높이기

const filter = (f, iter) => {
    const arr = [];
    for(const a of iter) {
        if(f(a)) arr.push(a);
    }
    return arr;
};

Lazy Evaluation

// declaretive(선언적)
go(
   range(100000000),
   filter(v => v % 2),
   map(v => v * v),
   reduce(add),
   take(10),
   console.log
)

100000000 loop 배열 생성

filter -> map -> take -> reduce

Lazy Evaluation

"필요한 시점에 호출해서 사용"

const filter = (f, iter) => {
    const arr = [];
    for(const a of iter) {
        if(f(a)) arr.push(a);
    }
    return arr;
};
L.filter = function* (f, iter) => {
    for(const a of iter) {
        if(f(a)) {
            yield a
        }
    }
};

Lazy Evaluation

// declaretive(선언적)
go(
   L.range(100000000),
   L.filter(v => v % 2),
   L.map(v => v * v),
   L.reduce(add),
   take(10),
   console.log
)

전체적으로 10번 호출 -> 성능 향상

호출순서

go

// 함수를 순차적으로 적용해서 축약 
const go = (...args) => reduce((a, f) => f(a), args);
go(
    10,
    a => a + 10,
    a => a + 20,
    console.log
);
// h(g(f(10)))
/** 
go(
  10,
  f,
  g,
  h
)
*/

const list = [{ count: 1 }, { count: 5 }, { count: 10 }, { count: 2 }];

go(
    list,
    list => map(v => v.count, list),
    list => filter(v => v < 5, list),
    list => reduce(add, list),
    console.log
);

pipe

// 함수를 파이프라인으로 엮은 새로운 함수를 리턴
// 평가시점을 뒤로 미룸
const pipe = (...fs) => (v) => go(v, ...fs);



const list = [{ count: 1 }, { count: 5 }, { count: 10 }, { count: 2 }];

const sum = 
    //함수를 미리 조합해놓고 
    pipe(
        list => map(v => v.count, list),
        list => filter(v => v < 5, list),
        list => reduce(add, list)
    );


//호출하는 시점에 list를 넘겨서 평가
sum(list);

Currying

f(x, y, z)

g(x)(y)(z)

함수를 재사용, 조합할때 사용

Curry 활용

// 함수를 파이프라인으로 엮은 새로운 함수를 리턴
// 평가시점을 뒤로 미룸
const pipe = (...fs) => (v) => go(v, ...fs);



const list = [{ count: 1 }, { count: 5 }, { count: 10 }, { count: 2 }];

const filter = (f) => function*(iter) { for(const a of iter) if(f(a)) yield a }

const sum = 
     pipe(
        map(v => v.count),
        filter(v => v < 5),
        reduce(add)
    );

sum(list);

Curry 함수

// 함수를 파이프라인으로 엮은 새로운 함수를 리턴
// 평가시점을 뒤로 미룸
const pipe = (...fs) => (v) => go(v, ...fs);

const list = [{ count: 1 }, { count: 5 }, { count: 10 }, { count: 2 }];

const filter = curry(function*(f, iter) { for(const a of iter) if(f(a)) yield a });
// filter(f)(iter)
// filter(f, iter)
// const testfilter = filter(f)
// testfilter(iter);

const map = curry(function*(f, iter) { for(const a of iter) yield f(a) });

const sum = 
     pipe(
        map(v => v.count),
        filter(v => v < 5),
        reduce(add)
    );
const sumCount = sum(v => v.count);

sumCount (list);

FP Library

Lodash.js

Ramda.js

Ramda.js Example

Validator

Validator UseCase

const userSchema = {
  name: [isString, min(3), max(30), isRequired],
  email: [isString, pattern(emailPattern), min(10), max(30), isRequired],
  age: [isNumber, isRequired]
};

const form = {
  name: "aaaa",
  email: "test",
  age: 33
};

// false
const result = validate(userSchema, form);

Validate Functions

const R = require("ramda");

const min = R.curry((a, b) => a < b.length);
const max = R.curry((a, b) => a > b.length);

const isTypeOf = type => val => typeof val === type;

const emailPattern = /^([a-zA-Z0-9])+([\.\+a-zA-Z0-9_-])*@([\+a-zA-Z0-9_-])+(\.[a-zA-Z0-9_-]+)+$/;
const pattern = R.curry((pattern, val) => pattern.test(val));

const isString = isTypeOf("string");
const isNumber = isTypeOf("number");
const isRequired = val => !!val;

Validator

const every = R.reduce((a, b) => a && b, true);

const validateFns = val =>
  R.pipe(
    R.map(testFn => testFn(val)),
    every
  );

const findInvalidColumn = form =>
  R.pipe(
    Object.entries,
    R.map(([k, v]) => [k, validateFns(form[k])(v)]),
    R.filter(([_, isValid]) => !isValid),
    R.take(1),
    R.flatten,
    ([k]) => k
  );

const validate = R.curry((schema, form) =>
  R.pipe(
    findInvalidColumn(form),
    R.isNil
  )(schema)
);

Node.js Middleware

Validator 활용

// validate middleware factory 
const validateSchema = schema => (req, res, next) => {
  if(validate(schema, req.body)) {
    next();
  }
  res.status(422).send({ message: 'validate error'});
} 

router.post('/register', validateSchema(userSchema), userController.register);

Tip

  • Break PointDebugging이 힘드므로 log함수를 만들어 사용
const debug = val => {
   console.debug(val);
   return val;
};

R.pipe(
    filter...,
    log,
    map...
    ...
)
  • 고차함수는 ES7Decorator 로도 활용 가능

후기..

  • 함수형은 Typescript와는 .... 
  • 익숙하지 않다면 함수형 스타일을 부분적용