简体   繁体   English

如何在 fp-ts 中处理“else if”

[英]How to handle "else if" in fp-ts

A lot of times I notice I am struggling how to implement a pretty simple flow chart with multiple if-else conditions.很多时候我注意到我在努力如何实现一个非常简单的流程图,其中包含多个 if-else 条件。

流程图

This example looks too verbose and is not really scalable if more conditions are added later on:如果稍后添加更多条件,此示例看起来太冗长并且无法真正扩展:

import * as O from "fp-ts/lib/Option"

type Category = {
  id: string
  slug: string
}

const getCategory = (category: unknown, categories: Category[]) =>
  pipe(
    O.fromNullable(category),
    O.filter((c): c is Partial<Category> => typeof c === 'object'),
    O.chain((category): O.Option<Category> => {
      if (category?.id) {
        return O.fromNullable(categories.find((item) => item.id === category.id))
      }

      if (category?.slug) {
        return O.fromNullable(categories.find((item) => item.slug === category.slug))
      }

      return O.none
    }
  )
)

It even gets more complicated if you would replace the category list with calls to the database and also want to capture possible errors in an Either.left.如果您将类别列表替换为对数据库的调用,并且还想在 Either.left 中捕获可能的错误,它甚至会变得更加复杂。

带有错误捕获的流程图

So my question is: How should we handle one or more "else if" statements in fp-ts?所以我的问题是:我们应该如何处理 fp-ts 中的一个或多个“else if”语句?

One function that might be helpful isalt which specifies a thunk that produces an option if the first thing in the pipe was none, but is otherwise not run.一个可能有用的函数是alt ,它指定一个 thunk,如果pipe中的第一件事是 none 则生成一个选项,否则不运行。 Using alt , your first example becomes:使用alt ,您的第一个示例变为:

import * as O from "fp-ts/Option";
import { pipe } from "fp-ts/function";

interface Category {
  id: string;
  slug: string;
}

declare const categories: Category[];

function getCategory(category: string | null, slug: string | null) {
  const cat = O.fromNullable(category);
  const s = O.fromNullable(slug);

  return pipe(
    cat,
    O.chain((id) => O.fromNullable(categories.find((c) => c.id === id))),
    O.alt(() =>
      pipe(
        s,
        O.chain((someSlug) =>
          O.fromNullable(categories.find((c) => c.slug === someSlug))
        )
      )
    )
  );
}

Asides:旁白:

One thing I noticed is you're filtering based on if type === "object" .我注意到的一件事是您正在根据 if type === "object"进行过滤。 I'm not sure if that was to simplify what the actual code is doing, but I'd recommend using a library like io-ts for that sort of thing if you're not already.我不确定这是否是为了简化实际代码正在做的事情,但如果你还没有这样做的话,我建议你使用像io-ts这样的库。

Either also has an implementation of alt that will only run if the thing before it is a Left . Either还有一个alt的实现,只有在它之前的东西是Left时才会运行。

I also find working with fromNullable sort of a hassle and try to keep the fp-ts style parts of my code fp-ts -y with Option and Either types at the inputs and outputs.我还发现使用fromNullable有点麻烦,并尝试将我的代码fp-ts -y 的fp-ts样式部分在输入和输出处使用OptionEither类型。 Doing that might help declutter some of the logic.这样做可能有助于整理一些逻辑。

Souperman's suggestion to use alt works, but can get a little complicated once you start involving other types like Either . Souperman 建议使用alt有效,但是一旦您开始涉及其他类型(例如Either ),可能会变得有点复杂。

You could use O.match (or O.fold which is identical) to implement the scenario in your second flowchart:您可以使用O.match (或相同的O.fold )在第二个流程图中实现场景:

import * as E from "fp-ts/lib/Either"
import * as O from "fp-ts/lib/Option"
import {pipe} from "fp-ts/lib/function"

type Category = {
  id: string
  slug: string
}

// Functions to retrieve the category from the database
declare const getCategoryById: (id: string) => E.Either<Error, O.Option<Category>>
declare const getCategoryBySlug: (slug: string) => E.Either<Error, O.Option<Category>>

const getCategory = (category: unknown): E.Either<Error, O.Option<Category>> =>
  pipe(
    O.fromNullable(category),
    O.filter((c): c is Partial<Category> => typeof c === "object"),
    O.match(
      // If it's None, return Right(None)
      () => E.right(O.none),
      // If it's Some(category)...
      category =>
        // Retrieve the category from the database
        category?.id   ? getCategoryById(category.id)     :
        category?.slug ? getCategoryBySlug(category.slug) :
        // If there's no id or slug, return Right(None)
        E.right(O.none)
    )
  )

In this case, I wouldn't complicate things by trying to "force" an fp-ts solution.在这种情况下,我不会通过尝试“强制”一个 fp-ts 解决方案来使事情复杂化。 You can greatly simplify your logic by just using the ternary operator:您可以通过使用三元运算符大大简化您的逻辑:

