广东高端建站:不要害怕功能编程

2019.08.12 mf_web

175

函数式编程是编程范式的必备行家。最初归功于计算机科学学术史,函数式编程最近的复兴很大程度上归功于它在分布式系统中的实用性(也可能因为像Haskell这样的“纯粹”函数式语言很难掌握,这给了它们一定的标记)。

当系统的性能和完整性都至关重要时,通常会使用更严格的函数式编程语言 - 即,您的程序需要完全按照您的期望执行,并且需要在可以在数百或数千台联网计算机上共享其任务的环境中运行。

例如,Clojure为Akamai提供支持,Akamai是Facebook等公司使用的大规模内容交付网络,而Twitter则采用 Scala作为其性能最密集的组件,而AT&T 则将Haskell用于其网络安全系统。

广东高端建站

对于大多数前端Web开发人员来说,这些语言有着陡峭的学习曲线; 然而,更多平易近人的语言包含了函数式编程的功能,最着名的是Python,它的核心库,函数map和reduce(我们将在稍后讨论),以及诸如Fn.py等库以及JavaScript ,再次使用收集方法,但也有资料库,如Underscore.js和Bacon.js。

函数式编程

功能编程可能令人生畏

但请记住,它不仅适用于博士,数据科学家和建筑宇航员。对于我们大多数人来说,采用功能风格的真正好处是我们的程序可以分解为更小,更简单的部分,这些部分更可靠,更易于理解。如果您是使用数据的前端开发人员,特别是如果您使用D3,Raphael等格式化数据以进行可视化,那么函数式编程将成为您的库中的重要武器。

找到函数式编程的一致定义是很困难的,而且大多数文献都依赖于某些预兆语句,如“作为一等对象的函数”和“消除副作用”。以防万一不会让你的大脑陷入困境,在更理论的层面上,函数式编程通常用lambda演算来解释(有些人认为函数式编程基本上是数学) - 但你可以放松一下。从更务实的角度来看,初学者只需要理解两个概念,以便将其用于日常应用(无需演算!)。

首先,功能程序中的数据应该是不可变的,这听起来很严重但只是意味着它永远不会改变。首先,这可能看起来很奇怪(毕竟,谁需要一个永远不会改变任何东西的程序?),但实际上,你只需要创建新的数据结构而不是修改已经存在的数据结构。例如,如果您需要操作数组中的某些数据,那么您将使用更新的值创建一个新数组,而不是修改原始数组。简单!

其次,功能性程序应该是无状态的,这基本上意味着它们应该像第一次一样执行每项任务,而不知道程序执行早期可能发生或不发生的事情(你可能会说无状态程序不知道过去)。结合不变性,这有助于我们将每个功能视为在真空中运行,除了其他功能之外,对应用程序中的任何其他内容一无所知。更具体地说,这意味着您的函数将仅对作为参数传入的数据进行操作,并且永远不会依赖外部值来执行其计算。

不可变性和无状态是函数式编程的核心,对于理解很重要,但是如果它们还没有意义的话就不要担心。在文章的最后,您将熟悉这些原则,并且我保证函数式编程的美观,精确性和强大功能将使您的应用程序变成明亮,闪亮,数据咀嚼的彩虹。现在,从返回数据(或其他函数)的简单函数开始,然后组合这些基本构建块以执行更复杂的任务。

例如,假设我们有一个API响应:

var data = [
  { 
    name: "Jamestown",
    population: 2047,
    temperatures: [-34, 67, 101, 87]
  },
  {
    name: "Awesome Town",
    population: 3568,
    temperatures: [-3, 4, 9, 12]
  }
  {
    name: "Funky Town",
    population: 1000000,
    temperatures: [75, 75, 75, 75, 75]
  }];
复制

如果我们想使用图表或图表库来比较平均温度和人口规模,我们需要编写一些JavaScript,在对数据进行格式化之前对数据进行一些更改以进行可视化。我们的图形库需要一个x和y坐标数组,如下所示:

[
  [x, y],
  [x, y]
  …etc]
复制

这里x是平均温度,y是人口规模。

如果没有函数式编程(或者没有使用所谓的“命令式”),我们的程序可能如下所示:

var coords = [],
    totalTemperature = 0,
    averageTemperature = 0;for (var i=0; i < data.length; i++) {
  totalTemperature = 0;
  for (var j=0; j < data[i].temperatures.length; j++) {
    totalTemperature += data[i].temperatures[j];
  }
  averageTemperature = totalTemperature / data[i].temperatures.length;
  coords.push([averageTemperature, data[i].population]);}
