JavaScript函数式编程 | 函数

5

我正在使用对象数组进行函数式编程实验。

我想了解是否有更好或更清晰的方法来执行一系列基于条件更新变量的函数。像全局范围内的变量使用,这样做是否不被赞成?我是否可以将例如狗的数量传递给“updateDogsAmt”函数?等等...

是否有更好的方法可以完成这项工作?

我已经将这些函数分为更新DOM和逻辑的部分。

演示:JSFIDDLE

    const animals = [
    {name: 'Zack', type: 'dog'},
  {name: 'Mike', type: 'fish'},
  {name: 'Amy', type: 'cow'},
  {name: 'Chris', type: 'cat'},
  {name: 'Zoe', type: 'dog'},
  {name: 'Nicky', type: 'cat'},
  {name: 'Cherry', type: 'dog'}
]

let dogs = [];
function getDogs() {
    //return only dogs
  animals.map((animal) => {
    if(animal.type === "dog") {
      dogs.push(animal);
    }
  });
}
getDogs();

let dogsAmt = 0;
function getDogsAmt() {
    //get dogs amount
    dogsAmt = dogs.length;
}
getDogsAmt();

function updateDogsAmt() {
  //update dom with dogs count
  let dogsHTML = document.getElementById('dogs-amt');
  dogsHTML.innerHTML = dogsAmt;
}
updateDogsAmt();

let otherAnimals = [];
function getOtherAnimals() {
    //return other animals count 
  animals.map((animal) => {
    if(animal.type != "dog") {
      otherAnimals.push(animal);
    }
  });
}
getOtherAnimals();

let otherAnimalsAmt = 0;
function getOtherAnimalsAmt() {
    otherAnimalsAmt = otherAnimals.length;  
}
getOtherAnimalsAmt();

function updateOtherAnimalsAmt() {
    //udate dom with other animals
  let otherAmt = document.getElementById('other-amt');
  otherAmt.innerHTML = otherAnimalsAmt;
}
updateOtherAnimalsAmt();

如果你想了解什么是函数式编程,我会全心全意地建议你学习Haskell的基础知识。Haskell甚至不支持像变量和循环这样的构造,这些你可能从命令式语言中了解到,因此你在Haskell中编写的每个程序都必须是不仅是函数式的,而且是纯函数式的。 - TeWu
你在特定情况下使用“函数式编程”,意思是在数组上使用forEachmapfilter。有些人之所以称其为函数式编程,是因为你将这些方法传递给函数。或许可以称之为函数式编程,但仅仅是勉强算得上。实际上,“函数式编程”有一个更加简洁、严谨的含义。你可能需要了解这一点,并将你的问题重命名为“在JS中使用函数式数组方法”。 - user663031
@torazaburo 尽管在Haskell中FP是一个精确的术语,但这并不适用于Javascript这样的多范式语言。我认为高阶函数已经足够接近FP了。 - user6445533
2个回答

8
在函数式编程中,函数是纯函数,这意味着:
  • 它们不会引起副作用(不会修改其作用域之外的变量)
  • 给定相同的输入,它们总是产生相同的输出
您定义的函数不是纯函数,因为它们:
  • 修改其作用域之外的变量
  • 根据其作用域之外变量的状态返回不同的结果
因此,这个函数是不纯的
let dogsAmt = 0;
function getDogsAmt() {
  // do something with dogs and modify dogsAmt
}

而这个则是纯净的

function getDogsAmt(dogs) {
  // do something with dogs and return dogsAmt
}
let dogsAmt = getDogsAmt(dogs);

写作函数式风格使得代码易于重用。例如,在您的示例中,您只需要一个函数分别计算动物数量和更新DOM:

const animals = [
  {name: 'Zack',type: 'dog'},
  {name: 'Mike',type: 'fish'}, 
  {name: 'Amy', type: 'cow'},
  {name: 'Chris', type: 'cat'}
];

function getDogs(animals) {
  //return only dogs
  return animals.filter(animal => animal.type === "dog");
}

function getOtherAnimals(animals) {
  //return other animals count 
  return animals.filter(animal => animal.type !== "dog");
}

function getAmt(animals) {
  //get number of animals in the array
  return animals.length;
}

