Fp-ts: widen reader environment

Created on 24 May 2018  路  7Comments  路  Source: gcanti/fp-ts

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.

  • db library exports a query function that is (sql: string) => ReaderTaskEither<{ db: DbConnection }, Error, Row[]>
  • logging library that exports a function like (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 馃槄 )

question

All 7 comments

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

Was this page helpful?
0 / 5 - 0 ratings

Related issues

Crashthatch picture Crashthatch  路  4Comments

mattgrande picture mattgrande  路  3Comments

gcanti picture gcanti  路  3Comments

steida picture steida  路  4Comments

josete89 picture josete89  路  3Comments