复制

即使在一个人为的例子中,这已经变得难以理解。让我们看看我们是否可以做得更好。

在以函数式编程时,您总是在寻找可以抽象为函数的简单,可重复的操作。然后我们可以通过按顺序调用这些函数(也称为“组合”函数)来构建更复杂的特性 - 更多内容在一秒钟内完成。与此同时,让我们看看在将初始API响应转换为可视化库所需结构的过程中我们将采取的步骤。在基本级别,我们将对数据执行以下操作:

  • 添加列表中的每个数字,

  • 计算平均值,

  • 从对象列表中检索单个属性。

我们将为这三个基本动作中的每一个编写一个函数,然后从这些函数中编写我们的程序。函数式编程起初可能有点混乱,你可能会陷入旧的命令式习惯。为避免这种情况,以下是一些简单的基本规则,以确保您遵循最佳实践:

  1. 您的所有函数必须至少接受一个参数。

  2. 您的所有函数都必须返回数据或其他函数。

  3. 没有循环!

好的,让我们在列表中添加每个数字。记住规则,让我们确保我们的函数接受一个参数(要添加的数字数组)并返回一些数据。

function totalForArray(arr) {
  // add everything
  return total;  }
复制

到现在为止还挺好。但是,如果我们不循环访问列表中的每个项目,我们将如何访问它?跟你的新朋友问好,递归!这有点棘手,但基本上,当您使用递归时,除非满足特定条件,否则创建一个自我调用的函数 - 在这种情况下,返回一个值。只是看一个例子可能是最简单的:

// Notice we're accepting two values, the list and the current totalfunction totalForArray(currentTotal, arr) {
  currentTotal += arr[0]; 
  // Note to experienced JavaScript programmers, I'm not using Array.shift on 
  // purpose because we're treating arrays as if they are immutable.
  var remainingList = arr.slice(1);
  // This function calls itself with the remainder of the list, and the 
  // current value of the currentTotal variable
  if(remainingList.length > 0) {
    return totalForArray(currentTotal, remainingList); 
  }
  // Unless of course the list is empty, in which case we can just return
  // the currentTotal value.
  else {
    return currentTotal;
  }}
复制

需要注意的是:递归会使您的程序更具可读性,并且以函数式编程是必不可少的。但是,在某些语言(包括JavaScript)中,当您的程序在一次操作中进行大量递归调用时,您会遇到问题(在撰写本文时,“大”是Chrome中的10,000个调用,Firefox中有50,000个调用和Node.js中的11,000)。详细信息超出了本文的范围,但要点是,至少在ECMAScript 6发布之前,JavaScript不支持称为“尾递归”的东西,这是一种更有效的递归形式。这是一个高级主题,不会经常出现,但值得了解。

如果不这样做,请记住我们需要从一系列温度计算总温度,然后计算平均值。现在,temperatures我们可以简单地写一下:而不是循环遍历数组中的每个项目:

var totalTemp = totalForArray(0, temperatures);
复制

如果你是纯粹的,你可能会说我们的totalForArray功能可以进一步细分。例如,将两个数字相加的任务可能会出现在应用程序的其他部分,随后应该是它自己的函数。

function addNumbers(a, b) {
  return a + b;}
复制

现在,我们的totalForArray函数如下所示:

function totalForArray(currentTotal, arr) {
  currentTotal = addNumbers(currentTotal, arr[0]);
  var remainingArr = arr.slice(1);
  if(remainingArr.length > 0) {
    return totalForArray(currentTotal, remainingArr);
  }
  else {
    return currentTotal;
  }}
复制

优秀!从函数编程中返回单个值是相当常见的,以至于它有一个特殊的名称“简化”,你会更常听到它作为一个动词,就像你“将一个数组减少到一个值” “JavaScript有一个特殊的方法只是为了执行这个常见的任务。Mozilla开发者网络提供了完整的解释,但出于我们的目的,它就像这样简单:

// The reduce method takes a function as its first argument, and that function // accepts both the current item in the list and the current total result from // whatever calculation you're performing.var totalTemp = temperatures.reduce(function(previousValue, currentValue){
  // After this calculation is returned, the next currentValue will be 
  // previousValue + currentValue, and the next previousValue will be the 
  // next item in the array.
  return previousValue + currentValue;});
