|Home | About | Partners | Contact Us|
XLR: Extensible Language and Runtime
XL, an extensible programming language, implements
If you want to know more, you should start here.
We were discussing how to simplify the implementation and extension of Write. Currently, two modules implement slightly different versions of Write:
- XL.UI.CONSOLE.Write writes to the console
- XL.TEXT_IO.Write writes to a file
The first one is implemented using the second one, writing to standard output. More are to come, like writing to a string. Like C++, we could decide to use a stream class, that would tell where to write, whether it's a file, a string, etc. We also need either that stream or something else to store formatting preferences, e.g. number of digits.
One problem with this approach is that it makes extending write rather complicated. For example, if you want to write a complex number, you need something like:
to Write (output : stream; Z : complex) is Write Z, "(", Z.re, ";", Z.im, ")"
This is not too complicated, but if all I actually use is writing to the console, the code above has a rather unnecessary built-in notion that "Write to stdout" is really a write to a stream. Also, when you write Z.re or Z.im, you will use the formatting for real-numbers (e.g. number of digits), but how would you specify additional formatting info for complex numbers, e.g. allow you to choose between (1;3), [1, 3] or 1 + 3i ?
Ideally, I would like this:
to Write(Z : complex) is Write ComplexFormat.OpenSeparator, Z.re, ComplexFormat.InnerSeparator, Z.im, ComplexFormat.ClosingSeparator
But then, that variant would not allow you to write to a file... or would it? This is where the idea of scope injection comes in.
It seems like we could actually make it work with a file, if we add the idea that the Write we call could have an implicit generic dependency on a StandardOutput variable. Consider that the final step in writing, writing a character, is currently written as something like:
to Write(F : stream; C : character) is C.putc(C, F)
What if we used any-lookup with an extension, and replaced that tail with one that doesn't take the standard output argument, but introduces it:
to Write(C : character) is Write any.StandardOutput, C
The idea is the following: if you do not redefine StandardOutput in the instantiation context, then any.StandardOutput evaluates as the StandardOutput in the current context, which is a stream, so you end up calling the Write-to-stream function. But you can override StandardOutput and instantiate complex to write into some text like this:
to Write (in out target : text; C : character) is target += C
Now, here is what happens. The call to Write Z gets decomposed to its components, until the final Write C for individual characters. But at that stage, the any.StandardOutput finds a StandardOutput in the instantiation context, specifically the local parameter to the second Write we just defined.
A few things are a bit complicated to make this work:
- We need to detect implicit dependencies to generic items, e.g. the any.StandardOutput in the character Write.
- This may now resolve to a local variable, not just some global. In that case, we need to pass some implicit parameter down the instantiation stack, in that case the StandardOutput : text parameter.
It is also not clear if it is a good idea to create this implicit dependency based solely on the implementation, i.e. based on the use of any.StandardOutput inside a function that, otherwise, would not even be generic. An alternative is to add some new syntax indicating that generic dependency in the interface, something like:
generic [name StandardOutput] to Write (C : character) is Write StandardOutput, C
This would make it much clearer which part is generic and which part is not.
Any opinion is welcome...