Thinking in Ramda 2

Thinking in Ramda: Combining funtions

Simple Combinations

Once you’ve gotten used to the idea of passing functions to other functions, you might start to find situations where you want to combine sereral functions together.

Ramda provides several functions for doing simple combinations.

Complement

Ramda provides a higher-order-function,complement, that takes another function and return a new function that returns true when the original function returns a falsy value, and false when the original function returns a truthy value.

Thinking in Ramda 4

Thinking in Ramda: Declarative Programming

As we start writing small functional building blocks and combining them, we find that we have to write a lot of functions that wrap JavaScript’s operators such as arithmetic, comparison, logic, and control flow. This can feel tedious, but Ramda has our back.

There are many different ways to divide up the programming language/style landscape. There’s static typing vs dynamic typing, interpreted languages vs compiled languages, low-level vs high-level, etc.

Another such division is imperative programming vs declarative programming.

Thinking in Ramda 3

Thinking in Ramda: Partial Application

Higher-Order Functions

Functions that take or return other functions are known as “higher-order functions”.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
//  Full function version:
function publishedInYear(year) {
return function(book){
return book.year === year
}
}

// Arrow function version:
const publishedInYear = year => book => book.year === year
const titlesForYear = (books, year) => {
const selected = filter(publishedInYear(year), books)

return map(book => book.title, selected)
}

Thinking in Ramda 5

Thinking in Ramda: Pointfree Style

Pointfree Style

  • Put the data last
  • Curry all the things
1
2
3
4
5
const water = cond([
[equals(0), always('water freezes at 0°C')],
[equals(100), alwyas('water boils at 100°C')],
[T, temp => `nothing sepcial happends at ${temp}°C`]
])

Multi-argument Functions

1
2
3
4
5
6
const titlesForYear = curry((year, books) =>
pipe(
filter(publishedInYear(year)),
map(book => book.title)
)(books)
)
1
2
3
4
5
const titlesForYear = year =>
pipe(
filter(publishedInYear(year)),
map(book => book.title)
)

Thinking in Ramda 6

Thinking in Ramda: Immutability and Objects

Reading Object Properties

We can make the functions more declarative using equals and gte.

Prop

Fortunately, Ramda can help us out.It provides the prop function for accesing properties of an object.

Pick

Where prop reads a single property from an object and returns the value, p ick reads multiple properties from an object and returns a new object with just those properties.

Thinking in Ramda 7

Thinking in Ramda: Immutability and Arrays

Reading Array Elements

Ramda functions for reading array elements nth and slice and contains

1
2
3
4
5
const numbers = [10,20,30,40,50,60]
nth(3, numbers) // => 40 (0-based indexing)
nth(-2, numbers) // =>50 (negative numbers start from the right)
slice(2, 5, numbers) // => [30,40, 50] (see below)
contains(20, numbers) // => true

nth(0) equals head, nth(-1) equals last.

It also provides functions for accesing all-but-the-first element tail,all-but-the-last element init,the first N elements take(N), and the last N elments takeLast(N).

Thinking in Ramda 8

Thinking in Ramda: Lenses

Ramda provides a more general tool for performing the operations such as read, update, and transform object properties and array elements in a declarative, immutable way, the lens.

What is a Lens?

A lens combines a “getter” function and a “setter” function into a single unit. Ramda provides a set of functions for working with lenses.

We can think of a lens as something that focuses on a specific part of a larger data structure.

How Do I Create a Lens?

The most generic way to create a lens in Ramda is with the lens function. lens takes a getter function and a setter function and returns the new lens.

1
2
3
4
5
6
7
8
9
10
11
12
13
const person = {
name: 'Randy',
socialMedia:{
github: 'randycoulamn',
twitter: '@randycoulman'
}
}

const nameLens = lens(prop('name'), assoc('name'))
const twitterLens = lens(
path(['socialMedia', 'twitter']),
assocPath(['socialMedia', 'twitter'])
)

Thinking in Ramda 9

Thinking in Ramda: Wrap-Up

Ramda has some underlying principles that drive its API:

  • Data last: Almost all of the functions take the data parameter as the last parameter.

  • Currying: Almost every function in Ramda is “curried”. That is, you can call a function with only a subset of its required arguments, and it will return a new function that takes the remaining arguments. Once all of the arguments are provided, the original function is invoked.

函数式编程入门

Function Program

几个原则

  • DRY(不要重复自己,don’t repeat yourself)
  • 高内聚低耦合(loose coupling high cohesion)
  • YAGNI (你不会用到它的,ya ain’t gonna need it)
  • 最小意外原则(Principle of least surprise)
  • 单一责任(single responsibility)
  • 纯函数,尽量减少副作用

纯函数是这样一种函数,即相同的输入,永远会得到相同的输出,而且没有任何可观察的副作用。

副作用是在计算结果的过程中,系统状态的一种变化,或者与外部世界进行的可观察的交互

