Unless I'm missing a trick somewhere, there's not an easy way to widen the E in ReaderTaskEither<E, L, A>. It seems like this is a natural thing to want to do, e.g.
(sql: string) => ReaderTaskEither<{ db: DbConnection }, Error, Row[]>(msg: string) => ReaderTaskEither<{ log: LogService }, Error, string>You might want to use these two functions in the same function and export a single merged reader environment. I think something like:
export const widen = <E, L, A, F extends E>(f: (a: A) => ReaderTaskEither<E, L, A>) => (a: A): ReaderTaskEither<F, L, A> => {
return new ReaderTaskEither<F, L, A>(f(a).value)
}
Giving:
interface Environment {
db: DbConnection;
log: LogService;
}
const queryAndLog = () =>
readerTaskEither
.of<Environment, Error, object[]>([])
.chain(widen(() => query('select * from customers')))
.chain(widen((rows: object[]) => logInfo(`found ${rows.length} rows`).map(() => rows))) // have to annotate rows here to guide the inference
Is that something you would be interested having a PR for? Or is there a more elegant solution that I'm not seeing? (there normally is 馃槄 )
Ah, the simpler version works better:
export const widen = <E, L, A, F extends E>(fa: ReaderTaskEither<E, L, A>): ReaderTaskEither<F, L, A> => {
return new ReaderTaskEither<F, L, A>(fa.value)
}
const queryAndLog = () =>
readerTaskEither
.of<Environment, Error, object[]>([])
.chain(() => widen(query('select * from customers')))
.chain(rows => widen(logInfo(`found ${rows.length} rows`).map(() => rows)))
Another option
declare const query: <E extends { db: DbConnection }>(sql: string) => ReaderTaskEither<E, Error, Row[]>
declare const logInfo: <E extends { log: LogService }>(msg: string) => ReaderTaskEither<E, Error, string>
const queryAndLog = query<Environment>('select * from customers').chain(rows =>
logInfo<Environment>(`found ${rows.length} rows`).map(() => rows)
)
p.s.
If you often log after an action you may want to define a combinator
// combinator
const withLog = <E extends { log: LogService }, A>(
fa: ReaderTaskEither<E, Error, A>,
f: (a: A) => string
): ReaderTaskEither<E, Error, A> => fa.chain(a => logInfo<E>(f(a)).map(() => a))
const queryAndLog2 = withLog(query<Environment>('select * from customers'), rows => `found ${rows.length} rows`)
I'm using the following approach with overloadings:
function combine<E, A>(a: Reader<E, A>): Reader<E, [A]>;
function combine<E1, A, E2, B>(a: Reader<E1, A>, b: Reader<E2, B>): Reader<E1 & E2, [A, B]>;
function combine<E1, A, E2, B, E3, C>(
a: Reader<E1, A>,
b: Reader<E2, B>,
c: Reader<E3, C>,
): Reader<E1 & E2 & E3, [A, B, C]>;
function combine<E1, A, E2, B, E3, C, E4, D>(
a: Reader<E1, A>,
b: Reader<E2, B>,
c: Reader<E3, C>,
d: Reader<E4, D>,
): Reader<E1 & E2 & E3 & E4, [A, B, C, D]>;
function combine<E, A>(...readers: Reader<E, A>[]): Reader<E, A[]> {
return asks((env: E) => readers.map(reader => reader.run(env)));
}
This allows you to compose different environments just using structural typing:
type AEnv = {
a: string;
};
type BEnv = {
b: number;
};
const a = asks((e: AEnv) => `A${e.a}`);
const b = asks((e: BEnv) => `B${e.b}`);
const ab = combine(a, b).map(([a, b]) => `A&B: ${a} ${b}`); //Reader<AEnv & BEnv, string>
I think this could be generalized for any ADT with a Reader interface.
@gcanti Could we somehow tweak Apply.sequenceT to support such behavior? (I believe we can't)
Slightly related:
I've read quite a bit online that seems to suggest that the local function should be able to change the environment type, however fp-ts's implementation of local does not support this. Note that implementations of local that _do_ support changing the environment type do _not_ require the new environment type to be a super type of the current.
I guess what I'm saying is that while I agree that we should have a means to _widen_ the environment type, we could _also_ consider updating the local function to support _arbitrary_ (making no assumptions about sub/super types) changing of the environment type.
Thoughts? Admittedly, this is probably a separate issue.
@tbrisbane Agree, I already have const mapE = <E, E2>(f: (e: E2) => E) => <A>(fa: Reader<E, A>): Reader<E2, A> => asks(e2 => fa.run(f(e2))); which is essentially local. I don't see any reasons to restrict the env type.
Between the new definitions of local and functions like mapE above I think this issue can be closed. 馃憤