Adapting the tagless-final use-case to PHP

First of all, I’m not trying to do tagless-final in PHP, but rather investigating what’s needed to cover a similar use-case, which would be an embedded DSL which can be evaluated in different ways.

Second point, the approach is very much “tagfull”, since it’s building an AST. :slight_smile:

The idea is to use the expression builder pattern to build an AST, and then inject either a live evaluator or a dummy evaluator into the builder class.

The end-goal is to get rid of mocking in the test suite, either by making more functions pure (by deferring effects) or use a “universal mock” (the dummy evaluator).

OCaml is my language of choice, but PHP is what I work in, so for me it’s always interesting to figure out if/how to transfer concepts between the languages. Another example of this is the Option type vs null flow-checking done in Psalm.

Motivating example (from here):

    public static string GetUpperText(string path)
    {
        if (!File.Exists(path)) return "DEFAULT";
        var text = File.ReadAllText(path);
        return text.ToUpperInvariant();
    }

In PHP with the effect EDSL:

    function getUpperText(string $file, St $st)
    {
        $result = 'DEFAULT';
        $st
            ->if(fileExists($file))
            ->then(set($result, fileGetContents($file)))
            ();
        return strtoupper($result);
    }

In PHP with a mockable class:

    function getUpperText(string $file, IO $io)
    {
        $result = 'DEFAULT';
        if ($io->fileExists($file)) {
            $result = $io->fileGetContents($file);
        }
        return strtoupper($result);
    }

The St class will build an abstract-syntax tree, which is then evaluated when invoked. It can be injected with either a live evaluator, or a dry-run evaluator which works as both mock, stub and spy.

St can also be used to delay or defer effects - just omit the invoke until later.

The unit test looks like this:

    // Instead of mocking return types, set the return values
    $returnValues = [
        true,
        'Some example file content, bla bla bla'
    ];
    $ev = new DryRunEvaluator($returnValues);
    $st = new St($ev);
    $text = getUpperText('moo.txt', $st);
    // Output: string(38) "SOME EXAMPLE FILE CONTENT, BLA BLA BLA"
    var_dump($text);
    // Instead of a spy, you can inspect the dry-run log
    var_dump($ev->log);
    /* Output:
       array(5) {
       [0] =>
       string(13) "Evaluating if"
       [1] =>
       string(27) "File exists: arg1 = moo.txt"
       [2] =>
       string(15) "Evaluating then"
       [3] =>
       string(33) "File get contents: arg1 = moo.txt"
       [4] =>
       string(50) "Set var to: Some example file content, bla bla bla"
       }
     */

The St class scales differently than mocking, so it’s not always sensible to use.

Full code: One universal dry-run mock-spy AST evaluator to rule them all · GitHub

I think this is off-topic for OCaml. There are plenty of general programming, PHP, and functional programming forums.

1 Like

Sure, feel free to delete.