【问题标题】:Javascript Functional Programming | FunctionsJavascript 函数式编程 |功能
【发布时间】:2016-11-23 16:42:38
【问题描述】:

我正在尝试使用一组对象进行函数式编程。

我想了解是否有一种更好更简洁的方式来执行一系列函数,这些函数根据条件获取值并更新变量。

就像使用全局范围的变量一样,这是否令人不悦?无论如何我可以将狗的数量传递给“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();

【问题讨论】:

  • 如果你想了解什么是函数式编程,我会竭诚推荐你learning basics of Haskell。 Haskell 甚至不支持你可能从命令式语言中知道的变量和循环等结构,所以你用 Haskell 编写的每个程序都必然不仅是函数式的,而且是纯函数式的。
  • 您在特定意义上使用“函数式编程”,意思是“在数组上使用forEachmapfilter”。有些人称其为函数式编程,因为您将这些方法传递给函数。它也许可以称为函数式编程,但只是勉强。 “函数式编程”实际上有更简洁、严谨的含义。您可能想知道这一点,并可以将您的问题重命名为“在 JS 中使用函数式数组方法”。
  • @torazaburo 虽然 Haskell 中的 FP 是一个准确的术语,但它不适用于 Javascript 等多范式语言。我想说高阶函数已经足够 FP 了。

标签: javascript functional-programming


【解决方案1】:

在函数式编程中,函数是的,这意味着:

  • 它们不会引起副作用(它们不会修改其范围之外的变量)
  • 给定相同的输入,它们总是产生相同的输出

你定义的函数不是纯函数,因为它们

  • 修改范围之外的变量
  • 返回不同的结果,具体取决于变量范围之外的状态

所以这个函数是不纯的

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!

如果您想解决这个基于 impure 函数手动更新 DOM 的问题,我建议您查看 React、Elm、Cycle.js 或 Vue.js 等库/框架 - 它们所有这些都使用了一个名为virtual DOM的概念,它允许将整个 DOM 表示为 JS 数据结构,并为您负责将虚拟 DOM 与真实 DOM 同步。

JavaScript 中的函数式编程

函数式语言(例如 Haskell、Lisp、Clojure、Elm)迫使您编写纯函数并引入许多令人费解的概念,而您的背景则更多地属于过程或面向对象的编程。在我看来,JavaScript 是一种“偶然发现”函数式编程的优秀语言。乍一看,它看起来像 Java,但仔细一看,JS 与 Lisp 的共同点要多得多。尝试理解闭包和原型继承之类的东西,而不是尝试像 Java 一样编写 JS,这对我有很大帮助。 (在这种情况下阅读很棒:The Two Pillars of JavaScript

对于函数式 JavaScript 的后续步骤,我建议

  • 尽可能在脚本中使用less state(不再使用let
  • 了解和使用 Array 原型上的函数,如 ma​​pfilterreduce (see e.g. this article)、some、any、.. .
  • 只写纯函数,不能写的时候要小心

在您对此感到更加熟悉之后,您可以开始一点一点地处理更高级的函数概念(高阶函数、部分应用程序、柯里化、不变性、monad、可观察对象/函数式反应式编程)。 JavaScriptLand 中一些有趣的库是

【讨论】:

    【解决方案2】:

    对我来说,函数式编程的灯泡并没有通过阅读所有关于非可变状态、第一类、高阶、声明性语法以及所有这些来继续。 OOP 的人通常会说,“但你可以在 OOP 中完成所有这些”。

    所以我得到了学术界,但我相信它必须有更基础的东西。我挣扎着,有一天读到这句话(我忘了在哪里)“而不是细粒度的,想想当然是细粒度的。”

    (顺便说一句,虽然很多函数式编程看起来很神秘,并且使用了各种可怕的东西,比如 =>、_ 等等,但它不必...更多的是关于你如何处理和思考)。

    我之前已经成功地用过这个例子:一个有一大堆迭代的类,你总是在 OOP 中看到它们(这是一个幼稚的例子,但你明白了):

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

    功能对吗?有点像您发布的代码。嗯……不是真的。这是一种声明性的,但没有更高阶或一流的功能使用。我们如何才能朝着这个方向迈出一步?

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

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

    // 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 函数作为 anon 函数直接在 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;
    

    甚至……

    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 看起来像是在屏幕上抛出的三行随机字符)。

    考虑到这一点,请再次查看您的代码。我试图使示例与您的代码相关,并且我相信很多人会为您提供特定的重写。但是,在您了解功能性思维实际上与 OOP 思维有何不同之前,无论如何,对我来说,世界上所有的例子都可能无济于事。

    它改变了您对几乎所有事情的看法:例如,在一次迭代中,您可能会听到一个 OOP 人员坚持增量变量应该有一个“有意义的”名称。类似于 "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 );
    

    再次不是功能性的必杀技,而是朝着正确方向的飞跃。

    【讨论】:

    • 非常感谢您花时间写这篇文章。我想我的任务是用它编写更好、更简洁的代码。一个接一个地调用每个函数并不适合我,我想有一种更好的方法来开发我正在做的事情。
    • 您提到的另一个要点是您的想法。我的代码表明了我的思考过程。让每个函数只执行一项任务,一旦该任务完成,将值传递给下一个函数以执行此操作。我想这就是我在整个职业生涯中被教导的方式,我正在努力突破下一个理解层次,真正处理像我的代码示例这样的情况的最佳方法是什么?
    • 删除了我以前的评论,支持对我的回答进行编辑。
    • @Tim 我相信必须有一些更基本的东西你使用高阶函数。它们是可能的,因为 FP 将函数视为正常值。这就是重点。 FP 尝试将各种效果视为值 - 只要有可能。例如,Promise 是一个异步控制流,具体化为一个值。 Promises 将异步控制流视为值。其他类型将副作用(如有状态计算)视为值。被视为价值的副作用不再是副作用。这种洞察力是我个人的“啊哈时刻”。