Javascript:如何用更实用的模式替换嵌套的 if/else?

Javascript: How can I replace nested if/else with a more functional pattern?

以下模式在我的 React 应用程序代码库中多次重复:

const {items, loading} = this.props
const elem = loading
  ? <Spinner />
  : items.length
    ? <ListComponent />
    : <NoResults />

虽然这肯定比嵌套 actual if/else 子句更干净,但我正在尝试采用更优雅和更实用的模式。我读过有关使用 Either monad 之类的东西的信息,但是我为此所做的所有努力最终看起来更加冗长且不可重用(这个伪代码可能不起作用,因为我正在尝试记住以前的尝试):

import {either, F, isEmpty, prop} from 'ramda'
const isLoading = prop('loading')
const renderLoading = (props) => isLoading(props) ? <Spinner /> : false
const loadingOrOther = either(renderLoading, F)
const renderItems = (props) => isEmpty(props.items) ? <NoResults /> : <ListComponent />
const renderElem = either(loadingOrOther, renderItems)
const elems = renderElem(props)

我可以使用什么模式更 DRY/reusable?

谢谢!

While this is certainly cleaner than nesting actual if/else clauses

render () {
  const {items, loading} = this.props
  return loading
    ? <Spinner />
    : items.length
      ? <ListComponent items={items} />
      : <NoResults />
}

您发布了不完整的代码,因此我将填补一些空白以获得更具体的示例。

查看您的代码,我发现很难阅读条件在哪里以及 return 值在哪里。条件分散在不同缩进级别的不同行中——同样,return 值也没有视觉上的一致性。事实上,在进一步阅读程序以查看 ? 之前,return loading 中的 loading 甚至不是一个条件并不明显。在这种情况下选择要呈现的组件是 flat 决定,您的代码结构应该反映这一点。

使用 if/else 在此处生成一个 非常 可读的示例。没有嵌套,您可以看到 returned 的各种类型的组件,它们整齐地放置在相应的 return 语句旁边。这是一个简单的平面决定,带有简单的、详尽的案例分析。

我在这里强调 exhaustive 这个词,因为您至少要提供 ifelse 选择分支来做出决定,这一点很重要。对于您的情况,我们有第三种选择,因此使用了一个 else if

render () {
  const {items, loading} = this.props
  if (loading)
    return <Spinner />
  else if (items.length)
    return <ListComponent items={items} />
  else
    return <NoResults />
}

如果您查看此代码并尝试 "fix" 它是因为您在想 "embrace more elegant and functional patterns",那么您误解了 "elegant" 和 "functional".

嵌套的三元表达式一点也不优雅。函数式编程并不是要用最少的击键次数编写程序,导致程序过于简洁且难以阅读。

if/else 像我使用的那样的语句并没有减少 "functional" 因为它们涉及不同的语法。当然,它们比三元表达式更冗长,但它们的运行完全符合我们的预期,它们仍然允许我们声明函数式 behaviour – 不要让语法本身迫使你做出关于编码风格的愚蠢决定。

我同意不幸的是 if 是 JavaScript 中的 语句 而不是 表达式 ,但是这就是你被赋予的工作。你仍然能够在这样的约束下生成优雅而实用的程序。


备注

个人 认为依赖真实的价值观是很恶心的。我宁愿把你的代码写成

render () {
  const {items, loading} = this.props
  if (loading)                              // most important check
    return <Spinner />
  else if (items.length === 0)              // check of next importance
    return <NoResults />
  else                                      // otherwise, everything is OK to render normally
    return <ListComponent items={items} />
}

与您的代码相比,这不太可能吞下错误。例如,假装你的组件以某种方式具有 loading={false} items={null} 的 prop 值——你可以争辩说你的代码会优雅地显示 NoResults 组件;我认为你的组件处于非加载状态并且没有项目是错误的,我的代码会产生一个错误来反映这一点:Cannot read property 'length' of null.

这向我发出信号,表明在该组件范围之上的某处发生了更大的问题——即该组件有 loading=true 一些项目数组(空或其他) );没有其他的道具组合是可以接受的。

