函数式编程 模式
This article targets an audience that’s graduating from functional libraries like ramda
to using Algebraic Data Types. We’re using the excellent crocks
library for our ADTs and helpers, although these concepts may apply to other ones as well. We’ll be focusing on demonstrating practical applications and patterns without delving into a lot of theory.
本文面向从ramda
类的功能库毕业到使用代数数据类型的ramda
。 我们正在为ADT和辅助程序使用出色的crocks
库,尽管这些概念也可能适用于其他概念。 我们将专注于演示实际的应用程序和模式,而无需深入研究很多理论。
安全执行危险功能 (Safely Executing Dangerous Functions)
Let’s say we have a situation where we want to use a function called darken
from a third-party library. darken
takes a multiplier, a color and returns a darker shade of that color.
假设有一种情况,我们想使用第三方库中的称为darken
函数。 darken
需要乘数,一种颜色并返回该颜色的较深阴影。
// darken :: Number -> String -> String
darken(0.1)("gray")
//=> "#343434"
Pretty handy for our CSS needs. But it turns out that the function is not as innocent as it seems. darken
throws errors when it receives unexpected arguments!
非常方便满足我们CSS需求。 但事实证明,该功能并不像看起来那样简单。 当它收到意外的参数时, darken
会引发错误!
darken(0.1)(null)
=> // Error: Passed an incorrect argument to a color function, please pass a string representation of a color.
This is, of course, very helpful for debugging — but we wouldn’t want our application to blow up just because we couldn’t derive a color. Here’s where tryCatch
comes to the rescue.
当然,这对于调试非常有帮助-但我们不希望仅由于无法派生颜色而使应用程序崩溃。 这是tryCatch
进行救援的地方。
import { darken } from "polished"
import { tryCatch, compose, either, constant, identity, curry } from "crocks"
// safeDarken :: Number -> String -> String
const safeDarken = curry(n =>
compose(
either(constant("inherit"), identity),
tryCatch(darken(n))
)
)
tryCatch
executes the provided function within a try-catch block and returns a Sum Type called Result
. In its essence, a Sum Type is basically an “or” type. This means that the Result
could be either an Ok
if an operation is successful or an Error
in case of failures. Other examples of Sum Types include Maybe
, Either
, Async
and so on. The either
point-free helper breaks the value out of the Result
box, and returns the CSS default inherit
if things went south or the darkened color if everything went well.
tryCatch
在try-catch块中执行提供的功能,并返回一个称为Result
的Sum Type。 本质上,Sum类型基本上是“或”类型。 这意味着该Result
可能是任一种Ok
是否操作成功或 Error
的故障的情况下。 Sum类型的其他示例包括Maybe
, Either
, Async
等。 either
点辅助程序都将值从Result
框中分解出来,如果一切向南,则返回CSS默认inherit
如果一切顺利,则返回暗色。
safeDarken(0.5)(null)
//=> inherit
safeDarken(0.25)('green')
//=> '#004d00'
使用Maybe Helpers强制类型 (Enforcing Types using Maybe Helpers)
With JavaScript, we often run into cases where our functions explode because we’re expecting a particular data type, but we receive a different one instead. crocks
provides the safe
, safeAfter
and safeLift
functions that allow us to execute code more predictably by using the Maybe
type. Let’s look at a way to convert camelCased strings into Title Case.
使用JavaScript,我们经常会遇到函数爆炸的情况,因为我们期望的是一种特定的数据类型,但是却收到了另一种数据类型。 crocks
提供safe
, safeAfter
和safeLift
功能,使我们能够使用更多的可预见的执行代码Maybe
类型。 让我们看看一种将驼峰式字符串转换为Title Case的方法。
import { safeAfter, safeLift, isArray, isString, map, compose, option } from "crocks"
// match :: Regex -> String -> Maybe [String]
const match = regex => safeAfter(isArray, str => str.match(regex))
// join :: String -> [String] -> String
const join = separator => array => array.join(separator)
// upperFirst :: String -> String
const upperFirst = x =>
x.charAt(0)
.toUpperCase()
.concat(x.slice(1).toLowerCase())
// uncamelize :: String -> Maybe String
const uncamelize = safeLift(isString, compose(
option(""),
map(compose(join(" "), map(upperFirst))),
match(/(((^[a-z]|[A-Z])[a-z]*)|[0-9]+)/g),
))
uncamelize("rockTheCamel")
//=> Just "Rock The Camel"
uncamelize({})
//=> Nothing
We’ve created a helper function match
that uses safeAfter
to iron out String.prototype.match
’s behavior of returning an undefined
in case there are no matches. The isArray
predicate ensures that we receive a Nothing
if there are no matches found, and a Just [String]
in case of matches. safeAfter
is great for executing existing or third-party functions in a reliable safe manner.
我们创建了一个辅助函数match
,该函数使用safeAfter
消除String.prototype.match
的行为,即在没有匹配项的情况下返回undefined
的行为。 isArray
谓词可确保在没有找到匹配项的情况下接收Nothing
,在匹配项的情况下接收Just [String]
。 safeAfter
非常适合以可靠的安全方式执行现有功能或第三方功能。
(Tip: safeAfter
works really well with ramda
functions that return a | undefined
.)
(提示: safeAfter
与返回a | undefined
ramda
函数配合使用非常好。)
Our uncamelize 🐪
function is executed with safeLift(isString)
which means that it’ll only execute when the input returns true for the isString
predicate.
我们的uncamelize 🐪
函数是使用safeLift(isString)
执行的,这意味着它仅在输入为isString
谓词返回true时才执行。
In addition to this, crocks also provides the prop
and propPath
helpers which allow you to pick properties from Object
s and Array
s.
除此之外, propPath
还提供了prop
和propPath
帮助器,使您可以从Object
和Array
选择属性。
import { prop, propPath, map, compose } from "crocks"
const goodObject = {
name: "Bob",
bankBalance: 7999,
address: {
city: "Auckland",
country: "New Zealand",
},
}
prop("name")(goodObject)
//=> Just "Bob"
propPath(["address", "city"])(goodObject)
//=> Just "Auckland"
// getBankBalance :: Object -> Maybe String
const getBankBalance = compose(
map(balance => balance.toFixed(2)),
prop("bankBalance")
)
getBankBalance(goodObject)
//=> Just '7999.00'
getBankBalance({})
//=> Nothing
This is great, especially if we’re dealing with data from side-effects that are not under our control like API responses. But what happens if the API developers suddenly decide to handle formatting at their end?
这很棒,特别是如果我们要处理的副作用数据不受API响应之类的控制。 但是,如果API开发人员突然决定在他们的末端处理格式会怎样?
const badObject = {
name: "Rambo",
bankBalance: "100.00",
address: {
city: "Hope",
country: "USA"
}
}
getBankBalance(badObject) // TypeError: balance.toFixed is not a function :-(
Runtime errors! We tried to invoke the toFixed
method on a String, which doesn’t really exist. We need to make sure that bankBalance
is really a Number
before we invoke toFixed
on it. Let’s try to solve it with our safe
helper.
运行时错误! 我们尝试在String上调用toFixed
方法,该方法实际上并不存在。 我们需要确保bankBalance
确实是一个Number
然后再调用toFixed
。 让我们尝试使用我们的safe
助手来解决它。
import { prop, propPath, compose, map, chain, safe, isNumber } from "crocks"
// getBankBalance :: Object -> Maybe String
const getBankBalance = compose(
map(balance => balance.toFixed(2)),
chain(safe(isNumber)),
prop("bankBalance")
)
getBankBalance(badObject) //=> Nothing
getBankBalance(goodObject) //=> Just '7999.00'
We pipe the results of the prop
function to our safe(isNumber)
function which also returns a Maybe
, depending on whether the result of prop
satisfies the predicate. The pipeline above guarantees that the last map
which contains the toFixed
will only be called when bankBalance
is a Number
.
我们将prop
函数的结果传递给我们的safe(isNumber)
函数,该函数还会返回一个Maybe
,这取决于prop
的结果是否满足谓词。 上面的管道保证只有在bankBalance
为Number
时才调用包含toFixed
的最后一个map
。
If you’re going to be dealing with a lot of similar cases, it would make sense to extract this pattern as a helper:
如果您要处理许多类似的情况,则可以将这种模式提取为帮助对象:
import { Maybe, ifElse, prop, chain, curry, compose, isNumber } from "crocks"
const { of, zero } = Maybe
// propIf :: (a -> Boolean) -> [String | Number] -> Maybe a
const propIf = curry((fn, path) =>
compose(
chain(ifElse(fn, of, zero)),
prop(path)
)
)
propIf(isNumber, "age", goodObject)
//=> Just 7999
propIf(isNumber, "age", badObject)
//=> Nothing
使用专用语保持功能清洁 (Using Applicatives to keep Functions Clean)
Often times, we find ourselves in situations where we would want to use an existing function with values wrapped in a container. Let’s try to design a safe add
function that allows only numbers, using the concepts from the previous section. Here’s our first attempt.
通常,我们发现自己希望使用一个将现有值包装在容器中的函数。 让我们尝试使用上一节中的概念设计一个仅允许数字的安全add
函数。 这是我们的第一次尝试。
import { Maybe, safe, isNumber } from "crocks"
// safeNumber :: a -> Maybe a
const safeNumber = safe(isNumber)
// add :: a -> b -> Maybe Number
const add = (a, b) => {
const maybeA = safeNumber(a)
const maybeB = safeNumber(b)
return maybeA.chain(
valA => maybeB.map(valB => valA + valB)
)
}
add(1, 2)
//=> Just 3
add(1, {})
//=> Nothing
This does exactly what we need, but our add
function is no longer a simple a + b
. It has to first lift our values into Maybe
s, then reach into them to access the values, and then return the result. We need to find a way to preserve the core functionality of our add
function while allowing it to work with values contained in ADTs! Here’s where Applicative Functors come in handy.
这正是我们需要的功能,但是我们的add
函数不再是简单的a + b
。 它必须首先将我们的值提升为Maybe
,然后进入它们以访问这些值,然后返回结果。 我们需要找到一种方法来保留add
函数的核心功能,同时使其能够与ADT中包含的值一起使用! 这是应用函子派上用场的地方。
An Applicative Functor is just a like a regular functor, but along with map
, it also implements two additional methods:
应用函子就像常规函子一样,但是与map
,它还实现了两个附加方法:
of :: Applicative f => a -> f a
The of
is a completely dumb constructor, and lifts any value that you give it into our data type. It’s also referred to as pure
in other languages.
of
是一个完全愚蠢的构造函数,并将您赋予它的所有值提升为我们的数据类型。 在其他语言中也称为pure
。
Maybe.of(null)
//=> Just null
Const.of(42)
//=> Const 42
And here’s where all the money is — the ap
method:
这就是所有金钱的来源ap
方法:
ap :: Apply f => f a ~> f (a -> b) -> f b
The signature looks very similar to map
, with the only difference being that our a -> b
function is also wrapped in an f
. Let’s see this in action.
签名看起来与map
非常相似,唯一的区别是我们a -> b
函数也包装在f
。 让我们来看看实际情况。
import { Maybe, safe, isNumber } from "crocks"
// safeNumber :: a -> Maybe a
const safeNumber = safe(isNumber)
// add :: a -> b -> c
const add = a => b => a + b
// add :: a -> b -> Maybe Number
const safeAdd = (a, b) => Maybe.of(add)
.ap(safeNumber(a))
.ap(safeNumber(b))
safeAdd(1, 2)
//=> Just 3
safeAdd(1, "danger")
//=> Nothing
We first lift our curried add
function into a Maybe
, and then apply Maybe a
and Maybe b
to it. We’ve been using map
so far to access the value inside a container and ap
is no different. Internally, it map
s on safeNumber(a)
to access the a
and applies it to add
. This results in a Maybe
that contains a partially applied add
. We repeat the same process with safeNumber(b)
to execute our add
function, resulting in a Just
of the result if both a
and b
are valid or a Nothing
otherwise.
我们首先将咖喱化的add
函数提升为Maybe
,然后将Maybe a
和Maybe b
应用于它。 到目前为止,我们一直在使用map
来访问容器内的值,而ap
也不ap
。 在内部,它将s map
到safeNumber(a)
以访问a
并将其应用于add
。 这导致Maybe
包含部分应用的add
。 我们使用safeNumber(b)
重复相同的过程以执行add
功能,如果a
和b
均有效,则结果为Just
,否则为Nothing
。
Crocks also provides us the liftA2
and liftN
helpers to express the same concept in a pointfree manner. A trivial example follows:
Crocks还为我们提供了liftA2
和liftN
帮助者,以无意义的方式表达了相同的概念。 一个简单的示例如下:
liftA2(add)(Maybe(1))(Maybe(2))
//=> Just 3
We shall use this helper extensively in the section Expressing Parallelism
.
我们将在“ Expressing Parallelism
”一节中广泛使用此帮助器。
Tip: Since we’ve observed that ap
uses map
to access values, we can do cool things like generating a Cartesian product when given two lists.
提示:由于我们已经观察到ap
使用map
来访问值,因此当给出两个列表时,我们可以做一些很酷的事情,例如生成笛卡尔乘积。
import { List, Maybe, Pair, liftA2 } from "crocks"
const names = List(["Henry", "George", "Bono"])
const hobbies = List(["Music", "Football"])
List(name => hobby => Pair(name, hobby))
.ap(names)
.ap(hobbies)
// => List [ Pair( "Henry", "Music" ), Pair( "Henry", "Football" ),
// Pair( "George", "Music" ), Pair( "George", "Football" ),
// Pair( "Bono", "Music" ), Pair( "Bono", "Football" ) ]
使用异步进行可预测的错误处理 (Using Async for Predictable Error Handling)
crocks
provides the Async
data type that allows us to build lazy asynchronous computations. To know more about it, you can refer to the extensive official documentation here. This section aims to provide examples of how we can use Async
to improve the quality of our error reporting and make our code resilient.
crocks
提供了Async
数据类型,该数据类型使我们能够构建惰性异步计算。 要了解更多信息,请参阅此处的大量官方文档。 本节旨在提供示例,说明如何使用Async
来改善错误报告的质量并使代码具有弹性。
Often, we run into cases where we want to make API calls that depend on each other. Here, the getUser
endpoint returns a user entity from GitHub and the response contains a lot of embedded URLs for repositories, stars, favorites and so on. We will see how we can design this use case with using Async
.
通常,我们会遇到需要相互依赖的API调用的情况。 在这里, getUser
端点从GitHub返回一个用户实体,并且响应包含许多用于存储库,星号,收藏夹等的嵌入式URL。 我们将看到如何使用Async
设计此用例。
import { Async, prop, compose, chain, safe, isString, maybeToAsync } from "crocks"
const { fromPromise } = Async
// userPromise :: String -> Promise User Error
const userPromise = user => fetch(`https://api.github.com/users/${user}`)
.then(res => res.json())
// resourcePromise :: String -> Promise Resource Error
const resourcePromise = url => fetch(url)
.then(res => res.json())
// getUser :: String -> Async User Error
const getUser = compose(
chain(fromPromise(userPromise)),
maybeToAsync('getUser expects a string'),
safe(isString)
)
// getResource :: String -> Object -> Async Resource Error
const getResource = path => user => {
if (!isString(path)) {
return Async.Rejected("getResource expects a string")
}
return maybeToAsync("Error: Malformed user response received", prop(path, user))
.chain(fromPromise(resourcePromise))
}
// logError :: (...a) -> IO()
const logError = (...args) => console.log("Error: ", ...args)
// logResponse :: (...a) -> IO()
const logSuccess = (...args) => console.log("Success: ", ...args)
getUser("octocat")
.chain(getResource("repos_url"))
.fork(logError, logSuccess)
//=> Success: { ...response }
getUser(null)
.chain(getResource("repos_url"))
.fork(logError, logSuccess)
//=> Error: The user must be as string
getUser("octocat")
.chain(getResource(null))
.fork(logError, logSuccess)
//=> Error: getResource expects a string
getUser("octocat")
.chain(getResource("unknown_path_here"))
.fork(logError, logSuccess)
//=> Error: Malformed user response received
The usage of the maybeToAsync
transformation allows us to use all of the safety features that we get from using Maybe
and bring them to our Async
flows. We can now flag input and other errors as a part of our Async
flows.
通过使用maybeToAsync
转换,我们可以使用通过使用Maybe
获得的所有安全功能,并将其带入Async
流。 现在,我们可以将输入和其他错误标记为Async
流程的一部分。
有效地使用Monoid (Using Monoids Effectively)
We’ve already been using Monoids when we perform operations like String
/Array
concatenation and number addition in native JavaScript. It’s simply a data type that offers us the following methods.
在本机JavaScript中执行诸如String
/ Array
串联和数字加法之类的操作时,我们已经在使用Monoid。 它只是一种为我们提供以下方法的数据类型。
concat :: Monoid m => m a -> m a -> m a
concat
allows us to combine two Monoids of the same type together with a pre-specified operation.
concat
允许我们将两个相同类型的Monoid与预定操作结合在一起。
empty :: Monoid m => () => m a
The empty
method provides us with an identity element, that when concat
ed with other Monoids of the same type, would return the same element. Here’s what I’m talking about.
所述empty
方法为我们提供了一个标识元件,使得当concat
与同类型的其它幺编,将返回相同的元件。 这就是我在说的。
import { Sum } from "crocks"
Sum.empty()
//=> Sum 0
Sum(10)
.concat(Sum.empty())
//=> Sum 10
Sum(10)
.concat(Sum(32))
//=> Sum 42
By itself, this doesn’t look very useful, but crocks
provides some additional Monoids along with helpers mconcat
, mreduce
, mconcatMap
and mreduceMap
.
就其本身而言,这似乎不是很有用,但是crocks
提供了一些附加的mconcat
以及辅助对象mconcat
, mreduce
, mconcatMap
和mreduceMap
。
import { Sum, mconcat, mreduce, mconcatMap, mreduceMap } from "crocks"
const array = [1, 3, 5, 7, 9]
const inc = x => x + 1
mconcat(Sum, array)
//=> Sum 25
mreduce(Sum, array)
//=> 25
mconcatMap(Sum, inc, array)
//=> Sum 30
mreduceMap(Sum, inc, array)
//=> 30
The mconcat
and mreduce
methods take a Monoid and a list of elements to work with, and apply concat
to all of their elements. The only difference between them is that mconcat
returns an instance of the Monoid while mreduce
returns the raw value. The mconcatMap
and mreduceMap
helpers work in the same way, except that they accept an additional function that is used to map over every element before calling concat
.
mconcat
和mreduce
方法采用Monoid和要使用的元素列表,并将concat
应用于所有元素。 它们之间的唯一区别是mconcat
返回Monoid的实例,而mreduce
返回原始值。 mconcatMap
和mreduceMap
辅助mreduceMap
工作方式相同,不同之处在于,它们在调用concat
之前接受了用于映射每个元素的附加函数。
Let’s look at another example of a Monoid from crocks
, the First
Monoid. When concatenating, First
will always return the first, non-empty value.
让我们看一个Monoid从另一个例子crocks
中, First
含半幺群。 串联时, First
将始终返回第一个非空值。
import { First, Maybe } from "crocks"
First(Maybe.zero())
.concat(First(Maybe.zero()))
.concat(First(Maybe.of(5)))
//=> First (Just 5)
First(Maybe.of(5))
.concat(First(Maybe.zero()))
.concat(First(Maybe.of(10)))
//=> First (Just 5)
Using the power of First
, let’s try creating a function that attempts to get the first available property on an object.
使用First
的功能,让我们尝试创建一个函数,该函数尝试获取对象上的第一个可用属性。
import { curry, First, mreduceMap, flip, prop, compose } from "crocks"
/** tryProps -> a -> [String] -> Object -> b */
const tryProps = flip(object =>
mreduceMap(
First,
flip(prop, object),
)
)
const a = {
x: 5,
z: 10,
m: 15,
g: 12
}
tryProps(["a", "y", "b", "g"], a)
//=> Just 12
tryProps(["a", "b", "c"], a)
//=> Nothing
tryProps(["a", "z", "c"], a)
//=> Just 10
Pretty neat! Here’s another example that tries to create a best-effort formatter when provided different types of values.
漂亮整齐! 这是另一个示例,它在提供不同类型的值时尝试创建尽力而为的格式化程序。
import {
applyTo, mreduceMap, isString, isEmpty, mreduce, First, not, isNumber, chain
compose, safe, and, constant, Maybe, map, equals, ifElse, isBoolean, option,
} from "crocks";
// isDate :: a -> Boolean
const isDate = x => x instanceof Date;
// lte :: Number -> Number -> Boolean
const lte = x => y => y <= x;
// formatBoolean :: a -> Maybe String
const formatBoolean = compose(
map(ifElse(equals(true), constant("Yes"), constant("No"))),
safe(isBoolean)
);
// formatNumber :: a -> Maybe String
const formatNumber = compose(
map(n => n.toFixed(2)),
safe(isNumber)
);
// formatPercentage :: a -> Maybe String
const formatPercentage = compose(
map(n => n + "%"),
safe(and(isNumber, lte(100)))
);
// formatDate :: a -> Maybe String
const formatDate = compose(
map(d => d.toISOString().slice(0, 10)),
safe(isDate)
);
// formatString :: a -> Maybe String
const formatString = safe(isString)
// autoFormat :: a -> Maybe String
const autoFormat = value =>
mreduceMap(First, applyTo(value), [
formatBoolean,
formatPercentage,
formatNumber,
formatDate,
formatString
]);
autoFormat(true)
//=> Just "Yes"
autoFormat(10.02)
//=> Just "10%"
autoFormat(255)
//=> Just "255.00"
autoFormat(new Date())
//=> Just "2019-01-14"
autoFormat("YOLO!")
//=> Just "YOLO!"
autoFormat(null)
//=> Nothing
以无点方式表达并行性 (Expressing Parallelism in a Pointfree manner)
We might run into cases where want to perform multiple operations on a single piece of data and combine the results in some way. crocks
provides us with two methods to achieve this. The first pattern leverages Product Types Pair
and Tuple
. Let’s look at a small example where we have an object that looks like this:
我们可能会遇到想要对单个数据执行多项操作并以某种方式合并结果的情况。 crocks
为我们提供了两种方法来实现这一目标。 第一种模式利用产品类型Pair
和Tuple
。 让我们看一个小示例,其中有一个看起来像这样的对象:
{ ids: [11233, 12351, 16312], rejections: [11233] }
We would like to write a function that accepts this object and returns an Array
of ids
excluding the rejected ones. Our first attempt in native JavaScript would look like this:
我们想编写一个接受该对象并返回一个ids
Array
(不包括拒绝的ids
的函数。 我们在原生JavaScript中的首次尝试如下所示:
const getIds = (object) => object.ids.filter(x => object.rejections.includes(x))
This of course works, but it would explode in case one of the properties is malformed or is not defined. Let’s make getIds
return a Maybe
instead. We use fanout
helper that accepts two functions, runs it on the same input and returns a Pair
of the results.
这当然可以,但是如果其中一个属性格式不正确或未定义,则会爆炸。 让我们让getIds
返回Maybe
代替。 我们使用fanout
帮助程序,该程序接受两个函数,在同一输入上运行它,并返回一Pair
结果。
import { prop, compose, equals, filter, fanout, merge, liftA2 } from "crocks"
/**
* object :: Record
* Record :: {
* ids: [Number]
* rejection: [Number]
* }
**/
const object = { ids: [11233, 12351, 16312], rejections: [11233] }
// excludes :: [a] -> [b] -> Boolean
const excludes = x => y => !x.includes(y)
// difference :: [a] -> [a] -> [a]
const difference = compose(filter, excludes)
// getIds :: Record -> Maybe [Number]
const getIds = compose(
merge(liftA2(difference)),
fanout(prop("rejections"), prop("ids"))
)
getIds(object)
//=> Just [ 12351, 16312 ]
getIds({ something: [], else: 5 })
//=> Nothing
One of the main benefits of using the pointfree approach is that it encourages us to break our logic into smaller pieces. We now have the reusable helper difference
(with liftA2
, as seen previously) that we can use to merge
both halves the Pair
together.
使用无点方法的主要好处之一是,它鼓励我们将逻辑分解为更小的部分。 现在,我们有了可重用的辅助程序difference
( liftA2
,使用了liftA2
),我们可以将它们merge
Pair
。
The second method would be to use the converge
combinator to achieve similar results. converge
takes three functions and an input value. It then applies the input to the second and third function and pipes the results of both into the first. Let’s use it to create a function that normalizes an Array
of objects based on their id
s. We will use the Assign
Monoid that allows us to combine objects together.
第二种方法是使用converge
组合器获得相似的结果。 converge
具有三个功能和一个输入值。 然后,它将输入应用于第二个和第三个函数,并将这两个函数的结果传递到第一个函数中。 让我们用它来创建一个函数,该函数根据对象的id
标准化对象Array
。 我们将使用允许将对象组合在一起的“ Assign
Monoid”。
import {
mreduceMap, applyTo, option, identity, objOf, map,
converge, compose, Assign, isString, constant
} from "crocks"
import propIf from "./propIf"
// normalize :: String -> [Object] -> Object
const normalize = mreduceMap(
Assign,
converge(
applyTo,
identity,
compose(
option(constant({})),
map(objOf),
propIf(isString, "id")
)
)
)
normalize([{ id: "1", name: "Kerninghan" }, { id: "2", name: "Stallman" }])
//=> { 1: { id: '1', name: 'Kerninghan' }, 2: { id: '2', name: 'Stallman' } }
normalize([{ id: null}, { id: "1", name: "Knuth" }, { totally: "unexpected" }])
//=> { 1: { id: '1', name: 'Knuth' } }
使用遍历和序列确保数据合理性 (Using Traverse and Sequence to Ensure Data Sanity)
We’ve seen how to use Maybe
and friends to ensure that we’re always working with the types we expect. But what happens when we’re working with a type that contains other values, like an Array
or a List
for example? Let’s look at a simple function that gives us the total length of all strings contained within an Array
.
我们已经了解了如何使用Maybe
和朋友来确保我们始终使用期望的类型。 但是,当我们使用包含其他值的类型(例如, Array
或List
时会发生什么? 让我们看一个简单的函数,该函数为我们提供Array
包含的所有字符串的总长度。
import { compose, safe, isArray, reduce, map } from "crocks"
// sum :: [Number] -> Number
const sum = reduce((a, b) => a + b, 0)
// length :: [a] -> Number
const length = x => x.length;
// totalLength :: [String] -> Maybe Number
const totalLength = compose(
map(sum),
map(map(length)),
safe(isArray)
)
const goodInput = ["is", "this", "the", "real", "life?"]
totalLength(goodInput)
//=> Just 18
const badInput = { message: "muhuhahhahahaha!"}
totalLength(badInput)
//=> Nothing
Great. We’ve made sure our function always returns a Nothing
if it doesn’t receive an Array
. Is this enough, though?
大。 我们确保函数如果未接收到Array
则始终返回Nothing
。 够了吗?
totalLength(["stairway", "to", undefined])
//=> TypeError: x is undefined
Not really. Our function doesn’t guarantee that the contents of the list won’t hold any surprises. One of the ways we could solve this would be to define a safeLength
function that only works with strings:
并不是的。 我们的功能不能保证列表的内容不会令人惊讶。 我们可以解决此问题的方法之一是定义仅适用于字符串的safeLength
函数:
// safeLength :: a -> Maybe Number
const safeLength = safeLift(isString, length)
If we use safeLength
instead of length
as our mapping function, we would receive a [Maybe Number]
instead of a [Number]
and we cannot use our sum
function anymore. Here’s where sequence
comes in handy.
如果我们使用safeLength
而不是length
作为映射函数,则将收到一个[Maybe Number]
而不是[Number]
并且我们不能再使用sum
函数。 这是sequence
派上用场的地方。
import { sequence, Maybe, Identity } from "crocks"
sequence(Maybe, Identity(Maybe.of(1)))
//=> Just Identity 1
sequence(Array, Identity([1,2,3]))
//=> [ Identity 1, Identity 2, Identity 3 ]
sequence(Maybe, [Maybe.of(4), Maybe.of(2)])
//=> Just [ 4, 2 ]
sequence(Maybe, [Maybe.of(4), Maybe.zero()])
//=> Nothing
sequence
helps swap the inner type with the outer type while performing a certain effect
, given that the inner type is an Applicative. The sequence
on Identity
is pretty dumb — it just map
s over the inner type and returns the contents wrapped in an Identity
container. For List
and Array
, sequence
uses reduce
on the list to combine its contents using ap
and concat
. Let’s see this in action in our refactored totalLength
implementation.
假定内部类型是Applicative, sequence
可以在执行某种effect
同时将内部类型与外部类型交换。 Identity
上的sequence
非常笨拙-只是map
s map
到内部类型上,然后返回包装在Identity
容器中的内容。 对于List
和Array
, sequence
使用ap
和concat
在list上使用reduce
组合其内容。 让我们在重构的totalLength
实现中看到这一点。
// totalLength :: [String] -> Maybe Number
const totalLength = compose(
map(sum),
chain(sequence(Maybe)),
map(map(safeLength)),
safe(isArray)
)
const goodString = ["is", "this", "the", "real", "life?"]
totalLength(goodString)
//=> Just 18
totalLength(["stairway", "to", undefined])
//=> Nothing
Great! We’ve built a completely bulletproof totalLength
. This pattern of mapping over something from a -> m b
and then using sequence
is so common that we have another helper called traverse
which performs both operations together. Let’s see how we can use traverse
instead of sequence in the above example.
大! 我们已经建立了一个完全防弹的totalLength
。 这种从a -> mb
到a -> mb
然后再使用sequence
的映射模式非常普遍,以至于我们有了另一个名为traverse
助手,它可以同时执行这两种操作。 让我们看看如何在上面的示例中使用traverse
而不是序列。
// totalLengthT :: [String] -> Maybe Number
const totalLengthT = compose(
map(sum),
chain(traverse(Maybe, safeLength)),
safe(isArray)
)
There! It works exactly the same way. If we think about it, our sequence
operator is basically traverse
, with an identity
as the mapping function.
那里! 它的工作方式完全相同。 如果我们考虑一下,我们的sequence
运算符基本上是traverse
,以identity
作为映射函数。
Note: Since we cannot infer inner type using JavaScript, we have to explicitly provide the type constructor as the first argument to traverse
and sequence
.
注意:由于无法使用JavaScript推断内部类型,因此必须显式提供类型构造函数作为traverse
和sequence
的第一个参数。
It’s easy to see how sequence
and traverse
are invaluable for validating data. Let’s try to create a generic validator that takes a schema and validates an input object. We’ll use the Result
type, which accepts a Semigroup on the left side that allows us to collect errors. A Semigroup is similar to a Monoid and it defines a concat
method — but unlike the Monoid, it doesn’t require the presence of the empty
method. We’re also introducing the transformation function maybeToResult
below, that’ll help us interoperate between Maybe
and Result
.
很容易看出sequence
和traverse
对于验证数据的价值。 让我们尝试创建一个采用模式并验证输入对象的通用验证器。 我们将使用Result
类型,该类型在左侧接受一个Semigroup,该Semigroup允许我们收集错误。 Semigroup与Monoid相似,它定义了concat
方法-但与Monoid不同,它不需要empty
方法。 我们还将在下面引入转换函数maybeToResult
,这将帮助我们在Maybe
和Result
之间进行互操作。
import {
Result, isString, map, merge, constant, bimap, flip, propOr, identity,
toPairs, safe, maybeToResult, traverse, and, isNumber, compose
} from "crocks"
// length :: [a] -> Int
const length = x => x.length
// gte :: Number -> a -> Result String a
const gte = x => y => y >= x
// lte :: Number -> a -> Result String a
const lte = x => y => y <= x
// isValidName :: a -> Result String a
const isValidName = compose(
maybeToResult("expected a string less than 20 characters"),
safe(and(compose(lte(20), length), isString))
)
// isAdult :: a -> Result String a
const isAdult = compose(
maybeToResult("expected a value greater than 18"),
safe(and(isNumber, gte(18)))
)
/**
* schema :: Schema
* Schema :: {
* [string]: a -> Result String a
* }
* */
const schema = {
name: isValidName,
age: isAdult,
}
// makeValidator :: Schema -> Object -> Result [String] Object
const makeValidator = flip(object =>
compose(
map(constant(object)),
traverse(Result, merge((key, validator) =>
compose(
bimap(error => [`${key}: ${error}`], identity),
validator,
propOr(undefined, key)
)(object)
)
),
toPairs
)
)
// validate :: Object -> Result [String] Object
const validate = makeValidator(schema)
validate(({
name: "Car",
age: 21,
}))
//=> Ok { name: "Car", age: 21 }
validate(({
name: 7,
age: "Old",
}))
//=> Err [ "name: expected a string less than 20 characters", "age: expected a value greater than 18" ]
Since we’ve flipped the makeValidator
function to make more suitable for currying, our compose
chain receives the schema that we need to validate against first. We first break the schema into key-value Pair
s, and pass the value of each property to it’s corresponding validation function. In case the function fails, we use bimap
to map on the error, add some more information to it, and return it as a singleton Array
. traverse
will then concat
all the errors if they exist, or return the original object if it’s valid. We could have also returned a String
instead of an Array
, but an Array
feels much nicer.
由于我们已经翻转了makeValidator
函数以使其更适合于currying,因此我们的compose
链会收到我们需要首先进行验证的架构。 我们首先将模式分为键值Pair
,然后将每个属性的值传递给其相应的验证函数。 万一函数失败,我们将使用bimap
映射错误,向其添加更多信息,并将其作为单例Array
返回。 然后, traverse
将concat
所有错误(如果存在),或者如果有效则返回原始对象。 我们也可以返回String
而不是Array
,但是Array
感觉更好。
Thanks to Ian Hofmann-Hicks, Sinisa Louc and Dale Francis for their inputs on this post.
感谢Ian Hofmann-Hicks, Sinisa Louc和Dale Francis在此职位上的投入。
翻译自: https://www.freecodecamp.org/news/functional-programming-patterns-cookbook-3a0dfe2d7e0a/
函数式编程 模式