Functional Patterns in JavaScript

<- Радослав Георгиев

Ще си говорим за:

  • Функции!

  • Къри

  • Композиране

  • Кутии

Защо JavaScript?

  1. Повечето хора го знаем и не може да избягаме от него.

  2. Има достатъчно weird ОOP, за да не ни пречи.

  3. Има всичко нужно, за да вдигнем каквото ни трябва.

function p

function p(x) { console.log(x); }

Refresher: 1st class functions

"Може да пазим функция в променлива и имаме тип функция"

function inc(x) { return x + 1; }
p([1, 2, 3].map(inc));
// [2, 3, 4]

Refresher: Math function

"A function relates an input to an output."

Images from http://www.mathsisfun.com/sets/function.html

Pure Functions

"A pure function is a function that, given the same input, will always return the same output and does not have any observable side effect."

function factorial(n) {
    return product(1, n);
}
function product(ns) {
    var result = 1, start = 0, end = ns.length;
    while(start < end) { product *= ns[start]; start += 1; }
}

Impure Functions

var arr = [1, 2, 3, 4]

p(arr.splice(0, 2)); // [1, 2]
p(arr.splice(0, 2)); // [3, 4]
var label = "Hello {name}",
    element = "#greeter";

function greet(user) {
 $(element).html(label.format({ name: user }));
}

Страничен ефект =>

нечиста функция

  • Мутиране на стойност на променлива

  • Правене на HTTP заявка

  • Отпечатване в конзолата

  • Всякакъв вид Input / Output

Свикнали сме да пишем нечисти функции

Какво ни дават чистите функции?

Memoization

function memoize(f) {
  var fTable = {};

  return function() {
    var input = JSON.stringify(arguments);
    
    console.log(fTable);

    if(!fTable[input]) {
      fTable[input] = f.apply(f, arguments);
    }

    return fTable[input];
  }
}

var add = memoize(function(x, y) { return x + y; })

console.log(add(1, 2));
console.log(add(1, 2));

Следене на зависимостите & portability

//impure
var signUp = function(attrs) {
  var user = saveUser(attrs);
  welcomeUser(user);
};

//pure
var signUp = function(Db, Email, attrs) {
  return function() {
    var user = saveUser(Db, attrs);
    welcomeUser(Email, user);
  };
};

Testability

Без нужда да създаваме и разваляме "света" преди и след всеки тест, за да работи нашата функция.

Сменяйки само стила на писане от impure към pure functions, може да постигнем много.

Function currying.

Named after Haskell Curry

Function currying.

"Викаме функция с по-малко от нужните и аргументи. Получаваме функция, която чака останалите."

function add(a) {
    return function(b) {
        return a + b;
    }
}
p(add(1)) // function
p(add(1)(2)) // 3
var inc = add(1)
p(inc(2)); // 3
function map(f) {
  return function(xs) {
    return xs.map(f);
  }
}

var addOne = map(add(1));
p(addOne([1, 2, 3])); // [2, 3, 4]

Позволява ни да пишем "декларативно"

var inc = add(1);
var inc = function(x) { add(1, x); }

Данните се слагат като последен аргумент.

Мощен инструмент, ама така е грозно.

function reduce(op) {
    return function(initial) {
        return function(xs) {
            return xs.reduce(op, initial, xs);
        }
    }
}

npm install ramda

var curry = require("ramda").curry;

var add = curry(function(a, b) {
  return a + b;
});

var inc = add(1);

var match = curry(function(what, x) {
  return x.match(what);
});

var replace = curry(function(what, replacement, x) {
  return x.replace(what, replacement);
});
var curry = require("ramda").curry;

var filter = curry(function(f, xs) {
  return xs.filter(f);
});

var map = curry(function(f, xs) {
  return xs.map(f);
});

var reduce = curry(function(op, initial, xs) {
  return xs.reduce(op, initial);
});

var split = curry(function(delimiter, xs) {
  return xs.split(delimiter);
});

var join = curry(function(delimiter, xs) {
  return xs.join(delimiter);
});
var words = split(" ");
p(words("Varna Conf 2015")) // ["Varna", "Conf", "2015"]
var unwords = join(" ");
p(join(["Varna", "Conf", "2015"])) // "Varna Conf 2015" 
var sum = reduce(add, 0);
var product = reduce(mult, 1);
var slice = curry(function(start, end, xs) {
    return xs.slice(start, end);
});

