在 fp-ts 的管道中混合 Either 和 TaskEither

Mixing Either and TaskEither in a pipe in fp-ts

我有以下程序,当 none 函数是异步时,它可以正常工作。

interface Product {
  count: number
  pricePerItem: number
}

interface Tax {
  tax: number
}

interface Delivery {
  delivery: number
}

interface PTD { //ProductTaxDelivery
  p: Product
  t: number
  d: number
}

function getProduct(): Either<Error, Product> {
  return E.right({ count: 10, pricePerItem: 5 })
}

function getTax(p: Product): Either<Error, number> {
  return E.right(p.pricePerItem * p.count * 0.085)
}

function getDelivery(p: Product): Either<Error, number> {
  return  E.right(p.count * 0.05)
  //or maybe return E.left(Error('some error in delivery happened'))
}
function run(): Either<Error, PTD> {
  return pipe(
    E.Do,
    E.bind('p', getProduct),
    E.bind('tax', ({p}) => getTax(p)),
    E.bind('delivery', ({p}) => getDelivery(p)),    
    E.map(({ p, tax, delivery }) => ({ p, t: tax, d: delivery }))
  )
}
function main() {
  pipe(
    run(),
    E.fold(
      (e) => {
        console.log(`error: ${e}`)
      },
      (it) => {
        console.log(`ok ${it.p.count} ${it.p.pricePerItem} ${it.t} ${it.d}`)
      }
    )
  )
}

main()

我遇到的问题是,如果我的某个函数(例如 getDelivery())是异步的,那么我不确定如何解决它。

这是我尝试过的方法:

function getDelivery(p: Product): TaskEither<Error, number> {
  return TE.right(p.count * 0.05)
}

TE.bind('delivery', ({p}) => getDelivery(p)),

和许多其他变体,但都以编译器错误告终。

命令式风格的等价物是这样的:

const getDelivery = async (p: Product) => {
   return await something()
}

const run = async (): PTD => {
   const product = getProduct()
   const tax = getTax(product)
   const delivery = await getDelivery(product)
   
   return {
      p: product, t: tax, d: delivery
   }
}

使用 fp-ts 的正确功能方式(我认为涉及 EitherTaskEither)是什么?

Update:我还尝试用 TaskEither 替换 Either,到处都用 TE 替换 E,但现在的问题是当我尝试 fold in main()。这是替换的代码:

function getProduct(): TaskEither<Error, Product> {
  return TE.right({ count: 10, pricePerItem: 5 })
}

function getTax(p: Product): TaskEither<Error, number> {
  return TE.right(p.pricePerItem * p.count * 0.085)
}

function getDelivery(p: Product): TaskEither<Error, number> {
  return TE.right(p.count * 0.05)
}

function run(): TaskEither<Error, PTD> {
  return pipe(
    TE.Do,
    TE.bind('p', getProduct),
    TE.bind('tax', ({ p }) => getTax(p)),
    TE.bind('delivery', ({ p }) => getDelivery(p)),
    TE.map(({ p, tax, delivery }) => ({ p, t: tax, d: delivery }))
  )
}

function main() {
  pipe(
    run(),
    TE.fold(
      (e) => { 
        console.log(`error: ${e}`)
      },
      (it) => {
        console.log(`ok ${it.p.count} ${it.p.pricePerItem} ${it.t} ${it.d}`)
        //doNonFunctional()
      }
    )
  )
}

main()

(e) => { 一致,编译器错误显示:

error TS2345: Argument of type '(e: Error) => void' is not assignable to parameter of type '(e: Error) => Task<unknown>'.
  Type 'void' is not assignable to type 'Task<unknown>'.

更新 2 好的,所以我得到了要编译的代码,但是程序运行时没有输出

const printError = (e: Error): T.Task<unknown> => {
  console.log(`error: ${e}`)
  return () => Promise.resolve()
}

const printPTD = (ptd: PTD): T.Task<unknown> => {
  console.log(`ok ${ptd.p.count} ${ptd.p.pricePerItem} ${ptd.t} ${ptd.d}`)
  return () => Promise.resolve()
}

function run(): TaskEither<Error, PTD> {
  return pipe(
    TE.Do,
    TE.bind('p', getProduct),
    TE.bind('tax', ({ p }) => getTax(p)),
    TE.bind('delivery', ({ p }) => getDelivery(p)),
    TE.map(({ p, tax, delivery }) => ({ p, t: tax, d: delivery }))
  )
}

function main() {
  pipe(
    run(),
    TE.fold(
      (e) => printError(e),
      (ptd) => printPTD(ptd)      
    )
  )
}

main()

问题是当您在 main 中使用 pipe 创建一个 Task 时,您实际上 运行 什么都不是。

Task 是这样定义的:

interface Task<A> {
  (): Promise<A>
}

// same as type <A> = () => Promise<A>

因为Task是一个thunk,你需要调用它来实际执行代码。

async function main(): Promise<void> {
  await pipe(
    // ...
// vv note the call here
  )()
}

main()

不过,我会这样做:

const main: T.Task<void> = pipe(/* ... */)

main()

同样,run 不需要是一个函数;可以是 const run = pipe(/* ... */).

此外,还有一个 Console 模块提供 return 和 IO(副作用操作类型)的日志功能。

你的代码可以写成

import * as Console from 'fp-ts/Console'
import * as E from 'fp-ts/Either'
import * as T from 'fp-ts/Task'
import * as TE from 'fp-ts/TaskEither'
import {pipe} from 'fp-ts/function'

// <A>(a: A) => Task<void>
const taskLog = T.fromIOK(Console.log)

// You can still keep getProduct and getTask synchronous
function getProduct(): E.Either<Error, Product> { /* ... */ }
function getTax(p: Product): E.Either<Error, number> { /* ... */ }

function getDelivery(p: Product): TE.TaskEither<Error, number> { /* ... */ }

const run: TE.TaskEither<Error, PTD> = pipe(
  TE.Do,
  // See below for what TE.fromEither(K) does
  TE.bind('p', TE.fromEitherK(getProduct)),
  TE.bind('tax', ({p}) => TE.fromEither(getTax(p))),
  TE.bind('delivery', ({p}) => getDelivery(p)),
  TE.map(({p, tax, delivery}) => ({p, t: tax, d: delivery}))
)

const main: T.Task<void> = pipe(
  run,
  TE.fold(
    e => taskLog(`error: ${e}`),
    it => taskLog(`ok ${it.p.count} ${it.p.pricePerItem} ${it.t} ${it.d}`)
  )
)

main().catch(console.error)

TE.fromEitherEither 转换为 TaskEither:

export declare const fromEither: NaturalTransformation22<'Either', 'TaskEither'>
// same as
export declare const fromEither: <E, A>(fa: Either<E, A>) => TaskEither<E, A>

TE.fromEitherKfromEither 相同,但对于函数:

export declare const fromEitherK: <E, A extends readonly unknown[], B>(f: (...a: A) => Either<E, B>) => (...a: A) => TaskEither<E, B>

你现在大概可以猜到 T.fromIOK(用于 taskLog)的作用:

export declare const fromIOK: <A, B>(f: (...a: A) => IO<B>) => (...a: A) => Task<B>

Here's a CodeSandbox with the full code.