Skip to content

decoder errors on recursive types #5

@tsukimizake

Description

@tsukimizake

Hi. I'm trying to use elm_rs to make Msg type between tauri <-> elm. It's so useful.

However, I found the generated decoder errors out when enum has recursion. (Well, I found it is listed in the Planned
list in the readme later.)

#[derive(Serialize, Deserialize, Debug, Elm, ElmEncode, ElmDecode, Clone)]
enum Enum {
    Unit,
    Recursive(Box<Enum>),
}

This rust Enum type generates this enumDecoder.

enumDecoder : Json.Decode.Decoder Enum
enumDecoder = 
    Json.Decode.oneOf
        [ Json.Decode.string
            |> Json.Decode.andThen
                (\x ->
                    case x of
                        "Unit" ->
                            Json.Decode.succeed Unit
                        unexpected ->
                            Json.Decode.fail <| "Unexpected variant " ++ unexpected
                )
        , Json.Decode.map Recursive (Json.Decode.field "Recursive" (enumDecoder))
        ]

and it errors like this.

Compilation failed
Compiling ...-- CYCLIC DEFINITION -------------------------------------- src/elm/Bindings.elm

The `enumDecoder` value is defined directly in terms of itself, causing an
infinite loop.

105| enumDecoder = 
     ^^^^^^^^^^^
Are you are trying to mutate a variable? Elm does not have mutation, so when I
see enumDecoder defined in terms of enumDecoder, I treat it as a recursive
definition. Try giving the new value a new name!

Maybe you DO want a recursive value? To define enumDecoder we need to know what
enumDecoder is, so let’s expand it. Wait, but now we need to know what
enumDecoder is, so let’s expand it... This will keep going infinitely!

Hint: The root problem is often a typo in some variable name, but I recommend
reading <https://elm-lang.org/0.19.1/bad-recursion> for more detailed advice,
especially if you actually do need a recursive value.


Detected problems in 1 module.
    at ChildProcess.<anonymous> (/Users/tsukimizake/try-tauri/node_modules/node-elm-compiler/dist/index.js:131:35)
    at ChildProcess.emit (node:events:518:28)
    at maybeClose (node:internal/child_process:1104:16)
    at Socket.<anonymous> (node:internal/child_process:456:11

This error can be fixed by wrapping the recursive call with Json.Decode.lazy.

enumDecoder : Json.Decode.Decoder Enum
enumDecoder = 
    Json.Decode.oneOf
        [ Json.Decode.string
            |> Json.Decode.andThen
                (\x ->
                    case x of
                        "Unit" ->
                            Json.Decode.succeed Unit
                        unexpected ->
                            Json.Decode.fail <| "Unexpected variant " ++ unexpected
                )
        , Json.Decode.map Recursive (Json.Decode.field "Recursive" (Json.Decode.lazy (\() -> enumDecoder)))
        ]

I tried to look into the code of elm_rs, but I coudn't figure out how to detect recursive type as the impl_builtin... macros works on each fields independently and doesn't know the name of the parent.
At least, above code works with this patch, although it should have many false positive lazies as it wraps all ptr like fields.

diff --git a/elm_rs/src/elm_decode.rs b/elm_rs/src/elm_decode.rs
index fd37260..f21cd98 100644
--- a/elm_rs/src/elm_decode.rs
+++ b/elm_rs/src/elm_decode.rs
@@ -198,7 +198,7 @@ macro_rules! impl_builtin_ptr {
     ($rust_type: ty) => {
         impl<T: Elm + ElmDecode + ?Sized> ElmDecode for $rust_type {
             fn decoder_type() -> String {
-                ::std::format!("{}", T::decoder_type())
+                ::std::format!("(Json.Decode.lazy (\\_ -> {}))", T::decoder_type())
             }
 
             fn decoder_definition() -> Option<String> {
diff --git a/elm_rs/src/test/enums_external.rs b/elm_rs/src/test/enums_external.rs
index 245a003..6694dbb 100644
--- a/elm_rs/src/test/enums_external.rs
+++ b/elm_rs/src/test/enums_external.rs
@@ -11,6 +11,7 @@ enum Enum {
     Tuple2(i32, i32),
     Named1 { field: i32 },
     Named2 { field: i32 },
+    Recursive(Box<Enum>),
 }
 
 #[test]
@@ -32,3 +33,8 @@ fn tuple() {
 fn named() {
     super::test_json(Enum::Named1 { field: 123 });
 }
+
+#[test]
+fn recursive() {
+    super::test_json(Enum::Recursive(Box::new(Enum::Unit1)));
+}

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions