Closed waynevanson closed 4 years ago
I'd go for a combinator like
import * as TE from 'fp-ts/lib/TaskEither'
import * as L from 'logging-ts/lib/IO'
export const withLogger = (logger: L.LoggerIO<string>) => <A>(
message: (a: A) => string
): (<E>(ma: TE.TaskEither<E, A>) => TE.TaskEither<E, A>) => TE.chainFirst((a) => TE.fromIO(logger(message(a))))
Example
declare const read: (path: string) => TE.TaskEither<Error, string>
declare const parse: (content: string) => TE.TaskEither<Error, string>
declare const reverse: (content: string) => TE.TaskEither<Error, string>
declare const write: (path: string) => (content: string) => TE.TaskEither<Error, void>
import { pipe, flow } from 'fp-ts/lib/function'
export const program = pipe(read('in'), TE.chain(parse), TE.chain(reverse), TE.chain(write('out')))
import * as C from 'fp-ts/lib/Console'
const log = withLogger(C.log)
export const programWithLogging = pipe(
read('in'),
log(() => 'file accessed!'),
TE.chain(parse),
log(() => 'file contents parsed!'),
TE.chain(reverse),
log(() => 'contents reversed'),
TE.chain(write('out')),
log(() => 'file has been saved!')
)
alternatively, with the same harness, you can enrich the base operations
const readWithLog = flow(
read,
log(() => 'file accessed!')
)
const parseWithLog = flow(
parse,
log(() => 'file contents parsed!')
)
// etc...
Wow, all the little details in this implementation are genius!
I think a combinator to create the combinator you suggested would be a cool idea, so any HKT that is above IO can be lift the logger to it. Not sure if it's possible or not, but I'll check.
Thank you so much.
Here's something that is compatible with the MonadIO2
monad:
import { MonadIO2 } from "fp-ts/lib/MonadIO";
import { URIS2, Kind2 } from "fp-ts/lib/HKT";
import { ioEither as IOE } from "fp-ts";
import * as L from "logging-ts/es6/IO";
const chainFirst = <F extends URIS2>(I: MonadIO2<F>) => <E, A, B>(
ma: Kind2<F, E, A>,
f: (a: A) => Kind2<F, E, B>
) => I.chain(ma, (a) => I.map(f(a), () => a));
export const withLogger = <F extends URIS2>(I: MonadIO2<F>) => <E, A>(
logger: L.LoggerIO<string>
) => <A>(message: (a: A) => string) => (ma: Kind2<F, E, A>) =>
chainFirst(I)(ma, (a) => I.fromIO(logger(message(a))));
const teLog = withLogger(TE.taskEither)(C.log);
const ioeLog = withLogger(IOE.ioEither)(C.log);
I wouldn't mind seeing something like this added to the library, but with all the MonadIO[x]
overloads.
I wouldn't mind seeing something like this added to the library, but with all the MonadIO[x] overloads.
Would you like to send a PR?
I'd love to. Does the implementation stay the same and we just add function overloads?
Does the implementation stay the same and we just add function overloads?
Yes, though I would
message
type more generalchainFirst
export function withLogger<M>(
M: MonadIO<M>
): <B>(logger: L.LoggerIO<B>) => <A>(message: (a: A) => B) => (ma: HKT<M, A>) => HKT<M, A> {
return (logger) => (message) => (ma) => M.chain(ma, (a) => M.map(M.fromIO(logger(message(a))), () => a))
}
I've been wanting to integrate logger-ts into my toolbelt but haven't been able to get my head wrapped around using it especially with third-party loggers like pino. This looks like it could help!
One other thing I'm having trouble with is how to log the Error (the M in HKT<M, A>)? Maybe this is obvious but any pointers would be great!
@matthewpflueger Do you mean the E
in Either<E,A>
? If not, I don't understand what you mean and need some clarification.
You could use the described withLogger
function with Task
instead of TaskEither
, or IO
instead of IOEither
. This way you can have a logger like ~LoggerIO<E, A>
~ LoggerIO<Either<E, A>>
and you can handle the left and right within the logger.
Yeah, I mean the E in Either<E, A>. I'm a little confused - how would you log the error (the E)?
Let's assume we're in TaskEither<E, A>
.
@matthewpflueger
This code should be all you need.
import { URIS3, Kind3, URIS2, Kind2, URIS, Kind, HKT } from "fp-ts/lib/HKT";
import {
MonadIO3,
MonadIO2C,
MonadIO2,
MonadIO1,
MonadIO,
} from "fp-ts/lib/MonadIO";
import { LoggerIO } from "logging-ts/lib/IO";
import { task as T, either as E, console as C } from "fp-ts";
/**
* @category Combinator
*
* @since 0.3.4
*
* @example
* import { pipe } from 'fp-ts/lib/pipeable'
* import * as IO from 'fp-ts/lib/IO'
* import * as C from 'fp-ts/lib/Console'
* import { withLogger } from 'logging-ts/lib/IO'
* import { equal } from 'assert'
*
* const log = withLogger(IO.io)(C.log)
*
* const result = pipe(
* IO.of(3),
* log(n => `lifted "${n}" to the IO monad`), // n === 3
* IO.map(n => n * n),
* log(n => `squared the value, which is "${n}"`), // n === 9
* )
*
* equal(result(), 9)
*/
export function withLogger<M extends URIS3>(
M: MonadIO3<M>
): <B>(
logger: LoggerIO<B>
) => <A>(
message: (a: A) => B
) => <R, E>(ma: Kind3<M, R, E, A>) => Kind3<M, R, E, A>;
export function withLogger<M extends URIS2, E>(
M: MonadIO2C<M, E>
): <B>(
logger: LoggerIO<B>
) => <A>(message: (a: A) => B) => (ma: Kind2<M, E, A>) => Kind2<M, E, A>;
export function withLogger<M extends URIS2>(
M: MonadIO2<M>
): <B>(
logger: LoggerIO<B>
) => <A>(message: (a: A) => B) => <E>(ma: Kind2<M, E, A>) => Kind2<M, E, A>;
export function withLogger<M extends URIS>(
M: MonadIO1<M>
): <B>(
logger: LoggerIO<B>
) => <A>(message: (a: A) => B) => (ma: Kind<M, A>) => Kind<M, A>;
export function withLogger<M>(
M: MonadIO<M>
): <B>(
logger: LoggerIO<B>
) => <A>(message: (a: A) => B) => (ma: HKT<M, A>) => HKT<M, A>;
export function withLogger<M>(
M: MonadIO<M>
): <B>(
logger: LoggerIO<B>
) => <A>(message: (a: A) => B) => (ma: HKT<M, A>) => HKT<M, A> {
return (logger) => (message) => (ma) =>
M.chain(ma, (a) => M.map(M.fromIO(logger(message(a))), () => a));
}
export const logEA = withLogger(T.taskSeq)(E.fold(C.error, C.log));
Now you can do it like:
pipe(
TE.right("value"),
TE.map((a) => a),
logEA(
E.bimap(
(e) => `Error! value is "${e}".`,
(a) => `Success! The value is "${a}"`
)
),
TE.map((a) => a)
);
I'm thinking about how to compose these loggers with other functions.
Let's say we have the following scenario:
We see the pattern, each action has a logger.
What would you try to do to make it happen? I don't need code, just the name of some tools and techniques to look through.
The current though i had was to run these in parallel.
We can see that these could all be lifted into
TaskEither
, but not sure what a good signature should be.I'll be happy to write an example for the docs once I figure it out.