UNPKG

veffect

Version:

powerful TypeScript validation library built on the robust foundation of Effect combining exceptional type safety, high performance, and developer experience. Taking inspiration from Effect's functional principles, VEffect delivers a balanced approach tha

809 lines (743 loc) 24.4 kB
import * as Cause from "../../Cause.js" import * as Context from "../../Context.js" import * as Effect from "../../Effect.js" import * as Either from "../../Either.js" import * as Equal from "../../Equal.js" import * as Exit from "../../Exit.js" import type * as FiberId from "../../FiberId.js" import * as FiberRef from "../../FiberRef.js" import type { LazyArg } from "../../Function.js" import { constVoid, dual, pipe } from "../../Function.js" import * as Hash from "../../Hash.js" import type * as Option from "../../Option.js" import { pipeArguments } from "../../Pipeable.js" import { hasProperty } from "../../Predicate.js" import type * as Scheduler from "../../Scheduler.js" import type * as STM from "../../STM.js" import { StreamTypeId } from "../../Stream.js" import { ChannelTypeId } from "../core-stream.js" import { withFiberRuntime } from "../core.js" import { effectVariance } from "../effectable.js" import { OP_COMMIT } from "../opCodes/effect.js" import { SinkTypeId } from "../sink.js" import * as OpCodes from "./opCodes/stm.js" import * as TExitOpCodes from "./opCodes/tExit.js" import * as TryCommitOpCodes from "./opCodes/tryCommit.js" import * as Journal from "./stm/journal.js" import * as STMState from "./stm/stmState.js" import * as TExit from "./stm/tExit.js" import * as TryCommit from "./stm/tryCommit.js" import * as TxnId from "./stm/txnId.js" /** @internal */ const STMSymbolKey = "effect/STM" /** @internal */ export const STMTypeId: STM.STMTypeId = Symbol.for( STMSymbolKey ) as STM.STMTypeId /** @internal */ export type Primitive = | STMEffect | STMOnFailure | STMOnRetry | STMOnSuccess | STMProvide | STMSync | STMSucceed | STMRetry | STMFail | STMDie | STMInterrupt /** @internal */ type Op<Tag extends string, Body = {}> = STM.STM<never> & Body & { readonly _tag: OP_COMMIT readonly effect_instruction_i0: Tag } /** @internal */ interface STMEffect extends Op<OpCodes.OP_WITH_STM_RUNTIME, { readonly effect_instruction_i1: ( runtime: STMDriver<unknown, unknown, unknown> ) => STM.STM<unknown, unknown, unknown> }> {} /** @internal */ interface STMOnFailure extends Op<OpCodes.OP_ON_FAILURE, { readonly effect_instruction_i1: STM.STM<unknown, unknown, unknown> readonly effect_instruction_i2: (error: unknown) => STM.STM<unknown, unknown, unknown> }> {} /** @internal */ interface STMOnRetry extends Op<OpCodes.OP_ON_RETRY, { readonly effect_instruction_i1: STM.STM<unknown, unknown, unknown> readonly effect_instruction_i2: () => STM.STM<unknown, unknown, unknown> }> {} /** @internal */ interface STMOnSuccess extends Op<OpCodes.OP_ON_SUCCESS, { readonly effect_instruction_i1: STM.STM<unknown, unknown, unknown> readonly effect_instruction_i2: (a: unknown) => STM.STM<unknown, unknown, unknown> }> {} /** @internal */ interface STMProvide extends Op<OpCodes.OP_PROVIDE, { readonly effect_instruction_i1: STM.STM<unknown, unknown, unknown> readonly effect_instruction_i2: (context: Context.Context<unknown>) => Context.Context<unknown> }> {} /** @internal */ interface STMSync extends Op<OpCodes.OP_SYNC, { readonly effect_instruction_i1: () => unknown }> {} /** @internal */ interface STMSucceed extends Op<OpCodes.OP_SUCCEED, { readonly effect_instruction_i1: unknown }> {} /** @internal */ interface STMRetry extends Op<OpCodes.OP_RETRY, {}> {} /** @internal */ interface STMFail extends Op<OpCodes.OP_FAIL, { readonly effect_instruction_i1: LazyArg<unknown> }> {} /** @internal */ interface STMDie extends Op<OpCodes.OP_DIE, { readonly effect_instruction_i1: LazyArg<unknown> }> {} /** @internal */ interface STMInterrupt extends Op<OpCodes.OP_INTERRUPT, { readonly effect_instruction_i1: FiberId.Runtime }> {} const stmVariance = { /* c8 ignore next */ _R: (_: never) => _, /* c8 ignore next */ _E: (_: never) => _, /* c8 ignore next */ _A: (_: never) => _ } /** @internal */ class STMPrimitive implements STM.STM<any, any, any> { public _tag = OP_COMMIT public _op = OP_COMMIT public effect_instruction_i1: any = undefined public effect_instruction_i2: any = undefined; [Effect.EffectTypeId]: any; [StreamTypeId]: any; [SinkTypeId]: any; [ChannelTypeId]: any get [STMTypeId]() { return stmVariance } constructor(readonly effect_instruction_i0: Primitive["effect_instruction_i0"]) { this[Effect.EffectTypeId] = effectVariance this[StreamTypeId] = stmVariance this[SinkTypeId] = stmVariance this[ChannelTypeId] = stmVariance } [Equal.symbol](this: {}, that: unknown) { return this === that } [Hash.symbol](this: {}) { return Hash.cached(this, Hash.random(this)) } commit(this: STM.STM<any, any, any>): Effect.Effect<any, any, any> { return unsafeAtomically(this, constVoid, constVoid) } pipe() { return pipeArguments(this, arguments) } } /** @internal */ export const isSTM = (u: unknown): u is STM.STM<unknown, unknown, unknown> => hasProperty(u, STMTypeId) /** @internal */ export const commit = <A, E, R>(self: STM.STM<A, E, R>): Effect.Effect<A, E, R> => unsafeAtomically(self, constVoid, constVoid) /** @internal */ export const unsafeAtomically = <A, E, R>( self: STM.STM<A, E, R>, onDone: (exit: Exit.Exit<A, E>) => unknown, onInterrupt: LazyArg<unknown> ): Effect.Effect<A, E, R> => withFiberRuntime((state) => { const fiberId = state.id() const env = state.getFiberRef(FiberRef.currentContext) as Context.Context<R> const scheduler = state.getFiberRef(FiberRef.currentScheduler) const priority = state.getFiberRef(FiberRef.currentSchedulingPriority) const commitResult = tryCommitSync(fiberId, self, env, scheduler, priority) switch (commitResult._tag) { case TryCommitOpCodes.OP_DONE: { onDone(commitResult.exit) return commitResult.exit } case TryCommitOpCodes.OP_SUSPEND: { const txnId = TxnId.make() const state: { value: STMState.STMState<A, E> } = { value: STMState.running } const effect = Effect.async( (k: (effect: Effect.Effect<A, E, R>) => unknown): void => tryCommitAsync(fiberId, self, txnId, state, env, scheduler, priority, k) ) return Effect.uninterruptibleMask((restore) => pipe( restore(effect), Effect.catchAllCause((cause) => { let currentState = state.value if (STMState.isRunning(currentState)) { state.value = STMState.interrupted } currentState = state.value if (STMState.isDone(currentState)) { onDone(currentState.exit) return currentState.exit } onInterrupt() return Effect.failCause(cause) }) ) ) } } }) /** @internal */ const tryCommit = <A, E, R>( fiberId: FiberId.FiberId, stm: STM.STM<A, E, R>, state: { value: STMState.STMState<A, E> }, env: Context.Context<R>, scheduler: Scheduler.Scheduler, priority: number ): TryCommit.TryCommit<A, E> => { const journal: Journal.Journal = new Map() const tExit = new STMDriver(stm, journal, fiberId, env).run() const analysis = Journal.analyzeJournal(journal) if (analysis === Journal.JournalAnalysisReadWrite) { Journal.commitJournal(journal) } else if (analysis === Journal.JournalAnalysisInvalid) { throw new Error( "BUG: STM.TryCommit.tryCommit - please report an issue at https://github.com/Effect-TS/effect/issues" ) } switch (tExit._tag) { case TExitOpCodes.OP_SUCCEED: { state.value = STMState.fromTExit(tExit) return completeTodos(Exit.succeed(tExit.value), journal, scheduler, priority) } case TExitOpCodes.OP_FAIL: { state.value = STMState.fromTExit(tExit) const cause = Cause.fail(tExit.error) return completeTodos( Exit.failCause(cause), journal, scheduler, priority ) } case TExitOpCodes.OP_DIE: { state.value = STMState.fromTExit(tExit) const cause = Cause.die(tExit.defect) return completeTodos( Exit.failCause(cause), journal, scheduler, priority ) } case TExitOpCodes.OP_INTERRUPT: { state.value = STMState.fromTExit(tExit) const cause = Cause.interrupt(fiberId) return completeTodos( Exit.failCause(cause), journal, scheduler, priority ) } case TExitOpCodes.OP_RETRY: { return TryCommit.suspend(journal) } } } /** @internal */ const tryCommitSync = <A, E, R>( fiberId: FiberId.FiberId, stm: STM.STM<A, E, R>, env: Context.Context<R>, scheduler: Scheduler.Scheduler, priority: number ): TryCommit.TryCommit<A, E> => { const journal: Journal.Journal = new Map() const tExit = new STMDriver(stm, journal, fiberId, env).run() const analysis = Journal.analyzeJournal(journal) if (analysis === Journal.JournalAnalysisReadWrite && TExit.isSuccess(tExit)) { Journal.commitJournal(journal) } else if (analysis === Journal.JournalAnalysisInvalid) { throw new Error( "BUG: STM.TryCommit.tryCommitSync - please report an issue at https://github.com/Effect-TS/effect/issues" ) } switch (tExit._tag) { case TExitOpCodes.OP_SUCCEED: { return completeTodos(Exit.succeed(tExit.value), journal, scheduler, priority) } case TExitOpCodes.OP_FAIL: { const cause = Cause.fail(tExit.error) return completeTodos( Exit.failCause(cause), journal, scheduler, priority ) } case TExitOpCodes.OP_DIE: { const cause = Cause.die(tExit.defect) return completeTodos( Exit.failCause(cause), journal, scheduler, priority ) } case TExitOpCodes.OP_INTERRUPT: { const cause = Cause.interrupt(fiberId) return completeTodos( Exit.failCause(cause), journal, scheduler, priority ) } case TExitOpCodes.OP_RETRY: { return TryCommit.suspend(journal) } } } /** @internal */ const tryCommitAsync = <A, E, R>( fiberId: FiberId.FiberId, self: STM.STM<A, E, R>, txnId: TxnId.TxnId, state: { value: STMState.STMState<A, E> }, context: Context.Context<R>, scheduler: Scheduler.Scheduler, priority: number, k: (effect: Effect.Effect<A, E, R>) => unknown ) => { if (STMState.isRunning(state.value)) { const result = tryCommit(fiberId, self, state, context, scheduler, priority) switch (result._tag) { case TryCommitOpCodes.OP_DONE: { completeTryCommit(result.exit, k) break } case TryCommitOpCodes.OP_SUSPEND: { Journal.addTodo( txnId, result.journal, () => tryCommitAsync(fiberId, self, txnId, state, context, scheduler, priority, k) ) break } } } } /** @internal */ const completeTodos = <A, E>( exit: Exit.Exit<A, E>, journal: Journal.Journal, scheduler: Scheduler.Scheduler, priority: number ): TryCommit.TryCommit<A, E> => { const todos = Journal.collectTodos(journal) if (todos.size > 0) { scheduler.scheduleTask(() => Journal.execTodos(todos), priority) } return TryCommit.done(exit) } /** @internal */ const completeTryCommit = <A, E, R>( exit: Exit.Exit<A, E>, k: (effect: Effect.Effect<A, E, R>) => unknown ): void => { k(exit) } /** @internal */ type Continuation = STMOnFailure | STMOnSuccess | STMOnRetry /** @internal */ export const context = <R>(): STM.STM<Context.Context<R>, never, R> => effect<R, Context.Context<R>>((_, __, env) => env) /** @internal */ export const contextWith = <R0, R>(f: (environment: Context.Context<R0>) => R): STM.STM<R, never, R0> => map(context<R0>(), f) /** @internal */ export const contextWithSTM = <R0, A, E, R>( f: (environment: Context.Context<R0>) => STM.STM<A, E, R> ): STM.STM<A, E, R0 | R> => flatMap(context<R0>(), f) /** @internal */ export class STMDriver<in out R, out E, out A> { private contStack: Array<Continuation> = [] private env: Context.Context<unknown> constructor( readonly self: STM.STM<A, E, R>, readonly journal: Journal.Journal, readonly fiberId: FiberId.FiberId, r0: Context.Context<R> ) { this.env = r0 as Context.Context<unknown> } getEnv(): Context.Context<R> { return this.env } pushStack(cont: Continuation) { this.contStack.push(cont) } popStack() { return this.contStack.pop() } nextSuccess() { let current = this.popStack() while (current !== undefined && current.effect_instruction_i0 !== OpCodes.OP_ON_SUCCESS) { current = this.popStack() } return current } nextFailure() { let current = this.popStack() while (current !== undefined && current.effect_instruction_i0 !== OpCodes.OP_ON_FAILURE) { current = this.popStack() } return current } nextRetry() { let current = this.popStack() while (current !== undefined && current.effect_instruction_i0 !== OpCodes.OP_ON_RETRY) { current = this.popStack() } return current } run(): TExit.TExit<A, E> { let curr = this.self as Primitive | Context.Tag<any, any> | Either.Either<any, any> | Option.Option<any> | undefined let exit: TExit.TExit<unknown, unknown> | undefined = undefined while (exit === undefined && curr !== undefined) { try { const current = curr if (current) { switch (current._tag) { case "Tag": { curr = effect((_, __, env) => Context.unsafeGet(env, current)) as Primitive break } case "Left": { curr = fail(current.left) as Primitive break } case "None": { curr = fail(new Cause.NoSuchElementException()) as Primitive break } case "Right": { curr = succeed(current.right) as Primitive break } case "Some": { curr = succeed(current.value) as Primitive break } case "Commit": { switch (current.effect_instruction_i0) { case OpCodes.OP_DIE: { exit = TExit.die(current.effect_instruction_i1()) break } case OpCodes.OP_FAIL: { const cont = this.nextFailure() if (cont === undefined) { exit = TExit.fail(current.effect_instruction_i1()) } else { curr = cont.effect_instruction_i2(current.effect_instruction_i1()) as Primitive } break } case OpCodes.OP_RETRY: { const cont = this.nextRetry() if (cont === undefined) { exit = TExit.retry } else { curr = cont.effect_instruction_i2() as Primitive } break } case OpCodes.OP_INTERRUPT: { exit = TExit.interrupt(this.fiberId) break } case OpCodes.OP_WITH_STM_RUNTIME: { curr = current.effect_instruction_i1(this as STMDriver<unknown, unknown, unknown>) as Primitive break } case OpCodes.OP_ON_SUCCESS: case OpCodes.OP_ON_FAILURE: case OpCodes.OP_ON_RETRY: { this.pushStack(current) curr = current.effect_instruction_i1 as Primitive break } case OpCodes.OP_PROVIDE: { const env = this.env this.env = current.effect_instruction_i2(env) curr = pipe( current.effect_instruction_i1, ensuring(sync(() => (this.env = env))) ) as Primitive break } case OpCodes.OP_SUCCEED: { const value = current.effect_instruction_i1 const cont = this.nextSuccess() if (cont === undefined) { exit = TExit.succeed(value) } else { curr = cont.effect_instruction_i2(value) as Primitive } break } case OpCodes.OP_SYNC: { const value = current.effect_instruction_i1() const cont = this.nextSuccess() if (cont === undefined) { exit = TExit.succeed(value) } else { curr = cont.effect_instruction_i2(value) as Primitive } break } } break } } } } catch (e) { curr = die(e) as Primitive } } return exit as TExit.TExit<A, E> } } /** @internal */ export const catchAll = dual< <E, B, E1, R1>( f: (e: E) => STM.STM<B, E1, R1> ) => <A, R>( self: STM.STM<A, E, R> ) => STM.STM<B | A, E1, R1 | R>, <A, E, R, B, E1, R1>( self: STM.STM<A, E, R>, f: (e: E) => STM.STM<B, E1, R1> ) => STM.STM<B | A, E1, R1 | R> >(2, (self, f) => { const stm = new STMPrimitive(OpCodes.OP_ON_FAILURE) stm.effect_instruction_i1 = self stm.effect_instruction_i2 = f return stm }) /** @internal */ export const mapInputContext = dual< <R0, R>( f: (context: Context.Context<R0>) => Context.Context<R> ) => <A, E>( self: STM.STM<A, E, R> ) => STM.STM<A, E, R0>, <A, E, R0, R>( self: STM.STM<A, E, R>, f: (context: Context.Context<R0>) => Context.Context<R> ) => STM.STM<A, E, R0> >(2, (self, f) => { const stm = new STMPrimitive(OpCodes.OP_PROVIDE) stm.effect_instruction_i1 = self stm.effect_instruction_i2 = f return stm }) /** @internal */ export const die = (defect: unknown): STM.STM<never> => dieSync(() => defect) /** @internal */ export const dieMessage = (message: string): STM.STM<never> => dieSync(() => new Cause.RuntimeException(message)) /** @internal */ export const dieSync = (evaluate: LazyArg<unknown>): STM.STM<never> => { const stm = new STMPrimitive(OpCodes.OP_DIE) stm.effect_instruction_i1 = evaluate return stm as any } /** @internal */ export const effect = <R, A>( f: (journal: Journal.Journal, fiberId: FiberId.FiberId, environment: Context.Context<R>) => A ): STM.STM<A, never, R> => withSTMRuntime((_) => succeed(f(_.journal, _.fiberId, _.getEnv()))) /** @internal */ export const ensuring = dual< <R1, B>(finalizer: STM.STM<B, never, R1>) => <A, E, R>(self: STM.STM<A, E, R>) => STM.STM<A, E, R1 | R>, <A, E, R, R1, B>(self: STM.STM<A, E, R>, finalizer: STM.STM<B, never, R1>) => STM.STM<A, E, R1 | R> >(2, (self, finalizer) => matchSTM(self, { onFailure: (e) => zipRight(finalizer, fail(e)), onSuccess: (a) => zipRight(finalizer, succeed(a)) })) /** @internal */ export const fail = <E>(error: E): STM.STM<never, E> => failSync(() => error) /** @internal */ export const failSync = <E>(evaluate: LazyArg<E>): STM.STM<never, E> => { const stm = new STMPrimitive(OpCodes.OP_FAIL) stm.effect_instruction_i1 = evaluate return stm as any } /** @internal */ export const flatMap = dual< <A, A2, E1, R1>(f: (a: A) => STM.STM<A2, E1, R1>) => <E, R>(self: STM.STM<A, E, R>) => STM.STM<A2, E1 | E, R1 | R>, <A, E, R, A2, E1, R1>(self: STM.STM<A, E, R>, f: (a: A) => STM.STM<A2, E1, R1>) => STM.STM<A2, E1 | E, R1 | R> >(2, (self, f) => { const stm = new STMPrimitive(OpCodes.OP_ON_SUCCESS) stm.effect_instruction_i1 = self stm.effect_instruction_i2 = f return stm }) /** @internal */ export const matchSTM = dual< <E, A1, E1, R1, A, A2, E2, R2>( options: { readonly onFailure: (e: E) => STM.STM<A1, E1, R1> readonly onSuccess: (a: A) => STM.STM<A2, E2, R2> } ) => <R>(self: STM.STM<A, E, R>) => STM.STM<A1 | A2, E1 | E2, R1 | R2 | R>, <A, E, R, A1, E1, R1, A2, E2, R2>( self: STM.STM<A, E, R>, options: { readonly onFailure: (e: E) => STM.STM<A1, E1, R1> readonly onSuccess: (a: A) => STM.STM<A2, E2, R2> } ) => STM.STM<A1 | A2, E1 | E2, R1 | R2 | R> >(2, <A, E, R, A1, E1, R1, A2, E2, R2>( self: STM.STM<A, E, R>, { onFailure, onSuccess }: { readonly onFailure: (e: E) => STM.STM<A1, E1, R1> readonly onSuccess: (a: A) => STM.STM<A2, E2, R2> } ): STM.STM<A1 | A2, E1 | E2, R1 | R2 | R> => pipe( self, map(Either.right), catchAll((e) => pipe(onFailure(e), map(Either.left))), flatMap((either): STM.STM<A1 | A2, E1 | E2, R | R1 | R2> => { switch (either._tag) { case "Left": { return succeed(either.left) } case "Right": { return onSuccess(either.right) } } }) )) /** @internal */ export const withSTMRuntime = <A, E = never, R = never>( f: (runtime: STMDriver<unknown, unknown, unknown>) => STM.STM<A, E, R> ): STM.STM<A, E, R> => { const stm = new STMPrimitive(OpCodes.OP_WITH_STM_RUNTIME) stm.effect_instruction_i1 = f return stm } /** @internal */ export const interrupt: STM.STM<never> = withSTMRuntime((_) => { const stm = new STMPrimitive(OpCodes.OP_INTERRUPT) stm.effect_instruction_i1 = _.fiberId return stm as any }) /** @internal */ export const interruptAs = (fiberId: FiberId.FiberId): STM.STM<never> => { const stm = new STMPrimitive(OpCodes.OP_INTERRUPT) stm.effect_instruction_i1 = fiberId return stm as any } /** @internal */ export const map = dual< <A, B>(f: (a: A) => B) => <E, R>(self: STM.STM<A, E, R>) => STM.STM<B, E, R>, <A, E, R, B>(self: STM.STM<A, E, R>, f: (a: A) => B) => STM.STM<B, E, R> >(2, (self, f) => pipe(self, flatMap((a) => sync(() => f(a))))) /** @internal */ export const orTry = dual< <A1, E1, R1>( that: LazyArg<STM.STM<A1, E1, R1>> ) => <A, E, R>( self: STM.STM<A, E, R> ) => STM.STM<A1 | A, E1 | E, R1 | R>, <A, E, R, A1, E1, R1>( self: STM.STM<A, E, R>, that: LazyArg<STM.STM<A1, E1, R1>> ) => STM.STM<A1 | A, E1 | E, R1 | R> >(2, (self, that) => { const stm = new STMPrimitive(OpCodes.OP_ON_RETRY) stm.effect_instruction_i1 = self stm.effect_instruction_i2 = that return stm }) /** @internal */ export const retry: STM.STM<never> = new STMPrimitive(OpCodes.OP_RETRY) /** @internal */ export const succeed = <A>(value: A): STM.STM<A> => { const stm = new STMPrimitive(OpCodes.OP_SUCCEED) stm.effect_instruction_i1 = value return stm as any } /** @internal */ export const sync = <A>(evaluate: () => A): STM.STM<A> => { const stm = new STMPrimitive(OpCodes.OP_SYNC) stm.effect_instruction_i1 = evaluate return stm as any } /** @internal */ export const zip = dual< <A1, E1, R1>( that: STM.STM<A1, E1, R1> ) => <A, E, R>( self: STM.STM<A, E, R> ) => STM.STM<[A, A1], E1 | E, R1 | R>, <A, E, R, A1, E1, R1>( self: STM.STM<A, E, R>, that: STM.STM<A1, E1, R1> ) => STM.STM<[A, A1], E1 | E, R1 | R> >(2, (self, that) => pipe(self, zipWith(that, (a, a1) => [a, a1]))) /** @internal */ export const zipLeft = dual< <A1, E1, R1>(that: STM.STM<A1, E1, R1>) => <A, E, R>(self: STM.STM<A, E, R>) => STM.STM<A, E1 | E, R1 | R>, <A, E, R, A1, E1, R1>(self: STM.STM<A, E, R>, that: STM.STM<A1, E1, R1>) => STM.STM<A, E1 | E, R1 | R> >(2, (self, that) => pipe(self, flatMap((a) => pipe(that, map(() => a))))) /** @internal */ export const zipRight = dual< <A1, E1, R1>(that: STM.STM<A1, E1, R1>) => <A, E, R>(self: STM.STM<A, E, R>) => STM.STM<A1, E1 | E, R1 | R>, <A, E, R, A1, E1, R1>(self: STM.STM<A, E, R>, that: STM.STM<A1, E1, R1>) => STM.STM<A1, E1 | E, R1 | R> >(2, (self, that) => pipe(self, flatMap(() => that))) /** @internal */ export const zipWith = dual< <A1, E1, R1, A, A2>( that: STM.STM<A1, E1, R1>, f: (a: A, b: A1) => A2 ) => <E, R>( self: STM.STM<A, E, R> ) => STM.STM<A2, E1 | E, R1 | R>, <A, E, R, A1, E1, R1, A2>( self: STM.STM<A, E, R>, that: STM.STM<A1, E1, R1>, f: (a: A, b: A1) => A2 ) => STM.STM<A2, E1 | E, R1 | R> >( 3, (self, that, f) => pipe(self, flatMap((a) => pipe(that, map((b) => f(a, b))))) )