flix
flix copied to clipboard
Add `Eff.Logger` effect
Expand the following to a real implementation:
pub eff Logger {
// TODO: DOC:
// Note: The msg is lazy. We could also use Lazy[String] ?
pub def log(s: Logger.Severity, msg: Unit -> String): Unit
}
mod Logger {
pub enum Severity {
case Fatal
case Warn
case Info
case Debug
case Trace
}
// TODO: Add convenience methods for each severity, e.g.:
pub def fatal(msg: a): Unit \ Logger with ToString[a] = do Logger.log(Severity.Fatal, () -> "${msg}")
// TODO: Add convenience methods for each severity, e.g.:
pub def lazyFatal(msg: Unit -> a): Unit \ Logger with ToString[a] = do Logger.log(Severity.Fatal, () -> "${msg()}")
// TODO: DOC
pub def ioHandler(f: Unit -> a \ ef + Logger): a \ ef + IO =
Logger.ioHandlerWithSeverity(Severity.Trace, f)
// TODO: DOC
pub def toListHandler(f: Unit -> a \ ef + Logger): (a, List[String]) \ ef + IO =
// TODO: Here we would use a MutList to collect the formatted messages and then return them.
// A question is whether instead of String, we should have some LogMessage data structure?
checked_ecast(???)
// TODO: pub def toCollectable() similiar to toListHandler, but would use the Collectable trait.
// TODO: An IO handler which prints all messages at least `s` severity to stderr.
// We could start with output like:
// [2014-07-02 20:52:39] [DEBUG] - This is a debug message
pub def ioHandlerWithSeverity(s: Severity, f: Unit -> a \ ef + Logger): a \ ef + IO =
checked_ecast(???)
}
We shall probably have to do a few iterations. The goal is not to have a "once since fits all", but to have a Logger
that works 90% of the time. We can always add an AdvancedLogger
later.
The key ingredients of the above design are:
- Use of a
Severity
which can be filtered on. -
Logger
only has a single operation, but the companion module offers lots of helpers. Programmers are intended to writeLogger.info
for example. - Use of
ToString
. - Use of
laziness
viaUnit -> a
. We may also consider whetherLazy[a]
is better. - Various standard handlers, e.g. to print to stderr or to collect into a list.
Things missing or deferred:
- Tracking source locations.
- No
isDebugEnabled
(I think these are problematic). - No
Markers
-- are they even used?
In any case, the above is intended as sketch for what we could do.
@stephentetley Would you be interested in working on the above?
Thanks Magnus, yes I'll take a look at this.
Considering ioHandlerWithSeverity
- do you want each message timestamped or just the start of the "run" of the collected messages when printed to the console?
Considering
ioHandlerWithSeverity
- do you want each message timestamped or just the start of the "run" of the collected messages when printed to the console?
I think each message. The handler is called every time someone calls e.g. warn
but there could be a big time gap between those calls.
I updated the documentation here a bit: https://doc.flix.dev/effects-and-handlers.html
I'm getting an error trying to implement the list handler for logger, below a simplified version (no thunks / laziness or severity levels). Is the error expected - i.e. is it falling foul of the current restriction not to combine IO and user defined effects?
mod Handler.Logger {
eff Logger {
pub def log(msg: String): Unit
}
pub def toListHandler(f: Unit -> a \ ef + Logger): (a, List[String]) \ ef + IO = region rc {
let l = MutList.new(rc);
let ans = try f() with Logger {
def log(msg, k) = {
MutList.push!(msg, l);
k()
}
};
let xs = MutList.toList(l);
(ans, xs)
}
pub def logExample(): Int32 \ Logger = {
do Logger.log("System exception");
-1
}
}
pub def main(): Unit \ IO =
let (a, xs) = Handler.Logger.toListHandler(_ -> Handler.Logger.logExample());
println("Trace:");
List.forEach(println, xs);
println("Answer:");
println(a)
The error message is:
> java -jar ..\bin\flix-master.jar .\src\Logger.flix
Exception in thread "ForkJoinPool-3-worker-3" ca.uwaterloo.flix.util.InternalCompilerException: Unexpected purity '(~(Pure + (~Logger))) + Pure + (~Logger)'
at ca.uwaterloo.flix.language.phase.Simplifier$.simplifyEffect(Simplifier.scala:736)
at ca.uwaterloo.flix.language.phase.Simplifier$.visitExp(Simplifier.scala:167)
at ca.uwaterloo.flix.language.phase.Simplifier$.visitDef(Simplifier.scala:46)
at ca.uwaterloo.flix.language.phase.Simplifier$.$anonfun$run$2(Simplifier.scala:36)
at ca.uwaterloo.flix.util.ParOps$.$anonfun$parMapValues$1(ParOps.scala:75)
at ca.uwaterloo.flix.util.ParOps$.$anonfun$parMap$2(ParOps.scala:59)
at java.base/java.util.concurrent.ForkJoinTask$RunnableExecuteAction.exec(ForkJoinTask.java:1423)
at java.base/java.util.concurrent.ForkJoinTask.doExec(ForkJoinTask.java:387)
at java.base/java.util.concurrent.ForkJoinPool$WorkQueue.topLevelExec(ForkJoinPool.java:1312)
at java.base/java.util.concurrent.ForkJoinPool.scan(ForkJoinPool.java:1843)
at java.base/java.util.concurrent.ForkJoinPool.runWorker(ForkJoinPool.java:1808)
at java.base/java.util.concurrent.ForkJoinWorkerThread.run(ForkJoinWorkerThread.java:188)
Yeah, its touching on the boundary of what works until the new type (rather effect) system arrives.
The following "works" but is actually slightly incorrect. But I think we can continue with it:
pub def toListHandler(f: Unit -> a \ ef ): (a, List[String]) \ ef
The correct type signature (which cannot work yet) is actually:
pub def toListHandler(f: Unit -> a \ ef ): (a, List[String]) \ (ef - Logger)
pub def toListHandler(f: Unit -> a \ ef ): (a, List[String]) \ ef
Thanks Magnus, I'll go with that.
We get the same error trying to introduce the logging level API. We can define functions like fatal
but get a error trying to call them - e.g. in logExample
:
mod Handler.Logger {
eff Logger {
pub def log(s: Severity, msg: String): Unit
}
pub enum Severity with Eq, Order, ToString {
case Fatal
case Warn
case Info
case Debug
case Trace
}
pub def toListHandler(f: Unit -> a \ ef ): (a, List[String]) \ ef = region rc {
let l = MutList.new(rc);
let ans = try f() with Logger {
def log(_, msg, k) = {
MutList.push!(msg, l);
k()
}
};
let xs = MutList.toList(l);
(ans, xs)
}
pub def fatal(msg: a): Unit \ Logger with ToString[a] = do Logger.log(Severity.Fatal, "${msg}")
pub def logExample(): Int32 \ Logger = {
// do Logger.log(Severity.Fatal, "System exception");
fatal("System exception");
-1
}
}
pub def main(): Unit \ IO =
let (a1, xs) = Handler.Logger.toListHandler(_ -> Handler.Logger.logExample());
println("Trace:");
List.forEach(println, xs);
println("Answer:");
println(a1);
()
The error message is:
> java -jar ..\bin\flix-master.jar .\src\Logger.flix
Exception in thread "ForkJoinPool-3-worker-2" ca.uwaterloo.flix.util.InternalCompilerException: Unable to unify: 't9229 -> Unit \ Logger' and 'String -> Unit \ IO'.
at ca.uwaterloo.flix.language.phase.MonoDefs$.infallibleUnify(MonoDefs.scala:678)
at ca.uwaterloo.flix.language.phase.MonoDefs$.specializeDef(MonoDefs.scala:619)
at ca.uwaterloo.flix.language.phase.MonoDefs$.specializeDefSym(MonoDefs.scala:563)
at ca.uwaterloo.flix.language.phase.MonoDefs$.visitExp(MonoDefs.scala:352)
at ca.uwaterloo.flix.language.phase.MonoDefs$.visitExp(MonoDefs.scala:368)
at ca.uwaterloo.flix.language.phase.MonoDefs$.visitExp(MonoDefs.scala:403)
at ca.uwaterloo.flix.language.phase.MonoDefs$.mkFreshDefn(MonoDefs.scala:311)
at ca.uwaterloo.flix.language.phase.MonoDefs$.$anonfun$run$3(MonoDefs.scala:275)
at ca.uwaterloo.flix.language.phase.MonoDefs$.$anonfun$run$3$adapted(MonoDefs.scala:272)
at ca.uwaterloo.flix.util.ParOps$.$anonfun$parMap$2(ParOps.scala:59)
at java.base/java.util.concurrent.ForkJoinTask$RunnableExecuteAction.exec(ForkJoinTask.java:1423)
at java.base/java.util.concurrent.ForkJoinTask.doExec(ForkJoinTask.java:387)
at java.base/java.util.concurrent.ForkJoinPool$WorkQueue.topLevelExec(ForkJoinPool.java:1312)
at java.base/java.util.concurrent.ForkJoinPool.scan(ForkJoinPool.java:1843)
at java.base/java.util.concurrent.ForkJoinPool.runWorker(ForkJoinPool.java:1808)
at java.base/java.util.concurrent.ForkJoinWorkerThread.run(ForkJoinWorkerThread.java:188)
Thanks @stephentetley. @mlutze CC
I think we are stuck until https://github.com/flix/flix/pull/7114 is resolved.
But the examples should be useful for debugging. Stay tuned!
Thanks Magnus, do you want me to make a draft PR of the work so far then it doesn't get lost?
The real implementation (rather than the examples above) uses thunks and has the set of logging level functions.
Yes. That seems reasonable.
Okay, will do.
Draft PR #7134 adds Logger effect and quite a bit of the API. No tests yet and currently crashing.
I would suggest we use the signature:
def toListHandler(f: Unit -> a \ Logger ): (a, List[String]) = region rc {
This is obviously not sufficiently effect polymorphic, but that is easy to refactor, once proper support lands.
The first example now compiles and runs.