'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?
Solution 1:[1]
One function that might be helpful is alt
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.
Solution 2:[2]
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)
)
)
Solution 3:[3]
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!
Sources
This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.
Source: Stack Overflow
Solution | Source |
---|---|
Solution 1 | Souperman |
Solution 2 | cherryblossom |
Solution 3 | user1713450 |