复制

但是,嘿,既然我们已经定义了一个addNumber函数,我们就可以使用它了。

var totalTemp = temperatures.reduce(addNumbers);
复制

实际上,因为总计一个数组是如此的酷,所以让我们把它放到它自己的函数中,这样我们就可以再次使用它而不必记住所有关于缩减和递归的混乱的东西。

function totalForArray(arr) {
  return arr.reduce(addNumbers);}var totalTemp = totalForArray(temperatures);
复制

啊,现在这是一些可读的代码!大家都知道,reduce大多数函数式编程语言中常见的方法都是如此。这些对数组执行操作而不是循环的辅助方法通常称为“高阶函数”。

向前移动,我们列出的第二项任务是计算平均值。这很简单。

function average(total, count) {
  return total / count;}
复制

我们怎样才能获得整个阵列的平均值?

function averageForArray(arr) {
  return average(totalForArray(arr), arr.length);}var averageTemp = averageForArray(temperatures);
复制

希望您开始了解如何组合功能以执行更复杂的任务。这是可能的,因为我们遵循本文开头列出的规则 - 即,我们的函数必须始终接受参数并返回数据。非常棒。

最后,我们想从一组对象中检索单个属性。我不会向你展示更多的递归示例,而是会切入到追逐中并使用另一种内置的JavaScript方法:map。此方法适用于具有一个结构且需要将其映射到另一个结构的数组,如下所示:

// The map method takes a single argument, the current item in the list. Check// out the link above for more complete examples.var allTemperatures = data.map(function(item) {
  return item.temperatures;});
复制

这很酷,但是从一组对象中拉出一个属性是你一直在做的事情,所以让我们为此做一个函数。

// Pass in the name of the property that you'd like to retrievefunction getItem(propertyName) {
  // Return a function that retrieves that item, but don't execute the function.
  // We'll leave that up to the method that is taking action on items in our 
  // array.
  return function(item) {
    return item[propertyName];
  }}
复制

检查一下:我们已经创建了一个返回函数的函数!现在我们可以将它传递给这样的map方法:

var temperatures = data.map(getItem('temperature'));
复制