function updateHTML(id, amount) {
  //update dom
  document.getElementById(id).innerHTML = amount;
}

updateHTML('dogs-amt', getAmt(getDogs(animals)));
updateHTML('other-amt', getAmt(getOtherAnimals(animals)));
<p>There are <span id="dogs-amt">0</span> dogs</p>
<p>There are <span id="other-amt">0</span> other animals</p>

是否全部都是纯函数?

这段代码中还有一个函数不是纯函数!updateHTML接受两个参数,并且总是返回undefined。但在执行过程中,它会引起副作用:它会更新DOM!

如果你想避免手动基于不纯的函数更新DOM的问题,我建议你看一下像React、Elm、Cycle.js或Vue.js这样的库/框架——它们都使用了一个叫做虚拟DOM的概念,它可以将整个DOM表示为JS数据结构,并负责同步虚拟DOM与真实DOM。

JavaScript中的函数式编程

函数式语言(例如Haskell、Lisp、Clojure、Elm)强制你编写纯函数,并在你的背景更多地依赖于过程式或面向对象编程时引入许多令人费解的概念。在我看来,JavaScript是一个非常适合"跌跌撞撞"进入函数式编程的语言。虽然乍一看它像Java,但当你仔细看时,JS与Lisp更有共同之处。尝试理解诸如闭包和原型继承等内容而不是试图将JS写成Java对我帮助很大。(在这种情况下,一个很好的阅读材料是:JavaScript的两个支柱

对于你在JavaScript中进行函数式编程的下一步,我建议你:

  • 在你的脚本中尽可能地少使用状态(不再使用let
  • 理解并使用数组原型上的函数,例如mapfilterreduce(参见例如这篇文章)、some、any等
  • 只编写纯函数,并在无法这样做时要小心

当你更加熟悉这些内容后,你可以逐步开始攻克更高级的函数式概念(高阶函数、部分应用、柯里化、不可变性、单子、可观察/函数响应式编程)。JavaScript领域中一些有趣的库包括:


2
对我来说,函数式编程的灯泡并不是通过阅读有关非可变状态、一等、高阶、声明性语法等方面的所有内容而点亮的。面向对象编程的人通常会说:“但你可以在面向对象编程中做到所有这些。”
所以我去了学术界,但我相信其中必须有更根本的东西。我苦苦挣扎,直到有一天读到了这句话(我忘记在哪里读到的):“不要考虑细粒度,而要考虑粗粒度。”
(只是一个旁白,虽然许多函数式编程看起来很神秘,使用各种可怕的东西,如 =>、_ 等等,但它并不一定需要…它更多的是关于你的方法和思考方式)。
我以前成功地使用过这个例子:一个类里有很多迭代,在面向对象编程中你经常见到它们(这只是一个天真的例子,但你明白我的意思)。
var someArr = [ 1, 2, 3 ];
var getSum = function ( arr ) { // iterate, get sum, return }
var getProd = function ( arr ) { // iterate, get prod, return };

现在我可以做这样的事情:

var getProductTimesLength = getSum ( someArr ) * getProd ( someArr );

功能性的,就像你发布的代码。嗯……不完全是这样。它有点类似于声明式编程,但没有高阶或头等的函数使用。我们如何朝着这个方向迈进一步?

也许创建一个单一的“高阶”函数,将指令(一个函数)作为参数(“头等”函数基本上是一个作为参数的函数),并封装迭代过程...... 针对任何东西。换句话说,将迭代从细粒度使用中取出,并使其成为粗粒度的实用工具。(当然,你可以添加到数组原型中,这是另一个讨论)。

现在我可能会这样做(再次强调,这完全是天真的,你需要确保你得到一个数字数组,需要时检查零,所有这些):

// This would probably be in a util module or some such.
var reduceArray = function ( arr, func ) { 
  var redux = 0;
  for ( var each in arr ) { redux = func ( redux, arr [ each ] ); }
  return redux;
}

// This is what would appear in your dev module.
var prod = function ( a, b ) { return a * b };
var sum = function ( a, b ) { return a + b; }
var sumProd = reduceArray ( arr, sum ) * reduceArray ( arr, prod );

虽不是函数式编程的极致,但朝着正确方向迈出了一步。此外,考虑它如何改变你所测试的内容。如果需要,我可以直接将prod和sum函数作为匿名函数传递给reduceArray参数,这样我甚至不需要声明它们:

// This is starting to look more like usual "functional" examples
var sum = reduceArray ( arr, function ( a, b ) { return a + b } );
var prod = reduceArray ( arr, function ( a, b ) { return a * b } );
var sumProd = sum * prod;

Or even...

var sumProd = reduceArray ( arr, function ( a, b ) { return a + b; } )
            * reduceArray ( arr, function ( a, b ) { return a * b; } );

当然,功能派似乎喜欢将所有内容放在一个语句中:
var sumProd = reduceArray ( arr, function ( a, b ) { 
   return ( reduceArray ( arr, function ( a, b ) { return a + b } )
   * reduceArray ( arr, function ( a, b ) { return a * b } ) );
}

很多人会说这种写法更难读懂。但实际上并不是这样。你只需要换一种思路看待它,当你理解了之后,就会发现这种写法更简单(尽管有些开发者会过分使用,有些Scala代码看起来像是随机字符拼凑而成的三行代码)。
考虑到这一点,请再次查看你的代码。我试图让示例与你的代码相关,并且我相信很多人会给你提供具体的重写建议。但在你真正理解函数式思维和面向对象思维的区别之前,对我来说,所有的示例都可能无济于事。
它改变了你对几乎所有事情的思考方式:例如,在迭代中,你可能会听到一个面向对象的人坚持认为增量变量应该有一个“有意义”的名称。像这样:"for ( name in ArrayNames ) { ... }";
但如果你将迭代过程粗略化,除了"item"之外,没有其他有意义的名称。因此,你可以使用"val",甚至只用"d"或"v"(在函数式示例中经常看到)。正在被迭代的特定类型并不重要,只要它可以被迭代,并且只要你测试了指令函数即可。
(天哪,我说了什么……特定类型并不重要???他是个巫师,烧死他!!!)
再次强调,这只是一个非常幼稚的例子,但我已经成功地使用过。一旦人们理解了这一点,他们会说:“哦……但你实际上可以将那些定义的指令函数添加到util模块中,这样你就可以创建reduceArray.prod(myArray),使高阶util更加健壮……”。突然间,一切都不再只关乎类和接口。
没错。
编辑:以下是对你原来的代码进行近似函数化的快速尝试:
// Course-grain the dom manipulation, allows you to use it declaratively. 
// See that now, you could check if selector returns an elem, 
// if not, try it as a css style/class selector, and so on. 
var updateDisplayElement = function ( selector, data ) { 
    var elem = document.getElementById(selector);
    elem.innerHTML = data + ''; // make sure it's a string, whatever.
};

// Now this, much more terse etc.
var dogs = animals.filter ( function ( d ) { return animal.type === 'dog' } );
updateDisplayElement ( 'dogs-amt', dogs.length );
// Note that 'other' is just 'all - dogs'
updateDisplayElement ( 'other-amt', animals.length - dogs.length );

这并不是完美的功能,但是向正确方向迈出了一大步。


非常感谢你抽出时间写这个。 我的使命是通过这种方式编写更好、更干净的代码。一个接一个地调用每个函数让我感到不对劲,我想找到更好的方法来开发我正在做的事情。 - Filth
你提到的另一个重要观点是你如何思考问题。我的代码反映了我的思维过程。每个函数只完成一个任务,一旦完成,将该值传递给下一个函数进行处理。我想这就是我职业生涯中学到的内容,我正在尝试突破到更高层次的理解,什么才是像我的代码示例那样处理问题的最佳方法? - Filth
我删除了之前的评论,改为在我的答案中进行编辑。 - Tim Consolazio
@Tim_我相信其中一定有更根本的东西_你使用了高阶函数。这是因为FP将函数视为普通值而成为可能。这是关键点。FP试图在可能的情况下将所有类型的效果视为值。例如,Promise是异步控制流,实现为值。Promise将异步控制流视为值。其他类型将副作用(如有状态计算)视为值。将副作用视为值的副作用不再是副作用。这个领悟是我的个人“啊哈时刻”。 - user6445533

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接