函数式编程入门(2)

Applicative functor

应用applicative functor作为一个接口可以让不通funcotr可以相互应用(apply)的能力。

协调与激励

1
2
3
4
5
6
7
// Http.get :: String -> Task Error HTML
var renderPage = curry(function(destinations, event) {
/* render page */
});

Task.of(renderPage).ap(Http.get('/destinations')).ap(Http.get('/events'))
// Task("<div>some page with dest and events</div>")

两个请求将会同时立即执行,当两者的响应都返回之后,renderPage 就会被调用。这与 monad 版本的那种必须等待前一个任务完成才能继续执行后面的操作完全不同。本来我们就无需根据目的地来获取事件,因此也就不需要依赖顺序执行。

再次强调,因为我们是使用局部调用的函数来达成上述结果的,所以必须要保证 renderpage 是 curry 函数,否则它就不会一直等到两个 Task 都完成。而且如果你碰巧自己做过类似的事,那你一定会感激 applicative functor 这个异常简洁的接口的。这就是那种能够让我们离“奇点”(singularity)更近一步的优美代码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 帮助函数:
// ===================
// $ :: String -> IO DOM
var $ = function(selector) {
return new IO(function() {
return document.querySelector(selector)
});
}

// getVal :: String -> IO String
var getVal = compose(map(_.prop('value')), $);

// Exapmle
// ===================
// signIn :: String -> String -> Bool -> User
var signIn = curry(function(username, password, remember_me)) {/* singing in */}

IO.of(signIn).ap(getVal('#email')).ap(getVal('#password')).ap(IO.of(false));

signIn 是一个接收 3 个参数的 curry 函数,因此我们需要调用 ap 3 次。在每一次的 ap 调用中,signIn 就收到一个参数然后运行,直到所有的参数都传进来,它也就执行完毕了。我们可以继续扩展这种模式,处理任意多的参数。另外,左边两个参数在使用 getVal 调用后自然而然地成为了一个 IO,但是最右边的那个却需要手动 lift,然后变成一个 IO,这是因为 ap 需要调用者及其参数都属于同一类型。

lift

我们来试试以一种 pointfree 的方式调用 applicative functor。因为 map 等价于 of/ap,那么我们就可以定义无数个能够 ap 通用函数。

1
2
3
4
5
6
7
8
var liftA2 = curry(function(f, functor1, functor2) {
return functor1.map(f).ap(functor2);
});

var liftA3 = curry(function(f, functor1, functor2, functor3) {
return functor1.map(f).ap(functor2).ap(functor3);
})
// liftA4, etc

操作符

在haskell、scala、PureScript以及swift等语言中,开发者可以创建自定义的中缀操作符(infix operators)

1
2
-- haskell
add <$> Right 2 <*> Right 3
1
map(add, Right(2)).ap(Right(3))

<$> 就是map(亦即fmap), <*>就是ap

衍生函数(derived function)

of/ap 等价于map

1
2
3
4
// 从of/ap衍生出的map
X.prototype.map = function(f) {
return this.constructor.of(f).ap(this);
}

如果已经有chain函数,可以免费得到functor 和 applicative:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 从chain 衍生出的map
X.prototype.map = function() {
var m = this;
return m.chain(function(a) {
return m.constructor.of(f(a));
});
}

// 从 chain/map 衍生出的ap
X.prototype.ap = function(other) {
return this.chain(function(f) {
return other.map(f);
});
}

定律

applicative functor 是“组合关闭”(closed under composition)的,意味着 ap 永远不会改变容器类型。

### 同一律

1
2
// 同一律
A.of(id).ap(v) == v

同态

1
2
//同态
A.of(f).ap(A.of(x)) == A.of(f(x))

同态就是一个能够保持接结构的映射(structure preserving map).实际上,funcotr就是一个再不通范畴间的同态,因为funcotr在经过映射之后保持了原始范畴的结构。

互换(interchange)

互换(interchange)表明的是选择让函数再ap的左边还是右边发生lift是无关紧要的。

1
2
// 互换
v.ap(A.of(x)) == A.of(function(f) { return f(x)}).ap(v)
1
2
3
4
var v = Task.of(_.reverse);
var x = 'Sparklehorse';

v.ap(Task.of(x)) == Task.of(function(f) { return f(x)}).ap(v)

组合(composition)

组合不过是在检查标准的函数组合是否适用于容器内部的函数调用。

1
2
// 组合
A.of(compose).ap(u).ap(v).ap(w) == u.ap(v.ap(w));
1
2
3
4
5
var u = IO.of(_.toUpper);
var v = IO.of(_.concat("& beyond"));
var w = IO.of("lood bath");

IO.of(_compose).ap(u).ap(v).ap(w) == u.ap(v.ap(w))

总结

函数式嗯编程

,
Fork me on GitHub