如果您喜欢细节,我们可以这样做的原因是,在JavaScript中,函数是“一等对象”,这基本上意味着您可以像传递任何其他值一样传递函数。虽然这是许多编程语言的一个特性,但它是任何可以在功能样式中使用的语言的要求。顺便说一句,这也是你可以做的事情的原因$(‘#my-element’).on(‘click’, function(e) … )。方法中的第二个参数on是a function,当你将函数作为参数传递时,你就像使用命令式语言中的值一样使用它们。很简约。

最后,让我们将调用包装map在自己的函数中,使事情更具可读性。

function pluck(arr, propertyName) {
  return arr.map(getItem(propertyName));} var allTemperatures = pluck(data, 'temperatures');
复制

好了,现在我们有一个通用函数工具包,我们可以在我们的应用程序中的任何地方使用它,甚至在其他项目中也可以使 我们可以计算数组中的项目,获取数组的平均值,并通过从对象列表中提取属性来创建新数组。最后但并非最不重要的是,让我们回到原来的问题:

var data = [
  { 
    name: "Jamestown",
    population: 2047,
    temperatures: [-34, 67, 101, 87]
  },
  …];
复制

我们需要将像上面那样的对象数组转换为一x, y对数组,如下所示:

[
  [75, 1000000],
  …];
复制

这里x是平均温度,y是总人口。首先,让我们隔离我们需要的数据。

var populations = pluck(data, 'population');var allTemperatures = pluck(data, 'temperatures');
复制

现在,让我们制作一组平均值。请记住,我们传递给的函数map将在数组中的每个项目上调用; 因此,传递函数的返回值将被添加到新数组中,并且最终将新数组分配给我们的averageTemps变量。

var averageTemps = allTemperatures.map(averageForArray);
复制

到现在为止还挺好。但现在我们有两个数组:

// populations[2047, 3568, 1000000]// averageTemps[55.25, 5.5, 75]
复制

显然,我们只需要一个数组,所以让我们编写一个函数来组合它们。我们的功能应该确保在索引项0第一阵列中与该项目在指数配对的0第二阵列中,等了索引1,以n(其中n为数组中的项的总数)。

function combineArrays(arr1, arr2, finalArr) {
  // Just so we don't have to remember to pass an empty array as the third
  // argument when calling this function, we'll set a default.
  finalArr = finalArr || [];
  // Push the current element in each array into what we'll eventually return
  finalArr.push([arr1[0], arr2[0]]);
  var remainingArr1 = arr1.slice(1),
      remainingArr2 = arr2.slice(1);
  // If both arrays are empty, then we're done
  if(remainingArr1.length === 0 && remainingArr2.length === 0) {
    return finalArr;
  }
  else {
    // Recursion!
    return combineArrays(remainingArr1, remainingArr2, finalArr);
  }};var processed = combineArrays(averageTemps, populations);
复制

或者,因为单行很有趣:

var processed = combineArrays(pluck(data, 'temperatures').map(averageForArray), pluck(data, 'population'));// [//  [ 55.25, 2047 ],//  [ 5.5, 3568 ],//  [ 75, 1000000 ]// ]
复制

让我们变得真实

最后但并非最不重要的,让我们来看看一个更真实世界的例子,这次加入到我们的功能工具区Underscore.js,一个JavaScript库,提供了许多伟大的函数式编程帮手。我们将从一个平台上提取数据,以获取我一直在使用的名为CrisisNET的冲突和灾难信息,我们将使用梦幻般的D3库来显示这些数据。

目标是让人们到CrisisNET的主页快速了解系统中的信息类型。为了证明这一点,我们可以计算分配给特定类别的API中的文档数量,例如“物理暴力”或“武装冲突”。这样,用户可以看到他们找到的主题有多少信息可用最有趣的。

气泡图可能非常适合,因为它们通常用于表示大群人的相对大小。幸运的是,D3具有pack为此目的而命名的内置可视化。因此,让我们创建一个图表,pack其中显示给定类别名称在CrisisNET API的响应中出现的次数。

在我们继续之前,请注意D3是一个复杂的库,它保证自己的教程(或许多教程,就此而言)。因为本文主要关注函数式编程,所以我们不会花很多时间研究D3的工作原理。但不要担心 - 如果您还不熟悉该库,您应该能够复制并粘贴特定于D3的代码片段并再次深入细节。如果您有兴趣了解更多,Scott Murray的D3教程是一个很好的资源。

继续,让我们首先确保我们有一个DOM元素,以便D3有一些地方放置它将使用我们的数据生成的图表。

<div id="bubble-graph"></div>
复制

现在,让我们创建我们的图表并将其添加到DOM。

// width of chartvar diameter = 960, 
    format = d3.format(",d"),
    // creates an ordinal scale with 20 colors. See D3 docs for hex values
    color = d3.scale.category20c(),// chart object to which we'll be adding datavar bubble = d3.layout.pack()
  .sort(null)
  .size([diameter, diameter])
  .padding(1.5);// Add an SVG to the DOM that our pack object will use to draw the // visualization.var svg = d3.select("#bubble-graph").append("svg")
  .attr("width", diameter)
  .attr("height", diameter)
  .attr("class", "bubble");
复制

该pack对象采用以下格式的对象数组:

{
  children: [
    {
      className: ,
      package: "cluster",
      value: 
    }
  ]}
复制

CrisisNET的数据API以这种格式返回信息:

{
  data: [
    {
      summary: "Example summary",
      content: "Example content",
      …
      tags: [
        {
          name: "physical-violence",
          confidence: 1
        }
      ]
    }
  ]}
复制

我们看到每个文档都有一个tags属性,该属性包含一个项目数组。每个标签项都有一个name属性,这就是我们所追求的。我们需要在CrisisNET的API响应中找到每个唯一标记名称,并计算标记名称出现的次数。让我们首先使用pluck我们之前创建的函数隔离我们需要的信息。

var tagArrays = pluck(data, 'tags');
复制

这给了我们一个数组数组,如下所示:

[
  [
    {
      name: "physical-violence",
      confidence: 1
    }
  ],
  [
    {
      name: "conflict",
      confidence: 1
    }
  ]]
复制

但是,我们真正想要的是一个包含每个标签的数组。所以,让我们使用Underscore.js中一个名为flatten的便捷函数。这将从任何嵌套数组中获取值,并为我们提供深度为一级的数组。

var tags = _.flatten(tagArrays);
复制

现在,我们的数组更容易处理:

[
  {
    name: "physical-violence",
    confidence: 1
  },
  {
    name: "conflict",
    confidence: 1
  }]
复制

我们可以pluck再次使用我们真正想要的东西,这是一个只有标签名称的简单列表。

var tagNames = pluck(tags, 'name');[
  "physical-violence",
  "conflict"]
复制

啊,那更好。

现在,我们要完成相对简单的任务,即计算每个标记名称在列表中出现的次数,然后将该列表转换为pack我们之前创建的D3 布局所需的结构。正如您可能已经注意到的那样,数组在函数式编程中是一种非常流行的数据结构 - 大多数工具在设计时都考虑了数组。作为第一步,我们将创建一个这样的数组:

[
  [ "physical-violence", 10 ],
  [ "conflict", 27 ]]
复制

这里,数组中的每个项目都有索引处的标记名称0和该标记在索引处的总计数1。我们只希望每个唯一标记名称只有一个数组,所以让我们首先创建一个数组,其中每个标记名称只出现一次。幸运的是,Underscore.js方法仅用于此目的。

var tagNamesUnique = _.uniq(tagNames);
复制

让我们也摆脱任何的false-y(false,null,””等)值使用另一个方便Underscore.js功能。

tagNamesUnique = _.compact(tagNamesUnique);
复制

从这里开始,我们可以编写一个函数,使用另一个内置的JavaScript集合方法生成我们的数组,该方法名为filter,它根据条件过滤数组。

function makeArrayCount(keys, arr) {
  // for each of the unique tagNames
  return keys.map(function(key) {
    return [
      key,
      // Find all the elements in the full list of tag names that match this key
      // and count the size of the returned array.
      arr.filter(function(item) { return item === key; }).length    ]
  });}
复制

现在,我们可以pack通过映射数组列表轻松创建所需的数据结构。

var packData = makeArrayCount(tagNamesUnique, tagNames).map(function(tagArray) {
  return {
    className: tagArray[0],
    package: "cluster",
    value: tagArray[1]
  }});
复制

最后,我们可以将数据传递给D3并在SVG中生成DOM节点,每个唯一标记名称一个圆圈,相对于标记名称出现在CrisisNET的API响应中的总次数。

function setGraphData(data) {
  var node = svg.selectAll(".node")
    // Here's where we pass our data to the pack object.
    .data(bubble.nodes(data)
    .filter(function(d) { return !d.children; }))
    .enter().append("g")
    .attr("class", "node")
    .attr("transform", function(d) { return "translate(" + d.x + "," + d.y + ")"; });
  // Append a circle for each tag name.
  node.append("circle")
    .attr("r", function(d) { return d.r; })
    .style("fill", function(d) { return color(d.className); });
  // Add a label to each circle, using the tag name as the label's text
  node.append("text")
    .attr("dy", ".3em")
    .style("text-anchor", "middle")
    .style("font-size", "10px")
    .text(function(d) { return d.className } ); }
复制

综合起来,这里的setGraphData和makeArray功能方面,包括使用jQuery CrisisNET的API的调用(你需要得到一个API密钥)。我还在GitHub上发布了一个完整的例子。

function processData(dataResponse) {
  var tagNames = pluck(_.flatten(pluck(dataResponse.data, 'tags')), 'name');
  var tagNamesUnique = _.uniq(tagNames);
  var packData = makeArrayCount(tagNamesUnique, tagNames).map(function(tagArray) {
    return {
      className: tagArray[0],
      package: "cluster",
      value: tagArray[1]
    }
  });
  return packData;}function updateGraph(dataResponse) {
  setGraphData(processData(dataResponse));}var apikey = // Get an API key here: http://api.crisis.netvar dataRequest = $.get('http://api.crisis.net/item?limit=100&apikey=' + apikey);dataRequest.done( updateGraph );
复制

这是一个非常深刻的潜水,所以祝贺你坚持下去!正如我所提到的,这些概念起初可能具有挑战性,但抵制了for在你的余生中敲出循环的诱惑。

在使用函数式编程技术的几周内,您将快速构建一组简单,可重用的函数,这些函数将显着提高应用程序的可读性。此外,您将能够更快地操作数据结构,在几行代码中淘汰过去30分钟令人沮丧的调试。一旦您的数据格式正确,您将花费更多时间在有趣的部分:使可视化看起来很棒!

广东高端建站

最新案例

寒枫总监

来电咨询

400-6065-301

微信咨询

寒枫总监

TOP