flix icon indicating copy to clipboard operation
flix copied to clipboard

Add `Eff.Logger` effect

Open magnus-madsen opened this issue 1 year ago • 17 comments

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 write Logger.info for example.
  • Use of ToString.
  • Use of laziness via Unit -> a. We may also consider whether Lazy[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.

magnus-madsen avatar Jan 22 '24 11:01 magnus-madsen

@stephentetley Would you be interested in working on the above?

magnus-madsen avatar Jan 22 '24 11:01 magnus-madsen

Thanks Magnus, yes I'll take a look at this.

stephentetley avatar Jan 22 '24 19:01 stephentetley

Considering ioHandlerWithSeverity - do you want each message timestamped or just the start of the "run" of the collected messages when printed to the console?

stephentetley avatar Jan 22 '24 19:01 stephentetley

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.

magnus-madsen avatar Jan 22 '24 19:01 magnus-madsen

I updated the documentation here a bit: https://doc.flix.dev/effects-and-handlers.html

magnus-madsen avatar Jan 22 '24 20:01 magnus-madsen

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)

stephentetley avatar Jan 23 '24 22:01 stephentetley

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

magnus-madsen avatar Jan 23 '24 22:01 magnus-madsen

The correct type signature (which cannot work yet) is actually:

pub def toListHandler(f: Unit -> a \ ef ): (a, List[String]) \ (ef  - Logger)

magnus-madsen avatar Jan 23 '24 22:01 magnus-madsen

pub def toListHandler(f: Unit -> a \ ef ): (a, List[String]) \ ef

Thanks Magnus, I'll go with that.

stephentetley avatar Jan 24 '24 17:01 stephentetley

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)

stephentetley avatar Jan 25 '24 18:01 stephentetley

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!

magnus-madsen avatar Jan 26 '24 07:01 magnus-madsen

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.

stephentetley avatar Jan 26 '24 18:01 stephentetley

Yes. That seems reasonable.

magnus-madsen avatar Jan 26 '24 18:01 magnus-madsen

Okay, will do.

stephentetley avatar Jan 26 '24 18:01 stephentetley

Draft PR #7134 adds Logger effect and quite a bit of the API. No tests yet and currently crashing.

stephentetley avatar Jan 26 '24 21:01 stephentetley

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.

magnus-madsen avatar Jan 27 '24 13:01 magnus-madsen

The first example now compiles and runs.

mlutze avatar Jan 29 '24 11:01 mlutze