函数式编程 模式_函数式编程模式:食谱

函数式编程 模式

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类型的其他示例包括MaybeEitherAsync等。 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提供safesafeAftersafeLift功能,使我们能够使用更多的可预见的执行代码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 isStringpredicate.

我们的uncamelize 🐪函数是使用safeLift(isString)执行的,这意味着它仅在输入为isString谓词返回true时才执行。

In addition to this, crocks also provides the prop and propPath helpers which allow you to pick properties from Objects and Arrays.

除此之外, propPath还提供了proppropPath帮助器,使您可以从ObjectArray选择属性。

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 propsatisfies the predicate. The pipeline above guarantees that the last mapwhich contains the toFixed will only be called when bankBalance is a Number.

我们将prop函数的结果传递给我们的safe(isNumber)函数,该函数还会返回一个Maybe ,这取决于prop的结果是否满足谓词。 上面的管道保证只有在bankBalanceNumber时才调用包含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 Maybes, 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 aand 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 maps 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 bare valid or a Nothing otherwise.

我们首先将咖喱化的add函数提升为Maybe ,然后将Maybe aMaybe b应用于它。 到目前为止,我们一直在使用map来访问容器内的值,而ap也不ap 。 在内部,它将s mapsafeNumber(a)以访问a并将其应用于add 。 这导致Maybe包含部分应用的add 。 我们使用safeNumber(b)重复相同的过程以执行add功能,如果ab均有效,则结果为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还为我们提供了liftA2liftN帮助者,以无意义的方式表达了相同的概念。 一个简单的示例如下:

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 Asyncflows. 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以及辅助对象mconcatmreducemconcatMapmreduceMap

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.

mconcatmreduce方法采用Monoid和要使用的元素列表,并将concat应用于所有元素。 它们之间的唯一区别是mconcat返回Monoid的实例,而mreduce返回原始值。 mconcatMapmreduceMap辅助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为我们提供了两种方法来实现这一目标。 第一种模式利用产品类型PairTuple 。 让我们看一个小示例,其中有一个看起来像这样的对象:

{ 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 Arrayof objects based on their ids. 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和朋友来确保我们始终使用期望的类型。 但是,当我们使用包含其他值的类型(例如, ArrayList时会发生什么? 让我们看一个简单的函数,该函数为我们提供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 sumfunction 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 maps over the inner type and returns the contents wrapped in an Identity container. For List and Array, sequenceuses 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容器中的内容。 对于ListArraysequence使用apconcat在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 -> mba -> 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 sequenceoperator 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推断内部类型,因此必须显式提供类型构造函数作为traversesequence的第一个参数。

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.

很容易看出sequencetraverse对于验证数据的价值。 让我们尝试创建一个采用模式并验证输入对象的通用验证器。 我们将使用Result类型,该类型在左侧接受一个Semigroup,该Semigroup允许我们收集错误。 Semigroup与Monoid相似,它定义了concat方法-但与Monoid不同,它不需要empty方法。 我们还将在下面引入转换函数maybeToResult ,这将帮助我们在MaybeResult之间进行互操作。

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 Pairs, 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 Arrayfeels much nicer.

由于我们已经翻转了makeValidator函数以使其更适合于currying,因此我们的compose链会收到我们需要首先进行验证的架构。 我们首先将模式分为键值Pair ,然后将每个属性的值传递给其相应的验证函数。 万一函数失败,我们将使用bimap映射错误,向其添加更多信息,并将其作为单例Array返回。 然后, traverseconcat所有错误(如果存在),或者如果有效则返回原始对象。 我们也可以返回String而不是Array ,但是Array感觉更好。

Thanks to Ian Hofmann-Hicks, Sinisa Louc and Dale Francis for their inputs on this post.

感谢Ian Hofmann-Hicks, Sinisa LoucDale Francis在此职位上的投入。

翻译自: https://www.freecodecamp.org/news/functional-programming-patterns-cookbook-3a0dfe2d7e0a/

函数式编程 模式

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值