var take = slice(0);

Function composition

f . g
f.gf . g

Чете се "f след g"

f . g = h
f.g=hf . g = h
h(x) = f(g(x))
h(x)=f(g(x))h(x) = f(g(x))

Function Composition

function compose(f, g) {
    return function(x) {
        return f(g(x));
    }
}
var exclaim = function(x) { return x + '!'; };
var shout = compose(exclaim, toUpperCase);

p(shout("varna baby")); // VARNA BABY!

Имаме следното правило:

f . (g . h) == (f . g) . h
f.(g.h)==(f.g).hf . (g . h) == (f . g) . h

=> Композирането на функции е асоциативно.

=> Може да композиране n на брой функции, без да пишем скоби.

Чрез currying и composition може да създаваме нови функции.

По този начин кодът ни е по-изразителен и "по-декларативен"

"Pointfree" стил на писане.

"Pointfree functions are functions that never mention the data upon which they operate."

"Pointfree" стил на писане.

//not pointfree because we mention the data: word
var snakeCase = function (word) {
  return word.toLowerCase().replace(/\s+/ig, '_');
};

//pointfree
var snakeCase = compose(replace(/\s+/ig, '_'), toLowerCase);

"Pointfree" стил на писане.

//not pointfree because we mention the data: name
var initials = function (name) {
  return name.split(' ').map(compose(toUpperCase, head)).join('. ');
};

//pointfree
var initials = compose(join('. '), 
                       map(compose(toUpperCase, head)), 
                       split(' '));

initials("hunter stockton thompson");
// 'H. S. T'

Може да превърнем нашата програма в серия от композиции.

var app = compose(f1, f2, ..., fn);

app(initialData);

Защо правим null / undefined checking?

var head = function(xs) { return xs[0]; }
var last = compose(head, reverse);

p(head([])); // undefined
p(last([])); // undefined

Винаги проверяваме с if, за да не гръмне.

if(!nullOrUndefined(arg1)) {
    // do something
} else {
    // stop the world!
}

if(!nullOrUndefined(arg2)) {
    // do something
} else {
    // stop the world!
}

И сега ще се запознаем с концепцията за кутия.

Или имаме стойност, или имаме нищо.

  • Нашият контейнер ще пази каквато и да е стойност.

  • Ще има метод map(f), чрез който ще получаваме нов контейнер с променената стойност.

Прост контейнер:

function Container(x) {
  this.__value = x;
}

Container.of = function(x) {
  return new Container(x);
}

Container.prototype.map = function(f) {
  return Container.of(f(this.__value));
}

Container.of поставя няшата стойност в "минимален контекст"

var a = Container.of("VarnaConf");
p(a) // Container("VarnaConf")

var b = a.map(length);

p(b) // Container(9)

Този вид контейнер се нарича "идентитет"

Сега ще направим контейнер, който да се грижи за Null Checking. Нарича се Maybe.

Maybe = Или стойност или нищо.

var a = Maybe.of(5);
p(a.map(inc)); // Maybe(6)

var b = Maybe.of(null);

p(b.map(inc)); // Maybe(null)
function Maybe(x) {
  this.__value = x;
}

Maybe.of = function(x) { return new Maybe(x); }

Maybe.prototype.isNothing = function() {
  return nullOrUndefined(this.__value);
}

Maybe.prototype.map = function(f) {
  if(this.isNothing()) {
    return this;
  }

  return Maybe.of(f(this.__value));
}

Този вид контейнери се наричат функтори.

Функтор е нещо, в/у което може да се map-не функция.

Нека да дефинираме map така:

var map = curry(function(f, functor) {
    return functor.map(f);
});

Функотрите могат да правят следните неща:

var n = Maybe.of(10);
var result = compose(map(inc), map(inc));
var result2 = map(compose(inc, inc));

console.log(result(n)); // 12
console.log(result2(n)); // 12

"Правила" на функторите:

// identity
map(id) === id;

// composition
compose(map(f), map(g)) === map(compose(f, g));

Материали

Functional Patterns in JavaScript

By Hack Bulgaria

Functional Patterns in JavaScript

  • 4,179