搜文章
推荐 原创 视频 Java开发 iOS开发 前端开发 JavaScript开发 Android开发 PHP开发 数据库 开发工具 Python开发 Kotlin开发 Ruby开发 .NET开发 服务器运维 开放平台 架构师 大数据 云计算 人工智能 开发语言 其它开发
Lambda在线 > AutoCFE > 【译文】不要害怕函数式编程

【译文】不要害怕函数式编程

AutoCFE 2017-11-30

函数式编程是时髦的编程范式。最初移交给计算机科学学术界,由于在分布式系统上很大的实用性, 有了函数式编程最近的复兴。 (也可能因为像Haskell这样的给他们一定的声望的函数式语言很难把握)。

当一个系统的性能和完整性都很关键的时候,严格的函数式编程语言被特别使用。即——你期望每次你的程序恰恰需要做什么并且需要在它的任务在成百上千的计算机网络的环境中操作。

例如,Clojure为Facebook等公司所使用的大量内容传送网络Akamai提供支持,而Twitter则以Scala为其最具性能密集型组件,而Haskell则被AT&T用于其网络安全系统。

这些语言对大多数前端Web开发人员有一个陡峭的学习曲线; 然而,更多的平易近人的语言结合了功能编程的特性,最突出的是Python,它的核心库,函数map和reduce(我们将讨论一下),以及诸如Fn.py的库,以及JavaScript ,再次使用收集方法,但也与像Underscore.js和Bacon.js的库。

函数式编程可能会让人望而生畏,但请记住,这不是博士,数据科学家和宇航员。对于大多数人来说,采用功能性风格的真正好处是我们的程序可以被分解成更小、更简单,更可靠和更容易理解。如果你是一个前端开发人员处理数据,特别是如果你使用D3格式化,数据可视化,拉斐尔或类似的,那么函数式编程将是你的兵器库里的重要武器。

找到一个一致的定义,函数式编程是艰难的,大多数的文献依赖预感的状态如“函数作为一类对象,”和“消除副作用。“以防不弯曲你的大脑进入海里,更多的理论水平,函数式编程通常是解释的演算(一些实际上认为,函数式编程基本上是数学)——但是你可以放松。从更加务实的角度来看,一个初学者只需要了解两个概念为了使用它为日常应用程序(不需要微积分!)。

首先,数据在函数式程序应该是不可变的,这听起来很严重,但只是意味着它永远不应该改变。起初,这可能看上去很奇怪(毕竟,谁需要一个程序不会改变什么?),但在实践中,您只需创建新的数据结构,而不是修改已经存在的。例如,如果您需要操作一些数据在数组中,然后你会创建一个新数组与更新的值,而不是修改原始数组。简单!

其次,功能程序应该是无状态的,这基本上意味着他们应该执行每个任务,好像第一次没有的知识可能会或可能不会发生在程序的执行(你可能会说,一个无状态的程序是无知的过去)。加上不变性,这有助于我们认为每个函数如果操作在真空中,幸福地无知的其他应用程序除了其他功能。在更具体的术语中,这意味着您的功能将只在数据作为参数传递,不会依赖外部值来执行他们的计算。

不变性和无状态性是函数式编程的核心和理解很重要,但是不要担心,如果他们还不很有意义。你会熟悉这些原则的文章中,我承诺,美,精度和函数式编程的力量将把应用程序变成明亮,闪闪发光,data-chomping彩虹。现在,开始简单的函数,返回数据(或其他功能),然后结合这些基本构建块来执行更复杂的任务。

例如,让我们看一个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) {
  return total;  
}

目前为止一切都很顺利。但是我们如何访问列表中的每一项如果我们不循环吗?向你的新朋友问好,递归!这是有点棘手,但基本上,当你使用递归,你创建一个函数调用本身,除非特定的条件满足,在这种情况下,返回一个值。看一个例子可能是简单的:

function totalForArray(currentTotal, arr) {
  
  currentTotal += arr[0]; 
  var remainingList = arr.slice(1);
  if(remainingList.length > 0) {
    return totalForArray(currentTotal, remainingList); 
  }
  else {
    return currentTotal;
  }
}

