【问题标题】:Trying to refactor this code to use only immutable structures尝试重构此代码以仅使用不可变结构
【发布时间】:2018-01-17 00:05:30
【问题描述】:

我是函数式编程的学生。我仍在让自己摆脱旧的可变突变习惯。但有时我会卡住。好的,那么问题来了——假设我们有以下闭包

const bookShelf = () => {
  let books = []
  const listBooks = () => books
  const addBook = (book) => {
    books = books.concat(book)
    return function removeBook() { books = books.filter( b => b !== book ) }
  }
  return {addBook,listBooks}
}

const { addBook, listBooks } = bookShelf()
const removeMobyDick = addBook('Moby Dick')
const removeWalden = addBook('Walden')
removeWalden()
console.log(listBooks()) // ["Moby Dick"]

请注意,我有一个发生变异的对象:书籍。

我的问题是,我怎样才能重构这段代码,使书籍是不可变的,但我达到了相同的最终结果。如果需要,请随意使用像 Ramda 这样的函数库。我在这里的天真想法是以某种方式使用递归来传递书籍的新值,然后将该版本传递回去。似乎有点过头了,所以我想向这个领域更博学的人寻求帮助。

感谢您的洞察力!

【问题讨论】:

  • 首先我会将addBookremoveBook 拆分为两个函数。我认为这是一个糟糕的设计,因为要删除一本书,您必须参考删除它的函数。如果我想删除 N 本书,那么我必须 N 引用 removeBook 函数。
  • 对于addBook,返回一个包含添加的书的新数组。 removeBook 也一样

标签: javascript ecmascript-6 functional-programming immutability ramda.js


【解决方案1】:

只需在您的bookshelf 中保留book 常量即可。这当然需要每次都创建一个新的bookshelf,所以最简单的方法是让books成为函数的参数:

function bookShelf(books) {
  return {
    listBooks() { return books },
    addBook(book) { return bookShelf(books.concat([book])); }
  }
}

const empty = bookShelf([]);
const shelfWithMobyDick = empty.addBook('Moby Dick');
const shelfWithMobyDickAndWalden = shelfWithMobyDick.addBook('Walden');
console.log(shelfWithMobyDick.listBooks());

如您所见,不需要 removeBook 函数 - 您只需使用尚未包含该书的旧值。

如果您希望能够从任意书架中移除刚刚添加的书籍,您还可以返回新书架和移除器函数:

    …,
    addBook(book) {
      return {
        bookShelf: bookShelf(books.concat([book]));
        removeThis(shelf) { return bookShelf(shelf.listBooks().filter(b => b !== book)); }
      };
    }

用作

const empty = bookShelf([]);
const {bookShelf: shelfWithMobyDick, removeThis: removeMobyDick} = empty.addBook('Moby Dick');
const {bookShelf: shelfWithMobyDickAndWalden, removeThis: removeWalden} = shelfWithMobyDick.addBook('Walden');
const shelfWithWalden = removeMobyDick(shelfWithMobyDickAndWalden);
console.log(shelfWithWalden.listBooks());

【讨论】:

  • 好的,但我要如何保持白鲸和瓦尔登湖?我的意思是如果我以这种方式添加了 50 本书并想删除第 25 本书,使用上述方法我可以只删除Book25() 并且仍然有 1-24 和 26-50 本书。
  • @Babak 查看编辑。在addBook 中创建removeThis 函数确实很难看(这不仅仅是我的示例中的变量名:-D),您最好将removeBook 方法添加到每个需要将书删除的书架对象一个论点。
  • 有趣,我得研究一下。我很好奇,如果我向书架本身添加了 removeBook 方法,即 { listBooks: ..., addBook: ..., removeBook: } removeBook 在那种情况下如何工作?
  • @Babak 你会打电话给const shelfWithWalden = shelfWithMobyDickAndWalden.removeBook('Moby Dick');
  • 因此,尽管书架上还有许多其他副本,但那将返回该书架的副本而没有那本书……我明白了。问题是,我从 Redux github.com/reactjs/redux/blob/v1.0.0/src/createStore.js#L42 的旧版本中对这个示例进行了建模,其中书籍 = 听众。尽管即使是当前版本的 Redux 仍然使用这种可变方式。我明白为什么,因为它们在 reducer 之后调用的回调函数对 createStore 函数保持私有;但是,我想知道他们是否不能以不可变的方式做到这一点。
【解决方案2】:

书架类型在这里似乎并没有真正完成任何事情,所以让我们将其设为列表(数组)。

let bookshelf = [];

现在看起来您想要一种方法来生成包含新项目的列表以及从列表中删除该项目的方法。有点奇怪,但您可以通过在元组(数组)中返回两者来做到这一点:

const remove = (list, value) =>
    list.filter(x => x !== value);

const addRemovable = (list, value) =>
    [[...list, value], list => remove(list, value)];


let bookshelf = [];
let removeMobyDick;
let removeWalden;

[bookshelf, removeMobyDick] = addRemovable(bookshelf, 'Moby Dick');
[bookshelf, removeWalden] = addRemovable(bookshelf, 'Walden');

bookshelf = removeWalden(bookshelf);

console.log(bookshelf);

这看起来不太好,你可能不想写类似的东西,但它确实和你原来的东西一样。

【讨论】:

  • 我的书架系统是 Redux 的 createStore 工作方式的简化版本。书籍=听众见链接:github.com/reactjs/redux/blob/v1.0.0/src/createStore.js#L42
  • 我试图理解为什么(即使在当前版本中)他们没有用更好的函数式编程实践重写这个 createStore 函数。
  • @Babak:JavaScript 不是围绕纯函数式编程设计的语言。如果变异被很好地包含,这不是一个真正的问题,并且可以使阅读和实施变得更加简单。
  • @Babak:尽管 Redux 和 change listeners 并不是优秀设计的光辉典范。添加侦听器的整个概念是非常无效的。
猜你喜欢
  • 1970-01-01
  • 2011-03-29
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2021-01-29
  • 2010-10-04
  • 2010-11-08
  • 1970-01-01
相关资源
最近更新 更多