roc icon indicating copy to clipboard operation
roc copied to clipboard

How to solve the errors about error union?

Open lost22git opened this issue 1 year ago • 3 comments

Record : {
    time : Utc.Utc,
    level : Level,
    msg : Lazy.Lazy Str,
}

Err : [
    FilterErr Str,
    FormatterErr Str,
    WriterErr Str,
]

Logger : {
    filter : Filter,
    writer : Writer,
    formatter : Formatter,
}

defaultLogger : Logger
defaultLogger = { filter: defaultFilter, writer: defaultWriter, formatter: defaultFormatter }

Filter : Record -> Task Bool [FilterErr Str]
Writer : Str -> Task {} [WriterErr Str]
Formatter : Writer, Record -> Task {} [WriterErr Str, FormatterErr Str]

defaultFilter : Filter
defaultFilter = \record ->
    minLevel = levelFromEnv {} |> Task.mapErr! \e -> FilterErr "Failed to filter log record"
    Task.ok ((levelToInt minLevel) <= (levelToInt record.level))

defaultWriter : Writer
defaultWriter = \str -> Stdout.line str |> Task.mapErr \e -> WriterErr "Failed to write log record"

defaultFormatter : Formatter
defaultFormatter = \writer, record ->
    msg = record.msg |> Lazy.tryInit |> Lazy.get |> Result.withDefault ""
    writer "$(record.time |> Time.utcToRFC3339) $(levelToAnsiStr record.level) - $(msg)"

## do log with the given [Record]
logRecord : Logger, Record -> Task {} [LogErr Err]
logRecord = \logger, record ->
    canLog = logger.filter record |> Task.mapErr! \e -> LogErr e
    if canLog then
        logger.writer |> logger.formatter record |> Task.mapErr \e -> LogErr e
    else
        Task.ok {}

compiler error report:

root in hello/hello_roc on  main [!?] 
❯ ./log.roc

── TYPE MISMATCH in ./log.roc ──────────────────────────────────────────────────

Something is off with the body of the defaultFormatter definition:

 99│   defaultFormatter : Formatter
100│>  defaultFormatter = \writer, record ->
101│>      msg = record.msg |> Lazy.tryInit |> Lazy.get |> Result.withDefault ""
102│>      writer "$(record.time |> Time.utcToRFC3339) $(levelToAnsiStr record.level) - $(msg)"

The body is an anonymous function of type:

    (Str -> Task {} […]), { … } -> Task {} […]

But the type annotation on defaultFormatter says it should be:

    (Str -> Task {} […]), { … } -> Task {} [FormatterErr Str, …]


── TYPE MISMATCH in ./log.roc ──────────────────────────────────────────────────

This 2nd argument to this function has an unexpected type:

107│      canLog = logger.filter record |> Task.mapErr! \e -> LogErr e
                   ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

The argument is an anonymous function of type:

    Bool -> Task {} [LogErr [
        FormatterErr Str,
        WriterErr Str,
    ]]

But this function needs its 2nd argument to be:

    Bool -> Task {} [LogErr [FilterErr Str]]


── TYPE MISMATCH in ./log.roc ──────────────────────────────────────────────────

Something is off with the body of the logRecord definition:

105│  logRecord : Logger, Record -> Task {} [LogErr Err]
106│  logRecord = \logger, record ->
107│      canLog = logger.filter record |> Task.mapErr! \e -> LogErr e
                   ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

This await call produces:

    Task {} [LogErr [FilterErr Str]]

But the type annotation on logRecord says it should be:

    Task {} [LogErr Err]a

────────────────────────────────────────────────────────────────────────────────

3 errors and 3 warnings found in 60 ms
.

lost22git avatar Oct 03 '24 12:10 lost22git

Hi @lost22git, your hello repo looks like an interesting multilingual project :)

Two of the errors are related to open and closed tag unions, but I'm not completely sure what was going wrong behind the scenes yet, it could be related to #5660.

This version is free of compile errors:

app [main] { pf: platform "https://github.com/roc-lang/basic-cli/releases/download/0.15.0/SlwdbJ-3GR7uBWQo6zlmYWNYOxnvo8r6YABXD-45UOw.tar.br" }

import pf.Stdout
import pf.Utc
import pf.Env
import Time
import Lazy
import Ansi

main =
    Stdout.line! "Hello, World!"

Record : {
    time : Utc.Utc,
    level : Level,
    msg : Lazy.Lazy Str,
}

Err : [
    FilterErr Str,
    WriterErr Str,
]

Logger a b : {
    filter : Filter a,
    writer : Writer,
    formatter : Formatter b,
}

defaultLogger : Logger _ _
defaultLogger = { filter: defaultFilter, writer: defaultWriter, formatter: defaultFormatter }

Filter a : Record -> Task Bool [FilterErr Str]a
Writer : Str -> Task {} [WriterErr Str]
Formatter a: Writer, Record -> Task {} [WriterErr Str]a

defaultFilter : Filter _
defaultFilter = \record ->
    minLevel = levelFromEnv {} |> Task.mapErr! \e -> FilterErr "Failed to filter log record"
    Task.ok ((levelToInt minLevel) <= (levelToInt record.level))

defaultWriter : Writer
defaultWriter = \str -> Stdout.line str |> Task.mapErr \e -> WriterErr "Failed to write log record"

defaultFormatter : Formatter _
defaultFormatter = \writer, record ->
    msg = record.msg |> Lazy.tryInit |> Lazy.get |> Result.withDefault ""
    writer "$(record.time |> Time.utcToRFC3339) $(levelToAnsiStr record.level) - $(msg)"

## do log with the given [Record]
logRecord : Logger _ _, Record -> Task {} [LogErr Err]
logRecord = \logger, record ->
    canLog = logger.filter record |> Task.mapErr! \e -> LogErr e
    if canLog then
        logger.writer |> logger.formatter record |> Task.mapErr \e -> LogErr e
    else
        Task.ok {}

Level : [
    Debug,
    Info,
    Warn,
    Error,
]

## convert `Level` to `Int`
levelToInt : Level -> I8
levelToInt = \lv ->
    when lv is
        Debug -> 0
        Info -> 10
        Warn -> 20
        Error -> 30

## convert plain `Str` to `Level`
levelFromStr : Str -> Result Level [LevelFromStrErr Str]
levelFromStr = \s ->
    when s is
        "DBG" -> Ok Debug
        "INF" -> Ok Info
        "WAR" -> Ok Warn
        "ERR" -> Ok Error
        _ -> Err (LevelFromStrErr s)

## convert `Level` to plain `Str`
levelToStr : Level -> Str
levelToStr = \lv ->
    when lv is
        Debug -> "DBG"
        Info -> "INF"
        Warn -> "WAR"
        Error -> "ERR"

## get `AnsiStyle`s of given `Level`
levelToAnsiStyles : Level -> List Ansi.Style
levelToAnsiStyles = \lv ->
    when lv is
        Debug -> [Bold, FgBlue]
        Info -> [Bold, FgGreen]
        Warn -> [Bold, FgYellow]
        Error -> [Bold, FgRed]

## convert `Level` to ansi `Str`
levelToAnsiStr : Level -> Str
levelToAnsiStr = \lv -> Ansi.ansiStr (levelToStr lv) (levelToAnsiStyles lv)

## read `Level` from environment variable `ROC_LOG_LEVEL`
levelFromEnv : {} -> Task Level _
levelFromEnv = \_ ->
    Env.var "ROC_LOG_LEVEL"
        |> Task.await \s -> (levelFromStr s |> Task.fromResult)
        |> Task.onErr! \_ -> Task.ok Info

Note that I removed FormatterErr Str in:

Formatter : Writer, Record -> Task {} [WriterErr Str, FormatterErr Str]

Because the error below was complaining that FormatterErr could not happen based on your code and it was right :)

The body is an anonymous function of type:

    (Str -> Task {} […]), { … } -> Task {} […]

But the type annotation on defaultFormatter says it should be:

    (Str -> Task {} […]), { … } -> Task {} [FormatterErr Str, …]

Anton-4 avatar Oct 04 '24 15:10 Anton-4

@Anton-4 Thank you very much for your answer, and I'm sorry that I didn't post all the code. But I still find it difficult to understand the compatibility rules of roc's error unions.

lost22git avatar Oct 04 '24 16:10 lost22git

But I still find it difficult to understand the compatibility rules of roc's error unions.

That's understandable, they are a pretty unique concept.

Can you put together a small example that you find confusing? That will make it easier to discuss things.

Sidenote: do you already have the roc language server set up? You can use for example this vscode extension for it. Being able to see the inferred type of something on hover can be helpful.

Anton-4 avatar Oct 04 '24 18:10 Anton-4