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

时间:2017-03-23 12:37:47

标签: javascript functional-programming monads react-jsx ramda.js

以下模式在我的React应用程序代码库中重复了一遍:

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

虽然这比嵌套实际的 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)

我可以使用哪种模式更干/可重复使用?

谢谢!

5 个答案:

答案 0 :(得分:5)

  

虽然这比嵌套实际 if/else条款更清晰

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

你发布了不完整的代码,所以我填补了一些空白,以获得更具体的例子。

查看您的代码,我发现很难读取条件的位置以及返回值的位置。条件分散在各种缩进级别的各行中 - 同样,返回值也没有视觉一致性。事实上,loading中的return loading甚至不是一个条件,直到您进一步阅读该程序才能看到?。在这种情况下选择要呈现的组件是 flat 决策,并且代码的结构应该反映出来。

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

我在这里强调详尽这个词,因为您决定至少提供ifelse选择分支是很重要的。在您的情况下,我们有第三个选项,因此使用了一个else if

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

如果您查看此代码并尝试“修复”它,因为您认为“拥抱更优雅和功能性模式”,您就会误解“优雅”和“功能性”。

嵌套三元表达式没有任何优雅。功能编程不是用最少的键击来编写程序,导致程序过于简洁且难以阅读。

像我使用的那样的

if/else语句在某种程度上不那么“功能”,因为它们涉及不同的语法。当然,它们比三元表达式更冗长,但它们的运行方式正如我们所希望的那样,它们仍然允许我们声明功能行为 - 不要让语法单独强迫你做出愚蠢的决定编码风格。

我同意遗憾的是,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 某些项目数组(空或其他);没有其他道具组合是可以接受的。

答案 1 :(得分:3)

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

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

您还可以查看Ramda的condwhenifElse等功能。你已经看过总和类型了。这些都是特定背景下强大而有用的策略。

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

人们可以不同意“功能”的含义。有人说功能编程是关于纯度或参考透明度;其他人可能会说它只是“用功能编程”。不同的社区有不同的解释。

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

声明性代码在一个地方定义算法或值,并且不会强制改变或变异。声明性代码声明的内容,而不是通过不同的代码路径强制将值分配给名称。您的代码目前是声明性的,这很好!声明性代码提供保证:例如“这个函数肯定会返回,因为return语句在第一行”。

这个错误的概念是三元嵌套,而if语句是扁平的。这只是格式化问题。

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

将条件放在自己的行上,然后嵌套响应。您可以根据需要多次重复此操作。最后的“else”就像任何其他结果一样缩进,但它没有条件。它可以根据您的需要扩展到尽可能多的情况。我已经看到并用这样的许多扁平三元组写了视图,我发现更容易完全遵循代码,因为路径没有分开。

你可以认为if语句更具可读性,但我认为再次阅读对不同的人来说意味着不同的东西。所以要解开那个,让我们考虑一下我们强调什么

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

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

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

答案 2 :(得分:2)

总和类型和模式匹配

您可以使用和类型和模式匹配来避免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需要一堆模式函数,构造函数和数据。除非匹配,否则针对数据测试每个模式函数。然后使用成功模式函数的结果调用相应的构造函数,并返回最终结果。

为了使match能够识别模式匹配是否不成功,模式必须实现一个简单的协议:每当模式不匹配时,该函数必须返回null。如果模式匹配但相应的构造函数是一个无效的构造函数,则它必须只返回一个空的Object。这是微调框的模式函数:

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

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

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

最后,这是一个微调框的构造函数。它不需要参数并返回一个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的各个状态,则可以在求和类型定义期间传递构造函数:

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

现在,您只需以非常干的方式致电List(props1)等。

如果没有模式匹配,

match将以静默方式返回null。如果您希望保证至少有一个模式成功匹配,您也可以抛出错误。

答案 3 :(得分:1)

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

Runnable示例(使用字符串而不是<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'] }));

答案 4 :(得分:0)

您无需为此安装额外的软件包:

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

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