declare const getById: (id: string) => Option<Category>
declare const getBySlug: (slug: string) => Option<Category>

const result: Option<Category> = id ? getById(id) : getBySlug(slug)

There's no need for complicated chaining of Optional stuff.不需要复杂的可选内容链接。 If you strip out your various pipe steps into short functions and then put those function names in your pipe, you'll see the logic doesn't need to be so complicated just as an excuse to use a monad.如果您将各种管道步骤剥离为短函数,然后将这些函数名称放入管道中,您会发现逻辑不需要那么复杂,只是作为使用 monad 的借口。

Although if this is truly a one or the other thing, you could also do this:尽管如果这确实是一回事,您也可以这样做:

const getter: (arg: Either<Id, Slug>) => Option<Category> = E.fold(getById, getBySlug)

Either isn't just for handling errors. Either只是用于处理错误。 It's also for modeling any mutually-exclusive either-or scenario.它还用于建模任何互斥的非此即彼的场景。 Just pass in a Left or a Right into that function.只需将 Left 或 Right 传入该函数即可。 The code is so much shorter that way, and as a bonus it's an excuse to use a monad!这样代码就短了很多,作为奖励,它是使用 monad 的借口!

Like Souperman, I really like alt here and like user1713450 I like io-ts here as well.和 Souperman 一样,我真的很喜欢这里的alt和 user1713450,我也喜欢这里的io-ts Even if the input is unknown we can define what we care about and code against that.即使输入unknown ,我们也可以定义我们关心的内容并针对它进行编码。 One of the things I really like about alt is its flexibility when we need to add more conditions.我真正喜欢alt的一件事是当我们需要添加更多条件时它的灵活性。 Say you want to check on a new property then you just add the new alt.假设您想检查一个新属性,那么您只需添加新的 alt。 The getCategory function stays very readable. getCategory function 保持非常可读。

import * as O from 'fp-ts/Option'
import {pipe} from 'fp-ts/function'
import * as t from 'io-ts'
import * as A from 'fp-ts/Array'

type Category = {
    id: string
    slug: string
  }

const PossibleCategory = t.union([
    t.partial({
        id:t.string,
        slug:t.string
    }),  
    t.undefined])

type PossibleCategory = t.TypeOf<typeof PossibleCategory>

const getCategory = (possibleCategory: PossibleCategory, categories: Category[]) => pipe(
    categoryById(possibleCategory, categories),
    O.alt(() => categoryBySlug(possibleCategory, categories))
)

const categoryById = (possibleCategory: PossibleCategory, categories: Category[]):O.Option<Category> => pipe(
    O.fromNullable(possibleCategory?.id),
    O.chain(id => pipe(categories, A.findFirst(c => c.id === id)))
)

const categoryBySlug =  (possibleCategory: PossibleCategory, categories: Category[]): O.Option<Category> => pipe(
    O.fromNullable(possibleCategory?.slug),
    O.chain(slug => pipe(categories, A.findFirst(c => c.slug === slug)))
)

The second scenario does make the getCategory function somewhat less readable.第二种情况确实使getCategory function 的可读性降低了一些。 As mentioned by cherryblossum, it goes the fold route.正如cherryblossum 所提到的,它采用fold路线。

import * as O from 'fp-ts/Option'
import {pipe, flow, identity} from 'fp-ts/function'
import * as t from 'io-ts'
import * as E from 'fp-ts/Either'

type Category = {
    id: string
    slug: string
  }

const PossibleCategory = t.union([
    t.partial({
        id:t.string,
        slug:t.string
    }),  
    t.undefined])

type PossibleCategory = t.TypeOf<typeof PossibleCategory>

type GetCategory = (x:string) => E.Either<Error, O.Option<Category>>
// placeholders for db calls
const getCategoryById:GetCategory = (x:string) => E.right(O.none)
const getCategoryBySlug:GetCategory = (x:string) => E.right(O.none)

declare const categories: Category[];

const getCategory = (possibleCategory: PossibleCategory) => pipe(
    categoryById(possibleCategory),
    E.chain( 
        O.fold(
            () => categoryBySlug(possibleCategory),
            c => E.right(O.some(c))
        )
    )
)

const categoryById = (possibleCategory: PossibleCategory) => pipe(
    O.fromNullable(possibleCategory?.id),
    O.map(
        flow(
            getCategoryById, 
            E.chainOptionK(() => new Error('id not found'))(identity),
        )
    ),
    O.sequence(E.Monad),
)

const categoryBySlug = (possibleCategory:PossibleCategory)=> pipe(
    O.fromNullable(possibleCategory?.slug),
    O.map(
        flow(
            getCategoryBySlug, 
            E.chainOptionK(() => new Error('slug not found'))(identity),
        )
    ),
    O.sequence(E.Monad)
)

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM