fictiveworks / CalyxSharp

Generative text processing for C# and Unity applications
Other
0 stars 0 forks source link

Improve usability of context dictionaries passed to Grammar.Generate #40

Open maetl opened 1 year ago

maetl commented 1 year ago

This is something that came up from working through the documentation guides, converting existing Ruby and JS examples to C#.

The dynamic context feature, where map structures passed to generate get converted to temp rules in the registry at runtime is very much an API feature that emerged from dynamic language thinking and template engine APIs. It’s an important feature to have because it opens up a whole lot of useful patterns that would not be possible with purely static grammars, but translating it to C# well is not as obvious/direct as just flinging around a bunch of untyped data structures as certain other languages encourage.

Example of one of the documentation examples translated to a C# test:

using User = System.Collections.Generic.Dictionary<string, string>;

// etc...

    [Test]
    public void AppWelcomeMessageExample()
    {
      Grammar greeting = new Grammar(G => {
        G.Start(new[] { "Hi {username}", "Welcome back {username}", "Hola {username}" });
      });

      User user = new User {
        { "name", "Erika" }
      };

      Result result = greeting.Generate(new Dictionary<string, string[]> {
        { "username", new[] { user["name"] }}
      });

      Assert.That(result.Text, Does.EndWith("Erika"));
    }

// etc...

A couple of things here which make the API more complicated to use:

An example from the JS port that doesn’t currently work in C#:

import calyx from "calyx"

const manu = calyx.grammar({
  backyard: ["house sparrow", "blackbird", "starling"],
  coastal: ["black-backed gull", "red-billed gull"],
  forest: ["tūī", "korimako", "pīwakawaka"]
})

const result = manu.generate({
  start: {
    "{backyard}": 3,
    "{coastal}": 2,
    "{forest}": 1
  }
})

console.log(result.text)

To improve the usability here, perhaps we could provide a builtin Context type which does a little bit of polymorphism housekeeping and internal wrangling on the key types to support simpler injection of dynamic state, rather than the brute force hammer solution of something like Dictionary<string, object> which potentially opens up a whole new area of bugs and mistakes.

maetl commented 1 year ago

A potential implementation concept using the dynamic type from .Net 4. The polymorphism currently built-in to the Grammar.Rule method does a lot of the heavy-lifting here. Not without its problems, and still needs to be tested/verified in Unity, but is at least a starting point for thinking about the direction for this feature.

using NUnit.Framework;
using Calyx;
using Context = System.Collections.Generic.Dictionary<string, dynamic>;

public class DynamicContext
{
  public Grammar BuildGrammar(Context context)
  {
    Grammar objGrammar = new Grammar(seed: 1234);

    foreach(var contextRule in context) {
      objGrammar.Rule(contextRule.Key, contextRule.Value);
    }

    return objGrammar;
  }
}

namespace Calyx.Test.Experimental
{
  public class DynamicContextTest
  {
    [Test]
    public void SupportsPolymorphicBranchTypesInContext()
    {
      DynamicContext experiment = new DynamicContext();
      Grammar grammar = experiment.BuildGrammar(new Context {
        { "fixedBranch", "one" },
        { "uniformBranch", new[] { "one", "two", "three" } },
        { "weightedBranch", new Dictionary<string, int> {
            { "one", 4 },
            { "two", 2 },
            { "three", 1 }
          }
        }
      });

      Result fixedResult = grammar.Generate("fixedBranch");
      Result uniformResult = grammar.Generate("uniformBranch");
      Result weightedResult = grammar.Generate("weightedBranch");

      Assert.That(fixedResult.Text, Is.EqualTo("one"));
      Assert.That(uniformResult.Text, Is.EqualTo("three"));
      Assert.That(weightedResult.Text, Is.EqualTo("two"));
    }
  }
}