Introduction
Mathematical, Pure Functions
Functional Programming
- Programming with functions
Set theoretically
- Every function is a single-valued collection of pairs
- One input, one output
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| const toLowerCase = { A: "a", B: "b", C: "c", D: "d", E: "e", F: "f", };
toLowerCase["C"];
const isPrime = { 1: false, 2: true, 3: true, 4: false, 5: true, 6: false };
isPrime[3];
|
Functions
- Total
- Deterministic
- No Observable Side-Effects
Total
- For every input there is a corresponding output
1 2 3 4 5 6 7 8 9 10 11
| const inc = i => { if(i === 0) return 1 if(i === 1) return 2 if(i === 2) return 3 }
const inc = i => { return i + 1 }
|
- Deterministic
- Always receive the same output for a given input(幂等)
1 2 3 4 5 6 7 8 9 10 11 12 13
| const timeSince = comment => { const now = new Date() const then = new Date(comment.createdAt) return getDifference(now, then) }
const getDifference = (now, then) => { const days = Math.abs(now.getDate() - then.getDate()) const hours = Math.abs(now.getHours() - then.getHours()) return {day, hours} }
|
- No Side Effects
- No observable effects besides computing a value
1 2 3 4 5 6 7 8 9
| const add = (x, y) => { console.log(`Adding ${x} ${y}`) return x + y }
const add = (x, y) => { return { result: x + y, log: `Adding ${x} ${y}` } }
|
Pure Functions Checklist
1 2 3 4 5 6 7 8 9 10 11 12
| var xs = [1, 2, 3, 4. 5]
xs.splice(0, 3) xs.splice(0, 3) xs.splice(0, 3)
xs.slice(0, 3) xs.slice(0, 3) xs.slice(0, 3)
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| const toSlug = (title) => { const urlFriendly = title.replace(/W+/ig, '-') if(urlFriendly.length < 1) { throw new Error('is bad') } return urlFriendly }
const toSlug = (title) => { return new Promise((res, rej) => { const urlFriendly = title.replace(/\W+/ig, '-')
if(urlFriendly.length < 1) { rej(new Error('is bad')) }
return res(urlFriendly) }) }
|
1 2 3 4 5 6 7 8 9 10 11 12 13
| const signUp = (attrs) => { const user = saveUser(attrs) welcomeUser(user) }
const signUp = (attrs) => { return () => { const user = saveUser(attrs) welcomeUser(user) } }
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| const birthday = user => { user.age += 1 return user }
const shout = word => word.toUpperCase().concat('!')
const headerText = header_selector => querySelector(header_selector).text()
const parseQuery = () => location.search.substring(1).split('&').map(x => x.split('='))
|
Pure Functions Advantages
- Why?
- Reliable
- Portable
- Reuseable
- Testable
- Composable
- Properties/Contract
Currying
Properties, Arguments & Currying
1 2 3 4 5 6 7 8 9 10 11
| add(add(x, y), z) == add(x, add(y, z))
add(x, y) == add(y, x)
add(x, 0) == x
add(multiply(x, y), multiply(x, z)) == multiply(x, add(y, z))
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| const url = (t) => `http://gdata.youtube.com/feeds/api/videos?q=${t}&alt=json`;
const src = _.compose( _.prop("url"), _.head, _.prop("media$thumbnail"), _.prop("media$group") );
const srcs = _.compose(_.map(src), _.prop("entry"), _.prop("feed"));
const images = _.compose(_.map(imageTag), srcs);
const widget = _.compose(_.map(images), getJSON, url)
widget('cats').fork(log, setHtml(document.querySelector('#youtube')))
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| const add = (x, y) => x + y;
const toPair = (f) => ([x, y]) => f(x, y);
const fromPair = (f) => (x, y) => f([x, y]);
const result = fromPair(toPair(add))(1, 2);
const flip = (y, x) => f(x, y);
const curry = (f) => (x) => (y) => f(x, y);
const curriedAdd = curry(add);
const increment = curriedAdd(1);
const three = increment(2)
const uncurry = (f) => (x, y) => f(x)(y);
const modulo = curry((x, y) => y % x);
const isOdd = modulo(2);
|
Currying Example & Argument Order
1 2 3 4 5 6 7 8 9
| const filter = (f, xs) => xs.filter(f)
const getOdds = filter(isOdd) const result = getOdds([1, 2, 3, 4])
const filter = (xs, f) => xs.filter(f) const getOdds = (xs) => xs.filter(f) const result = getOdds([1, 2, 3, 4])
|
Ramda Generalized Currying
1 2 3
| const replace = (regex, replacement, str) => str.replace(regex, replacement);
const replaceVowels = replace(/[AEIOU]/gi, "!");
|
Partial Application vs Currying
partial application一次传入部分参数(可以是多个), curry一次传入一个参数。
Currying Exercise
Composition
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| const toUpper = (str) => str.toUpperCase();
const exclaim = (str) => str + "!";
const first = (xs) => xs[0];
const compose = (f, g) => (x) => f(g(x));
const shout = compose(exclaim, toUpper);
console.log(shout("tear"));
console.log(compose(first, compose(exclaim, toUpper))("tear")); console.log(compose(compose(first, exclaim), toUpper)("tear"));
const loudFirst = compose(toUpper, first); const shoutFirst = compose(exclaim, loudFirst); console.log(shoutFirst("tears"));
|
Creating Programs with Curry & Compose
使用curry和compose可以使多元或二元函数转换为一元函数(一次接收一个参数)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31
| const _ = require("ramda");
const doStuff = _.compose( _.join(""), _.filter((x) => x.length > 3), _.reverse, _.map(_.trim), _.split(" "), _.toLower );
const doStuff = (str = "") => { const lower = str.toLowerCase(); const words = lower.split(" ");
words.reverse();
for (let i in words) { words[i] = words[i].trim(); }
const keepers = [];
for (let i in words) { if (words[i].length > 3) { keepers.push(words[i]); } }
return keepers.join(""); };
|
Composition is Dot Chaining
1 2 3 4 5 6 7 8 9
| const doStuff = (str = "") => str .toLowerCase() .split(" ") .map((c) => c.trim()) .reverse() .filter((x) => x.length > 3) .join("");
|
Logging in Composition
1 2 3 4 5
| const log = curry((tag, x) => (console.log(tag, x), x)); const loudFirst = compose(toUpper, first); const shoutFirst = compose(exclaim, loudFirst); const logShoutFirst = compose(shoutFirst, log("here")); console.log(logShoutFirst("tears"));
|
Compose Practice
Functors
Creating the Identity Functor
1 2 3 4 5 6 7 8
| const nextCharForNumberString = (str = "") => { const trimmed = str.trim(); const number = parseInt(trimmed); const nextNumber = number + 1; return String.fromCharCode(nextNumber); };
console.log(nextCharForNumberString(" 64 "));
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| const Box = (x) => ({ map: (f) => Box(f(x)), toString: `Box(${x})`, fold: (f) => f(x), });
const nextCharForNumberString = (str = "") => { return Box(str) .map((x) => x.trim()) .map((x) => parseInt(x)) .map((x) => x + 1) .fold(String.fromCharCode); };
console.log(nextCharForNumberString(" 64 "));
|
Refactoring to Dot Chaining
1 2 3 4 5 6 7 8 9 10
| const first = (xs) => xs[0];
const halfTheFirstLargeNumber = (xs) => { const found = xs.filter((x) => x >= 20); const answer = first(found) / 2; return `The answer is ${answer}`; };
const res = halfTheFirstLargeNumber([1, 4, 50]); console.log(res);
|
1 2 3 4 5 6 7 8
| const halfTheFirstLargeNumber = (xs) => Box(xs) .map((xs) => xs.filter((x) => x >= 20)) .map((found) => first(found) / 2) .fold((answer) => `The answer is ${answer}`);
const res = halfTheFirstLargeNumber([1, 4, 50]); console.log(res);
|
1
| const compose = (f, g) => (x) => Box(x).map(g).fold(f);
|
Functor Practice
1 2 3 4 5 6 7
| const Box = (x) => ({ map: (f) => Box(f(x)), toString: `Box(${x})`, fold: (f) => f(x), chain: (f) => f(x), });
|
Either Monad
Either
1 2 3 4 5 6 7 8 9
| const findColor = (name) => ({ red: "#ff4444", blue: "#3b5998", yellow: "3fff68f", }[name]);
console.log(findColor("red").toUpperCase()); console.log(findColor("redddddd").toUpperCase());
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51
|
const Left = (x) => ({ chain: (f) => Left(x), map: (f) => Left(x), fold: (f, g) => f(x), toString: `Left(${x})`, });
const Right = (x) => ({ chain: (f) => f(x), map: (f) => Right(f(x)), fold: (f, g) => g(x), toString: `Right(${x})`, });
const findColor = (name) => { const found = { red: "#ff4444", blue: "#3b5998", yellow: "3fff68f", }[name];
return found ? Right(found) : Left("not found"); };
console.log( findColor("red") .map((x) => x.toUpperCase()) .fold( () => "no color", (x) => x ) );
console.log( findColor("redd") .map((x) => x.toUpperCase()) .fold( () => "no color", (x) => x ) );
|
fromNullable
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30
| const fromNullable = (x) => (x !== null && x !== undefined ? Right(x) : Left());
const findColor = (name) => fromNullable( { red: "#ff4444", blue: "#3b5998", yellow: "3fff68f", }[name] );
console.log( findColor("red") .map((x) => x.toUpperCase()) .fold( () => "no color", (x) => x ) );
console.log( findColor("redd") .map((x) => x.toUpperCase()) .fold( () => `no color`, (x) => x ) );
|
Refactoring Using the Either Monad
1 2 3 4 5 6 7 8 9 10 11 12 13
| const fs = require("fs");
const getPort = () => { try { const str = fs.readFileSync("config.json"); const config = JSON.parse(str); return config.port; } catch (e) { return 3000; } };
console.log(getPort());
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| const tryCatch = (f) => { try { return Right(f()); } catch (e) { return Left(e); } };
const readFileSync = (path) => tryCatch(() => fs.readFileSync(path));
const getPort = () => readFileSync("./config.json") .map((content) => JSON.parse(content)) .map((config) => config.port) .fold( () => 8080, (x) => x );
console.log(getPort());
|
Flattering Either Monads with Chain
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40
|
const Left = (x) => ({ chain: (f) => Left(x), map: (f) => Left(x), fold: (f, g) => f(x), toString: `Left(${x})`, });
const Right = (x) => ({ chain: (f) => f(x), map: (f) => Right(f(x)), fold: (f, g) => g(x), toString: `Right(${x})`, });
const tryCatch = (f) => { try { return Right(f()); } catch (e) { return Left(e); } };
const readFileSync = (path) => tryCatch(() => fs.readFileSync(path.join(__dirname + path)));
const parseJSON = (content) => tryCatch(() => JSON.parse(content));
const getPort = () => readFileSync("config.json") .chain((content) => parseJSON(content)) .map((config) => config.port) .fold( () => 8080, (x) => x );
console.log(getPort());
|
Either Practice
Debugging with Logging
1 2 3 4
| const logIt = x => { console.log(x) return x }
|
Task
Task Monad
1 2 3 4 5 6 7 8 9 10 11
| Task.of(2).map(two => two + 1)
const t1 = Task((rej, res) => res(2)) .map(two => two + 1) .map(three => three + 1)
t1.fork(console.error, console.log)
|
Refactoring Node IO with Task
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| const app = () => fs.readFile('config.json', 'utf-8', (err, contents) => { console.log(err, contents) if(err) throw err
const newContents = contents.replace(/3/g, '6')
fs.writeFile('config1.json', newContents, (err, _) => { if(err) throw err console.log('success!') }) })
app()
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| const readFile = (path, enc) => Task((rej, res) => fs.readFile(path, enc, (err, contents) => err ? rej(err) : res(contents) ) )
const writeFile = (path, contents) => Task((rej, res) => fs.writeFile(path, contents, (err, contents) => err ? rej(err) : res(contents) ) )
const app = () => readFile('config.json', 'utf-8') .map(contents => contents.replace(/3/g, '6')) .chain(newContents => writeFile('config1.json', newContents))
app() .fork(console.error, () => console.log('success'))
|
Task Practice
1 2 3 4 5 6 7 8 9 10
| const httpGet = (path, params) => Task.of(`${path}: result`)
const getUser = x => httpGet('/user', { id: x}) const getTimeline = x => httpGet(`/timeline/${x}`, {}) const getAds = () => httpGet('/ads', {})
List([getUser, getTimeline, getAds]) .traverse(Task.of, f => f()) .fork(console.log, x => console.log(x.toJS()))
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| const greaterThan5 = x => x.length > 5 ? Right(x) : Left('not greater than 5')
const looksLikeEmail = x => x.match(/@/ig) ? Right(x) : Left('not an email')
const email = 'blahh@yadda.com'
const res = [greaterThan5, looksLikeEmail] .map(x => x(email)) console.log(res)
const res2 = List([greaterThan5, looksLikeEmail]) .traverse(Either.of, x => x(email)) res2.fold(console.log, x => console.log(x.toJS()))
|