Practical
Functional
Programming
-
Why FP
-
Benefit
Won't talk about
Difficulty
- 看不懂
- Some magic inside
Isomorphism
Mathematics
of Programming
同构——编程中的数学: https://github.com/liuxinyu95/unplugged
Reading
-
Lambda calculuc
-
Combinatory logic
-
Category theory
$$(\lambda x. + x y)$$
函数的自变量是x,将x与y相加
定义一个函数
形参
函数体 (前缀表达式)
// JS, TypeScript, Scala
(x, y) => x + y
// Java
(x, y) -> x + y
// Haskell
\x y -> x + y
// Python
lambda x, y : x + y
// Clojure
(fn [x y] (+ x y))
// Rust
|x, y| x + y
Lambda
in different
programming
languages
$$(\lambda x. + x y) 2$$
将函数应用(apply)到参数2
自由变量(open binding)
实参
被绑定(bind)的变量
函数
Closure
A function "remembers" its lexical scope even when the function is executed outside that lexical scope.
A lambda whose open bindings (free variables) have been closed by (bound in) the lexical environment.
Lambda calculus
-
α-conversion
-
β-reduction
-
η-conversion
A curried function
is a function that
takes multiple arguments one at a time
// normal
const add = (a, b) => a + b;
const result = add(2, 3);
// curried
const add = a => b => a + b;
const result = add(2)(3);
const toSeconds = h => m => s => (h*60 + m)*60 + s;
// ===>
const toSeconds =
h => // return a function with single parameter m
( m => // return a function with single parameter s
( s => (h*60 + m)*60 + s )
)
const [hour, min, sec] = [7, 23, 48];
const ss = toSeconds(hour)(min)(sec);
// ===>
const toSecWithBoundHour = toSeconds(hour); // partial application
const toSecWithBoundHourMin = toSecWithBoundHour(min); // partial application
const ss = toSecWithBoundHourMin(sec);
A partial application
is a function which has been applied to some,
but not yet all of its arguments
Has some arguments fixed inside its closure scope
Encapsulation in FP
by partial application
// curried function
const runCmd = root => cmd => api => {
const cwd = path.resolve(root, api.apiPath, api.majorVersion);
const result = shell.exec(cmd, { cwd, env });
return result.code;
};
const rootPath = argv["root-path"];
const cmd = argv._.join(' ');
const run = runCmd(rootPath)(cmd); // capture two variables
apiRepo.apiList(rootPath)
.then(... process the api list ...)
.then(_.map(run)) // apply the closure to each API
.then(success => {
if (success) {
process.exit(0);
} else {
console.log('runforeach got at least one failure')
process.exit(1);
}
})
Function composition
combination of two or more functions to create a new function
Function composition
function remoteGet(url){
return httpGet(encode(preformat(trim(url))));
}
// becomes
const remoteGet = compose(
httpGet,
encode,
preformat,
trim
);
remoteGet(url1);
remoteGet(url2);
const remoteGet = pipe(
trim,
preformat,
encode,
httpGet,
);
remoteGet(url1);
remoteGet(url2);
- flowRight, compose
- flow, pipe
Compose
const splitLines =
_.pipe([
_.split('\n'), // _.split(separater, string) => string[]
_.map(_.trim), // _.trim(string) => string
_.filter(_.isEmpty), // _.filter(predicate, string[])
]);
const lines = splitLines(fileContent);
Point-free style
is a way of defining a function
without reference to its arguments
const sum = _.reduce(_.add, 0);
sum([1,2,3]); ==> 1+2+3=6
function sum(arg1) {
// ...
}
const foo = (arg1) => // ...
pointful
point-free
η-conversion
lines.map(line => trim(line));
lines.map(trim);
Point-free or pointful?
const getAdminEmail =
_.pipe(
_.filter(_.pipe(
_.property('role'),
_.eq('admin')
)),
_.map(_.property('contact.email')),
)
const mapName =
_.map(owner =>
(owner.type === 'group')
? owner.name + owner.contact.name
: 'user-' + owner.name
)
pointful
point-free
Level
advanced
// {scheme}://{host}/foo/v1 ====> foo/v1
const convert = _.flow([
_.split('/'),
(segments) => {
const idx = _.findLastIndex(containAny(['{', '}', ':']))(segments);
return _.drop(idx+1, segments);
},
_.reject(_.isEmpty),
_.join('/'),
]);
origin
const f =
(segments: string[]) =>
_.drop(
_.findLastIndex(containAny(['{', '}', ':']))(segments) + 1,
segments
);
step 1
const indexAfterLastFound = _.flow(
_.findLastIndex(containAny(['{', '}', ':'])),
_.add(1)
);
const f =
(segments: string[]) =>
_.drop(
indexAfterLastFound(segments),
segments
);
step 2
const indexAfterLastFound = _.flow(
_.findLastIndex(containAny(['{', '}', ':'])),
_.add(1)
);
const f =
(segments: string[]) =>
_.drop(
indexAfterLastFound(segments),
segments
);
step 2
const indexAfterLastFound = _.flow(
_.findLastIndex(containAny(['{', '}', ':'])),
_.add(1)
);
// chain combinator
const chain = f => g => x => f(g(x))(x);
const convert = _.flow([
_.split('/'),
chain(_.drop)(indexAfterLastFound),
_.reject(_.isEmpty),
_.join('/'),
]);
final
A combinator
is a higher-order function that
combine 2 or more functions into
a more complex function
函数式编程中的组合子: https://segmentfault.com/a/1190000016803036
Reading
Tap combinator
// (a → b) → a → a const tap = f => x => (f(x), x)
Converge combinator
// (b → c → d) → (a → b) → (a → c) → a → d const S2 = f => g => h => x => f( g(x) )( h(x) )
fork & join
- g: forker 1
- h: forker 2
- f: join
Level
advanced
Common used combinators
- Common combinators: https://gist.github.com/Avaq/1f0636ec5c8d6aed2e45
- Combinator library: https://github.com/fantasyland/fantasy-birds
Reading
Level
advanced
How to learn
functional programming?
Learn a pure FP language
For example:
- Haskell language
- 99 problems in Haskell
Pure functional
- Immutable data structure
- No side-effect
Writing code
in functional style
in your daily work
... practically
Step by Step
writing code in functional style
Be familiar with common
utility functions
&
high order functions
Level
basic
Utility functions for manipulating data set
-
concat
-
head
-
tail
-
join
Higher order functions
(combinators)
-
filter
-
forEach
-
sortBy
-
groupBy
JavaScript: lodash, lodash/fp
Java: jOOλ, Functional Java, vavr (?)
-
map
-
flatMap
-
reduce
-
...
-
zip
-
union
-
...
Level
basic
map
// (a → b) → a[] → b[] const newArr = map(transformer)(arr)
Level
basic
fold / reduce
// ((b,a) → b) → b → a[] → b
const = foldr(reducer)(init)(arr)
Level
basic
Make variable immutable
let x = expression
Level
basic
const x = expression
Why do you modify a variable?
branching
Why do you modify a variable?
let data;
if (extSysIntegrated) {
data = fetchRemote();
} else {
data = loadFromDb();
}
process(data);
ternary operator
Why do you modify a variable? > Branching
// JavaScript
let data;
if (extSysIntegrated) {
data = fetchRemote();
} else {
data = loadFromDb();
}
process(data);
// const data =
// if (extSysIntegrated) {
// fetchRemote();
// } else {
// loadFromDb();
// }
const data =
extSysIntegrated
? fetchRemote()
: loadFromDb();
process(data);
Level
basic
switch expression
Why do you modify a variable? > Branching
// Java 14
final int ndays = switch(month) {
case JAN, MAR, MAY, JUL, AUG, OCT, DEC -> 31;
case APR, JUN, SEP, NOV -> 30;
case FEB -> {
if (year % 400 ==0) yield 29;
else if (year % 100 == 0) yield 28;
else if (year % 4 ==0) yield 29;
else yield 28;
}
};
Level
basic
Use immutable
data structures
Level
advanced
const arr = [1,2];
arr.push(3);
const arr = [1,2]; const newArr = _.concat(arr, 3);
loop
Why do you modify a variable?
const list2object = (list) => {
const result = {};
list.forEach(item => {
result[item.id] = item;
});
return result;
};
Use a state variable inside the loop
to accumulate the result
forEach, map
Why do you modify a variable? > Loop
Level
basic
Remove the loop variable
// run a function 100 times
_.forEach(doSomething)(_.range(100))
// convert a list
const output = _.map((item) => {
// process the item and return the new value
})(input);
fold, reduce
Why do you modify a variable? > Loop
Level
basic
Remove the state variable;
pass context between iterations via memo parameter
// reducer: (memo, value) => newMemo
const maxMin = ([max, min], value) => {
const newMax = value > max ? value : max;
const newMin = value < min ? value : min;
return [newMax, newMin];
}
const input = [3, 5, 2, 2, 9, 8];
// reduce(reducer, init, list)
const [max, min] =
_.reduce(
maxMin,
[input[0], input[0]],
input
);
// ===> [9, 2]
function metrics2Lines(input: Metrics[]): string[] {
const result: string[] = [];
_.forEach(input, (metrics) => {
// Measurement
let oneLine = `${metrics.name}`;
// Tag set
_.forEach(metrics.tags, (value, key) => { oneLine += `,${key}=${value}`; });
// Field set
let tagStr = '';
_.forEach(metrics.fields, (value, key) => { tagStr += `,${key}=${value}`; });
oneLine += ` ` + tagStr.slice(1, tagStr.length);
// Timestamp
oneLine += ` ${metrics.timestamp ? metrics.timestamp * 1000000 : Date.now() * 1000000}`;
// New line
result.push(oneLine);
});
return result;
}
Input array, output array ➧ map
Immutable: append multiple times ➧ join at the end
function kvPairsToStr(pair) {
return _.map(pair, (value, key) => `${key}=${value}`);
}
// name,tag1=value1,tag2=value1 field1="foo",field2="bar" 1465839830100400200
// | |
// 1st space 2nd space
function metrisToLine(input) {
return _.map(
input,
(metrics: Metrics) => {
const tagSet = kvPairsToStr(metrics.tags);
const fieldSet = kvPairsToStr(metrics.fields);
const timestamp = 1000000 * (metrics.timestamp ?? Date.now());
const measurementAndTags = _.join(',', [metrics.name, tagSet]);
return _.join(' ', [measurementAndTags, fieldSet, `${timestamp}`]);
}
);
};
Create generic function and reuse by
currying and partial application
$$ (a, b, c) \rightarrow d $$
may always be replaced with
$$ a \rightarrow b \rightarrow c \rightarrow d $$
- Write function in currying
- Auto-currying
- JS: lodash/fp
- Curry combintor
public <A, B, C> Function<A, Function<B, C>> curry(final BiFunction<A, B, C> f) {
return (A a) -> (B b) -> f.apply(a, b);
}
Level
advanced
function removeUndefinedOrNull(source) {
return _.pickBy(source,
(val) => ((val !== undefined) && (val != null)));
}
function removeUndefinedOrNull(source) {
return _.pickBy(source,
(val) => !_.isNil(val);
}
// arrow function notation
const removeUndefinedOrNull = source => _.pickBy(source, (val) => !_.isNil(val));
const removeUndefinedOrNull = source => _.omitBy(source, (val) => _.isNil(val));
// beta reduction
const removeUndefinedOrNull = source => _.omitBy(source, _.isNil);
// currying and partial application
// curryRight(_.omitBy) ====> omitBy(predicate)(obj)
const removeUndefinedOrNull = _.curryRight(_.omitBy)(_.isNil);
function kvPairsToStr(pair) {
return _.map(pair, (value, key) => `${key}=${value}`);
}
function metrisToLine(input) {
return _.map(
input,
(metrics: Metrics) => {
const tagSet = kvPairsToStr(metrics.tags);
const fieldSet = kvPairsToStr(metrics.fields);
const timestamp = 1000000 * (metrics.timestamp ?? Date.now());
const measurementAndTags = _.join(',', [metrics.name, tagSet]);
return _.join(' ', [measurementAndTags, fieldSet, `${timestamp}`]);
}
);
};
lodash/fp: auto currying
_.map(fn)(arr)
const kvPairsToStr = _.map((value, key) => `${key}=${value}`));
const metrisToLine = _.map(
(metrics: Metrics) => {
const tagSet = kvPairsToStr(metrics.tags);
const fieldSet = kvPairsToStr(metrics.fields);
const timestamp = 1000000 * (metrics.timestamp ?? Date.now());
const measurementAndTags = _.join(',', [metrics.name, tagSet]);
return _.join(' ', [measurementAndTags, fieldSet, `${timestamp}`]);
}
);
partial application of _.map
Thinking functionally
Thinking functionally
Thinking functionally
Your code should
tell a story
Thinking functionally
-
通过函数来重用
-
通过包/模块scope来隔离
-
通过闭包来封装
-
通过partial application来实现builder模式
Stream processing of data
Thinking functionally
Pipeline operation
JavaScript TC39 stage 2
const name = "charles"
const greet = name => `Hello, ${name}`
const capitalize = str => str.toUpperCase()
const exclaim = str => `${str}!!!`
const loudGreeting = name
|> greet
|> capitalize
|> exclaim
console.log(loudGreeting) // HELLO, CHARLES!!!
expression >| function
Functional
Reactive Programming
Introduction to RP: https://gist.github.com/staltz/868e7e9bc2a7b8c1f754
Reading
Thanks!
Happy Coding
Practical Functional Programming
By Leo Liang
Practical Functional Programming
- 144