In RubySharp, we can define new functions (methods of the current object), and invoke them. There are some built-in functions in C#:
Every function should implement the interface:
public interface IFunction { object Apply(DynamicObject self, Context context, IList<object> values); }
On apply, each function receives the object (self), the context for variables (locals, arguments, closure….), and a list of already evaluated arguments.
An example, the implemention of puts:
public class PutsFunction : IFunction { private TextWriter writer; public PutsFunction(TextWriter writer) { this.writer = writer; } public object Apply(DynamicObject self, Context context, IList<object> values) { foreach (var value in values) this.writer.WriteLine(value); return null; } }
It receives the object to which it is a method, the context, and a list of arguments. Each of the arguments was evaluated. The implementation simply sends the arguments to a TextWriter, one argument per line. The TextWriter is provided when the function object is created (in the Machine object, that represents the current running environment). This injected facilites the test of the function, example:
[TestMethod] public void PutsTwoIntegers() { StringWriter writer = new StringWriter(); PutsFunction function = new PutsFunction(writer); Assert.IsNull(function.Apply(null, null, new object[] { 123, 456 })); Assert.AreEqual("123\r\n456\r\n", writer.ToString()); }
The above code was born using the TDD workflow.
Let’s see another built-in function, require:
public class RequireFunction : IFunction { private Machine machine; public RequireFunction(Machine machine) { this.machine = machine; } public object Apply(DynamicObject self, Context context, IList<object> values) { string filename = (string)values[0]; return this.machine.RequireFile(filename); } }
This time, the job of loading a file is delegated to the Machin object, injected in the constructor.
And finally, let’s review the code of a defined function (defined in RubySharp):
public class DefinedFunction : IFunction { private IExpression body; private IList<string> parameters; private Context context; public DefinedFunction(IExpression body, IList<string> parameters, Context context) { this.body = body; this.context = context; this.parameters = parameters; } public object Apply(DynamicObject self, Context context, IList<object> values) { Context newcontext = new Context(self, this.context); int k = 0; int cv = values.Count; foreach (var parameter in this.parameters) { newcontext.SetLocalValue(parameter, values[k]); k++; } return this.body.Evaluate(newcontext); } }
In this case, a new contexts is built, containing the new arguments to the defined function, but with the original closure, injected in the constructor. The context provided in the Apply is not used.
Next topics: some command implementations, the Machine object, the REPL, etc…
Stay tuned!
Angel “Java” Lopez
http://www.ajlopez.com