[FR] Monadic operators on ValueTree

Would be really cool to get some monadic operators like andThen(), and orElse() on juce::ValueTree. This seems to be the direction many of the STL containers are taking and I for one find they can help write some really clean code.

juce::ValueTree::andThen() should take a function to act on a juce::ValueTree when it is non-empty, and likewise juce::ValueTree::orElse() should take a function to act on a juce::ValueTree when it is empty - allowing for some nice error handling.

This would be particularly useful for things like loading from XML:

const auto validVtXML = R"(<Tree foo="123"/>)";
const auto invalidVtXML = R"(<Tree>Some inline text</Tree>)";

juce::ValueTree::fromXML (validVtXML)
    .orElse ([](auto& /*emptyTree*/) {
                 return juce::ValueTree {"Tree"}; // Some fallback state
             })
    .andThen ([] (auto& populatedTree) {
                  loadState (populatedTree);
                  return populatedTree;
              });

The current way of doing this would be something like:

auto state = juce::ValueTree::fromXML (invalidVtXML);

if (state == juce::ValueTree{})
    state = juce::ValueTree {"Tree"};

loadState (state);

Which is fine if you prefer more imperitive code, but I personally much prefer the more declarative style.

Interesting idea. Couldn’t this be done quite easily with a wrapper around juce::ValueTree?

Yeah a wrapper would work as a temporary solution, but I think having the functionality on the data structure itself is much more useful.

Closest you could get for now is probably a std::optional<juce::ValueTree>, and use C++23’s and_then() and or_else(). However I’m not a huge fan of that because then you effectively have two empty states, when !optionalTree.has_value() and !optionalTree->isValid(). To have a Total Function you’d always need to check both conditions, which kinda defeats the purpose IMO.

Yeah double dereferencing is the worst! I was thinking of something different though, just something simple like this:

#include <functional>

class MonadicVT
{
public:
    using VTF = std::function<juce::ValueTree(juce::ValueTree)>;

    MonadicVT(const juce::ValueTree& vt_) : vt(vt_) {}

    MonadicVT and_then(const VTF& f) const
    {
        if (vt.isValid())
            return MonadicVT(std::invoke(f, vt));
        else return MonadicVT(vt);
    }

    MonadicVT or_else(const VTF& f) const
    {
        if (!vt.isValid())
            return MonadicVT(std::invoke(f, vt));
        else return MonadicVT(vt);
    }

    juce::ValueTree vt;
};

void tryItOut()
{
    auto testVT = juce::ValueTree("test");
    auto testMVT = MonadicVT(testVT)

        .and_then([](juce::ValueTree vt) { vt.setProperty("a", 1, nullptr); return vt; })
        .and_then([](juce::ValueTree vt) { auto child = juce::ValueTree("child"); vt.appendChild(child, nullptr); return child; })
        .and_then([](juce::ValueTree vt) { return vt.getChild(0); })                            // return VT doesn't exist...
        .and_then([](juce::ValueTree vt) { vt.setProperty("nope", 2, nullptr); return vt; })    // ...so this won't be applied
        .or_else([](juce::ValueTree empty) { return juce::ValueTree("backup"); })
        .and_then([](juce::ValueTree vt) { vt.setProperty("b", 2, nullptr); return vt; });

    DBG(testVT.toXmlString());
    /*
        <test a = "1">
          <child/>
        </test>
    */
    DBG(testMVT.vt.toXmlString());
    /*
        <backup b = "2"/>
    */
}

I’m using std::function here because it makes for more explicit code, but you could probably use templates instead to make it more efficient.

Note that I’m not opposing your feature request, and I hope that a feature like this gets added to the ValueTree class. I just wrote this because it was fun, and so that maybe you can use this feature now (in Jive?) without having to wait for it.

1 Like

Maybe it is just me, but that code looks very noisy and hard to read to me.

1 Like

You’re right, the test example is a bit over the top, and it would be much easier to just use the normal VT functions here. I suppose another option would be to replicate all of the main VT functions individually in the wrapper class (setProperty, addChild etc.) and have them all return a new MonadicVT wrapper. That way you wouldn’t need to use lambdas, so the code would be more concise. It would also allow for daisy-chaining, and you could retain the or_else condition that was originally proposed. Am I making sense?

I like the MonadicVT idea, I think it’s actually quite concise if you use auto& as the lambda argument.
I’ve started to use tl::expected a lot and the code looks very similar and a lot cleaner than all the if/elses you get otherwise.

It’s a little bit verbose to have to return the tree each time but I guess you wouldn’t normally make huge chains, usually just a valid/error operation.

Returning a tree does also kind of get around the problem in the other VT thread that you could and_then/append and return the child/and_then on the child.

1 Like

[promising code to begin with, but bad news ahead]

I built another simple class without the lambdas, which in theory can address all three feature requests at once (daisy-chaining, default undoManager, and or_else).

class SpeedyVT
{
public:
    SpeedyVT(const juce::ValueTree& vt_) : vt(vt_) {}

    SpeedyVT setProp(const juce::Identifier& id, const juce::var& v, juce::UndoManager* um = nullptr)
    {
        if (vt.isValid())
            vt.setProperty(id, v, um);

        return vt;
    }

    SpeedyVT addAndReturnChild(const juce::ValueTree& child, int i = -1, juce::UndoManager* um = nullptr)
    {
        if (vt.isValid())
            vt.addChild(child, i, um);

        return child;
    }

    SpeedyVT or_else(const juce::ValueTree& other) const
    {
        if (vt.isValid())
            return vt;

        else return other;
    }
    // add other methods as desired: getOrCreateChild(), getParent(), etc.

    juce::ValueTree vt;
};

void doSomeSpeedyVT()
{
    auto invalidVT = juce::ValueTree();
        
    auto svt = SpeedyVT(invalidVT)
        .or_else(juce::ValueTree("replacement"))
        .setProp("p", 1)
        .addAndReturnChild(juce::ValueTree("child"))
        .setProp("x", 2);

    DBG(svt.vt.getRoot().toXmlString());
    // <child x="2"/>  !?
}

But then I ran into a problem that I hadn’t anticipated. I expected the above code to print:

<replacement p="1">
    <child x="2"/>
</replacement>

But instead “replacement” is gone and you get:

<child x="2"/>

The problem is that the “replacement” ValueTree goes out of scope when the daisy-chained code ends, and this wipes the parent pointer inside child. This behavior was new to me, but it kind of makes sense that I’d never seen it before, since the existing API doesn’t really allow for a parent ValueTree to go out of scope before a child does. (Maybe that’s why daisy-chaining is disallowed in the first place!)

Note that while I’m working with a wrapper class here, you’d face the same issue if you added these methods to the ValueTree class itself, as far as I can tell.

I’m afraid that this problem puts the breaks on both of @ImJimmi 's ideas (or_else and daisy-chaining). It’s definitely a solvable problem, but not in a simple way.

That’s exactly what I’d expect with that setup - using addAndReturnChild(), you let go of the only reference to the top-level node and so it gets deleted. I think you’d need to do

    auto svt = SpeedyVT(invalidVT)
        .or_else(juce::ValueTree("replacement"))
        .setProp("p", 1)
        .addAndReturnChild(SpeedyVT(juce::ValueTree("child"))
                               .setProp("x", 2));

Also or_else() would typically take a function that takes and returns a VT, rather than a VT itself.

1 Like