我认为您的问题并不是真正关于 if 语句与三元组的问题。我认为您可能正在寻找不同的数据结构,以允许您以强大的 DRY 方式对条件进行抽象。

有一些数据类型可以派上用场来抽象条件。例如,您可以使用 AnyAll 幺半群来抽象相关条件。您可以使用 EitherMaybe.

您还可以查看 Ramda 的 condwhenifElse 等函数。您已经看过求和类型。在特定情况下,这些都是强大而有用的策略。

但根据我的经验,这些策略确实在观点之外大放异彩。在视图中,我们实际上想要可视化层次结构,以便了解它们将如何呈现。所以三元组是一个很好的方法。

人们可以不同意 "functional" 的意思。有人说函数式编程是关于纯度或引用透明性的;其他人可能会说它只是 "programming with functions"。不同的社区有不同的解释。

因为 FP 对不同的人意味着不同的东西,所以我将重点关注一个特定的属性,即声明性代码。

声明式代码在一个地方定义一个算法或一个值,并且不会强制性地在单独的部分中改变或变异。声明式代码说明 是什么 ,而不是通过不同的代码路径强制性地为名称赋值。您的代码目前是声明性的,这很好!声明性代码提供保证:例如"This function definitely returns because the return statement is on the first line"。

有一种错误的观念认为三元组是嵌套的,而 if 语句是扁平的。只是格式问题。

return ( 
  condition1
    ? result1
  : condition2
    ? result2
  : condition3
    ? result3
    : otherwise
)

将条件放在单独的行中,然后嵌套响应。您可以根据需要重复多次。最后的 "else" 像任何其他结果一样缩进,但它没有条件。它可以扩展到您想要的任意数量的案例。我已经看到并编写了许多像这样的平面三元视图,我发现更容易完全遵循代码,因为路径没有分开。

您可能会争辩说 if 语句更具可读性,但我认为可读性对不同的人来说意味着不同的东西。因此,为了解决这个问题,让我们考虑一下我们强调

当我们使用三元组时,我们强调只有一种可能的方式来声明或返回某物。如果一个函数只包含表达式,我们的代码更有可能被当作公式来读,而不是公式的实现。

当我们使用 if 语句时,我们强调单独的、分离的步骤来产生输出。如果您更愿意将您的视图视为单独的步骤,那么 if 语句就有意义。如果您希望将视图视为具有基于上下文的不同表示的单个实体,那么三元和声明性代码会更好。

总而言之,您的代码已经可以正常运行了。可读性和易读性是主观的,关注你想强调的内容。不要觉得表达式中的多个条件是一种代码味道,它只是代表了你的 UI 的复杂性,解决这个问题(如果需要解决)的唯一方法是改变你的设计UI。 UI 代码可以很复杂,让您的代码诚实并代表所有潜在状态并不丢人。

求和类型和模式匹配

您可以使用求和类型和模式匹配来避免 if/else 语句。由于 Javascript 不包含这些功能,您必须自己实现它们:

const match = (...patterns) => (...cons) => o => {
  const aux = (r, i) => r !== null ? cons[i](r)
   : i + 1 in patterns ? aux(patterns[i + 1](o), i + 1)
   : null;

  return aux(patterns[0](o), 0);
};

match 需要一堆模式函数、构造函数和数据。除非有一个匹配,否则每个模式函数都会针对数据进行测试。然后使用成功模式函数的结果调用相应的构造函数,returns 最终结果。

为了 match 识别模式匹配是否不成功,模式必须实现一个简单的协议:每当模式不匹配时,函数必须 return null .如果模式匹配但对应的构造函数是空构造函数,则它必须只是 return 一个空的 Object。这是微调器案例的模式功能:

({loading}) => loading ? {} : null

由于我们使用解构赋值来模拟模式匹配,我们必须将每个模式函数包装在一个 try/catch 块中,以避免在解构过程中出现未捕获的错误。因此我们不直接调用模式函数,而是使用一个特殊的应用程序:

const tryPattern = f => x => {
  try {
    return f(x);
  } catch (_) {
    return null;
  }
};

最后,这是微调器案例的构造函数。它不接受任何参数,并且 return 是一个 JSX 微调器元素:

const Spinner = () => <Spinner />;

让我们把它们放在一起看看它是如何工作的:

// main function

const match = (...patterns) => (...cons) => x => {
  const aux = (r, i) => r !== null ? cons[i](r)
   : i + 1 in patterns ? aux(patterns[i + 1](x), i + 1)
   : null;

  return aux(patterns[0](x), 0);
};

// applicator to avoid uncaught errors during destructuring

const tryPattern = f => x => {
  try {
    return f(x);
  } catch (_) {
    return null;
  }
};

// constructors

const Spinner = () => "<Spinner />";
const NoResult = () => "<NoResult />";
const ListComponent = items => "<ListComponent items={items} />";

// sum type

const List = match(
  tryPattern(({loading}) => loading ? {} : null),
  tryPattern(({items: {length}}) => length === 0 ? {} : null),
  tryPattern(({items}) => items !== undefined ? items : null)
);

// mock data

props1 = {loading: true, items: []};
props2 = {loading: false, items: []};
props3 = {loading: false, items: ["<Item />", "<Item />", "<Item />"]};

// run...

console.log(
  List(Spinner, NoResult, ListComponent) (props1) // <Spinner />
);

console.log(
  List(Spinner, NoResult, ListComponent) (props2) // <NoResult />
);

console.log(
  List(Spinner, NoResult, ListComponent) (props3) // <ListComponent />
);

现在我们有一个 List 和类型,它具有三个可能的构造函数:SpinnerNoResultListComponent。输入(props)决定最终使用哪个构造函数。

如果 List(Spinner, NoResult, ListComponent) 对您来说仍然太费力并且您不想显式列出 List 的各个状态,您可以在 sum 类型定义期间传递构造函数:

const List = match(
  tryPattern(({loading}) => loading ? {} : null),
  tryPattern(({items: {length}}) => length === 0 ? {} : null),
  tryPattern(({items}) => items)
) (
  Spinner,
  NoResult,
  ListComponent
);

现在你可以简单地调用 List(props1) 等,以一种非常干燥的方式。

如果没有模式匹配,

match 静默 returns null。如果你想保证至少有一个模式匹配成功,你也可以抛出一个错误。

您不必为此安装额外的软件包:

content() {
  const {items, loading} = this.props
  if (loading) {
    return <Spinner />;
  }
  return items.length ? <ListComponent /> : <NoResult />;
}

render() {
  return this.content();
}

由于 Ramda 有一个 ifElse 函数,您可以使用它以可重用的无点样式编写您的条件。

可运行示例(使用字符串而不是 <Tags>,因此它可以 运行 作为堆栈片段)。

const { compose, ifElse, always, prop, isEmpty } = R;

const renderItems = ifElse(isEmpty, always('noResults'), always('listComponent'));

const renderProps = ifElse(
    prop('loading'), 
    always('spinner'), 
    compose(renderItems, prop('items'))
);

// usage: const elem = renderProps(this.props);

// test
console.log(renderProps({ loading: true, items: ['a', 'b', 'c'] }));
console.log(renderProps({ loading: false, items: [] }));
console.log(renderProps({ loading: false, items: ['a', 'b', 'c'] }));
<script src="https://cdnjs.cloudflare.com/ajax/libs/ramda/0.25.0/ramda.min.js"></script>

当然,另一种选择是使用箭头函数和条件运算符将您的条件拆分为两个函数。像上面的例子一样,这给了你一个可重用的 renderItems 函数:

const renderItems = list => list.length ? 'listComponent' : 'noResults'; 
const renderProps = props => props.loading ? 'spinner' : renderItems(props.items);

// usage: const elem = renderProps(this.props);

// test
console.log(renderProps({ loading: true, items: ['a', 'b', 'c'] }));
console.log(renderProps({ loading: false, items: [] }));
console.log(renderProps({ loading: false, items: ['a', 'b', 'c'] }));