简体   繁体   中英

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.

流程图

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.

带有错误捕获的流程图

So my question is: How should we handle one or more "else if" statements in fp-ts?

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. Using alt , your first example becomes:

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" . 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.

Either also has an implementation of alt that will only run if the thing before it is a 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. 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 .

You could use O.match (or O.fold which is identical) to implement the scenario in your second flowchart:

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. 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.

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. It's also for modeling any mutually-exclusive either-or scenario. Just pass in a Left or a Right into that function. The code is so much shorter that way, and as a bonus it's an excuse to use a monad!

Like Souperman, I really like alt here and like user1713450 I like io-ts here as well. Even if the input is unknown we can define what we care about and code against that. One of the things I really like about alt is its flexibility when we need to add more conditions. Say you want to check on a new property then you just add the new alt. The getCategory function stays very readable.

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. As mentioned by cherryblossum, it goes the fold route.

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

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

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