The most straightforward approach to this type of decoder, is to create a branch-decoder, a leaf-decoder and a tree-decoder, which would look something like this:
decoder : Decoder Tree
decoder =
oneOf
[ branchDecoder
, leafDecoder
]
branchDecoder : Decoder Tree
branchDecoder =
map2 Branch
(field "name" string)
(field "children" (list decoder))
leafDecoder : Decoder Tree
leafDecoder =
map2 Leaf
(field "name" string)
(field "value" int)
Since we work with an eager language, that can't work - decoder
and
branchDecoder
refer to one another without any laziness being introduced, and
the compiler will tell you about it:
The following definitions depend directly on each other:
┌─────┐
│ branchDecoder
│ ↓
│ decoder
└─────┘
You seem to have a fairly tricky case, so I very highly recommend reading this:
<https://github.com/elm-lang/elm-compiler/blob/0.18.0/hints/bad-recursion.md> It
will help you really understand the problem and how to fix it. Read it!
So, after reading that document, you know you need to introduce laziness using
- in this case -
Json.Decode.lazy
. You may be wondering where to put the call tolazy
- should you lazily refer tobranchDecoder
fromdecoder
, or should you lazily refer todecoder
frombranchDecoder
?
The answer is: there is no way to know for sure.
See, Elm orders its output based on how strongly connected different components are. In other words, a function that has more dependencies is more likely to appear later, a function that has fewer dependencies is more likely to appear earlier. In our case, that makes the most likely order leafDecoder -> branchDecoder -> decoder
.
If we lazily refer to branchDecoder
from decoder
, this order doesn't change; and branchDecoder
will still eagerly refer to decoder
whose definition appears only later in the compiled code.
decoder : Decoder Tree
decoder =
oneOf
[ lazy (\_ -> branchDecoder)
, leafDecoder
]
branchDecoder : Decoder Tree
branchDecoder =
map2 Branch
(field "name" string)
(field "children" (list decoder))
leafDecoder : Decoder Tree
leafDecoder =
map2 Leaf
(field "name" string)
(field "value" int)
The above results in the following result - slightly cleaned up to make it a little more readable.
var leafDecoder = A3(
map2,
Leaf,
A2(field, 'name', string),
A2(field, 'value', int));
var branchDecoder = A3(
map2,
Branch,
A2(field, 'name', string),
A2(
field,
'children',
list(decoder)));
var decoder = oneOf(
{
ctor: '::',
_0: lazy(
function (_p0) {
return branchDecoder;
}),
_1: {
ctor: '::',
_0: leafDecoder,
_1: {ctor: '[]'}
}
});
Notice that there's only a single function
definition in this whole thing -
everything else is eagerly evaluated and has all of its argument available.
Note, also, that branchDecoder
is defined before decoder
is defined; yet
it references decoder
. Since only function declarations are hoisted to the
top in JavaScript; the above code can't actually work! decoder
will be
undefined when branchDecoder
is used.
So our next option is to place the lazy
call in branchDecoder
and see if
that works:
decoder : Decoder Tree
decoder =
oneOf
[ branchDecoder
, leafDecoder
]
branchDecoder : Decoder Tree
branchDecoder =
map2 Branch
(field "name" string)
(field "children" (list <| lazy (\_ -> decoder)))
leafDecoder : Decoder Tree
leafDecoder =
map2 Leaf
(field "name" string)
(field "value" int)
In the compiled result, we see that we have actually achieved our goal:
var leafDecoder = A3(
map2,
Leaf,
A2(field, 'name', string),
A2(field, 'value', int));
var branchDecoder = A3(
map2,
Branch,
A2(field, 'name', string),
A2(
field,
'children',
list(
lazy(
function (_p0) {
return decoder;
}))));
var decoder = oneOf(
{
ctor: '::',
_0: branchDecoder,
_1: {
ctor: '::',
_0: leafDecoder,
_1: {ctor: '[]'}
}
});
Yay, success!
However, the order in which compiled results appear in the output isn't
something you, while writing Elm code, should worry about. Figuring out the
strongly connected components to figure out where the lazy
should go, is not
what you want to be worrying about. So the safest option when dealing with 2
functions that refer to one another, is to introduce laziness at both places:
decoder : Decoder Tree
decoder =
oneOf
[ lazy (\_ -> branchDecoder)
, leafDecoder
]
branchDecoder : Decoder Tree
branchDecoder =
map2 Branch
(field "name" string)
(field "children" (list <| lazy (\_ -> decoder)))
leafDecoder : Decoder Tree
leafDecoder =
map2 Leaf
(field "name" string)
(field "value" int)