警告:递归将使程序更具可读性,它在功能性编程风格至关重要。然而,在一些语言(包括JavaScript),你就会遇到问题,当您的程序使得大量递归调用单个操作(在撰写本文时,“大”是在Chrome中10000个调用,在Firefox中50000个调用并且在node.js中50000个调用)。细节超出了本文的范围,但要点是,至少在ECMAScript6发布之前,JavaScript不支持所谓的“尾递归,”,是一种更有效的递归。这是一个高级主题,不会经常出现,但它是值得了解的。

请记住,我们需要从温度数组中计算总共的温度而不是计算温度平均值,我们可以简单地写这个:

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开发人员网络提供了一个完整的解释,但对我们来说就是这么简单:

var totalTemp = temperatures.reduce(function(previousValue,     currentValue){
  return previousValue + currentValue;
});

但是,嘿,因为我们已经定义了一个addNumber函数,我们可以用这个代替。

var totalTemp = temperatures.reduce(addNumbers);

事实上,因为总计数组太酷了,让我们把它变成自己的函数,这样我们可以再次使用它而不需要记住所有的令人困惑的东西关于减少和递归。

function totalForArray(arr) {
  return arr.reduce(addNumbers);
}

var totalTemp = totalForArray(temperatures);

啊,现在是一些可读的代码!你知道方法,如减少最常见的函数式编程语言。这些辅助方法对数组代替循环执行的行为通常被称为“高阶函数”。

始终,我们列出的第二个任务是计算平均。这是非常容易的。

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

我们如何得到整个数组的平均吗?

function averageForArray(arr) {
  return average(totalForArray(arr), arr.length);
}

var averageTemp = averageForArray(temperatures);

希望你开始看到如何结合函数来执行更复杂的任务。这是可能的因为我们遵守规则制定在本文的开始——即,我们的函数必须接受参数和返回数据。非常棒。

最后,我们想要获取一个属性从一个对象数组。而不是显示你更多的递归的例子,我开门见山,知道你在另一个内置的JavaScript方法:地图。这种方法是当你有一个数组和一个结构,需要将它映射到另一个结构,像这样:

var allTemperatures = data.map(function(item) {
  return item.temperatures;
});

这是很酷,但把一个属性从一个对象集合是你将会做所有的时间,所以让我们做一个函数。

function getItem(propertyName) {
  return function(item) {
    return item[propertyName];
  }
}

检查一下:我们做了一个函数,它返回一个函数!现在我们可以通过它来映射方法是这样的:

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

如果你喜欢细节,我们可以这样做的原因是,在JavaScript中,函数是“一流的对象,这基本上意味着你可以通过在功能就像任何其他值。虽然这是一个许多编程语言的特性,这是一个可以使用任何语言,要求在函数式风格。顺便说一句,这也是为什么你能做类似 $(“#my-element”).on('click', function(e) ...)。在方法的第二个参数是一个函数,当你将函数作为参数进行传递,你使用它们就像在命令式语言使用值。很整洁。

最后,让我们用调用的函数映射到使事情更加可读。

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');

现在,让我们做一个数组的平均值。记住,我们通过映射函数将数组中的每一项被称为;因此,通过函数的返回值将被添加到一个新数组,这最终将分配给新数组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) {
  finalArr = finalArr || [];

  finalArr.push([arr1[0], arr2[0]]);

  var remainingArr1 = arr1.slice(1),
      remainingArr2 = arr2.slice(1);
  if(remainingArr1.length === 0 && remainingArr2.length === 0) {
    return finalArr;
  }
  else {
    return combineArrays(remainingArr1, remainingArr2, finalArr);
  }
};

var processed = combineArrays(averageTemps, populations);

或者,因为俏皮话是有趣的:

var processed = combineArrays(pluck(data, 'temperatures').map(averageForArray), pluck(data, 'population'));

让我们得到真正的

最后,让我们来看一个真实世界的例子,这一次增加我们与强调的功能直到之后。js JavaScript库,提供了许多函数式编程的帮手。我们会把数据从一个平台的冲突和灾难信息,我一直在做CrisisNET命名,并且我们将使用神奇的D3库数据的可视化。

我们的目标是让人们来CrisisNET的主页快速快照的类型的信息系统。为了说明这一点,我们可以计算从API文档的数量分配给一个特定的类别,如“暴力”或“武装冲突。“通过这种方式,用户可以看到多少信息是他们最感兴趣的主题上可用。

泡沫图表或许是不错的选择,因为他们通常用来代表大型群体的相对大小。幸运的是,D3有一个内置的可视化包命名这一目的。所以,让我们创建一个图形与包装显示给定类别的次数的名字出现在回应CrisisNET的API。

在我们继续之前,注意,D3认股权证自己的教程是一个复杂的库(或许多教程)。由于本文重点是函数式编程,我们不会花很多时间在D3是如何工作的。不过别担心,如果你不熟悉图书馆,你应该能够复制粘贴代码片段特定D3和深入细节。斯科特•莫里的D3教程是一个伟大的资源,如果你对学习更感兴趣。

沿着,首先确保我们有一个DOM元素,这D3有地方放图生成与我们的数据。

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

现在,让我们创建一个表格并且把它添加到dom。

var diameter = 960, 
    format = d3.format(",d"),
    color = d3.scale.category20c(),

var bubble = d3.layout.pack().sort(null).size([diameter, diameter]).padding(1.5);
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
        }
      ]
    }
  ]
}

我们可以看到,每个文档都有一个标签属性,属性包含一个条目数组。每个标签项有一个名称属性,它是我们所追求的。我们需要找到每一个独特的标记名CrisisNET API的响应和计数,标记名称出现的次数。先隔离的信息我们需要用勇气我们前面创建的函数。

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
  }
]

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

var tagNames = pluck(tags, 'name');

[
  "physical-violence",
  "conflict"
]

啊,那是更好的。

现在我们的任务相对简单的计算每个标记的次数的名字出现在我们的列表,然后转换成所需的结构D3包我们前面创建的布局。正如你可能已经注意到的,数组是一个非常受欢迎的数据结构在函数式编程中,大部分的工具是设计时考虑到数组。作为第一步,然后,我们将创建一个数组:

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

这里,数组中的每一项的标记名称索引0和标签的总计数指数1。我们想要为每个独特的标记名称只有一个数组,我们首先创建一个数组中的每个标记名称只出现一次。幸运的是,一个下划线。js方法存在只是为了这个目的。

var tagNamesUnique = _.uniq(tagNames);

让我们也摆脱任何false-y(false,null,”“,等等)值使用另一个方便的下划线。js函数。

tagNamesUnique = _.compact(tagNamesUnique);

从这里,我们可以编写一个函数,生成数组使用另一个内置的JavaScript收集方法,名叫过滤器,过滤数组基于一个条件。

function makeArrayCount(keys, arr) {
  
  return keys.map(function(key) {
    return [
      key,
      arr.filter(function(item) { return item === key; }).length
    ]
  });

}

我们现在可以轻松地创建包需要的数据结构映射列表的数组。

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")
    .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 + ")"; });
  node.append("circle")
    .attr("r", function(d) { return d.r; })
    .style("fill", function(d) { return color(d.className); });

  node.append("text")
    .attr("dy", ".3em")
    .style("text-anchor", "middle")
    .style("font-size", "10px")
    .text(function(d) { return d.className } ); 
}

把它们放在一起,这是setGraphData makeArray函数上下文,包括调用CrisisNET使用jQuery 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.net
var dataRequest = $.get('http://api.crisis.net/item?limit=100&apikey=' + apikey);

dataRequest.done( updateGraph );

这是个很深入,所以祝贺坚持它!正如我提到的,这些概念可能是一个挑战,但抵制诱惑敲定为你的余生循环。

使用函数式编程技术在几周内,你会很快建立一套简单、可重用的功能,将极大地提高应用程序的可读性。加上,你可以操作数据结构更迅速,敲出过去30分钟的令人沮丧的调试几行代码。一旦你的数据格式正确,你将会花更多的时间在有趣的部分:使可视化看起来太棒了!

版权声明:本站内容全部来自于腾讯微信公众号,属第三方自助推荐收录。《【译文】不要害怕函数式编程》的版权归原作者「AutoCFE」所有,文章言论观点不代表Lambda在线的观点, Lambda在线不承担任何法律责任。如需删除可联系QQ:516101458

文章来源: 阅读原文

相关阅读

关注AutoCFE微信公众号

AutoCFE微信公众号:gh_42e83caea35a

AutoCFE

手机扫描上方二维码即可关注AutoCFE微信公众号

AutoCFE最新文章

精品公众号随机推荐