Nytril: A programming language and a markup language in one

Posted by Impressive-Gear-4334@reddit | programming | View on Reddit | 4 comments

Nytril has a unique syntax that makes it easy to create complex documents from the results of your code. There are no "Write" or "Print" statements. The results of each expression simply become a part of the document.

Here are some of the standout features and new concepts:

global functions and namespaces

Nytril allows global functions that do not belong to a class and have no 'this' pointer. They can also be defined inside of a namespace.

F1(x) = x + 1;      // F1 defined globally
N1.F2(x) = x + 2;   // F2 defined inside the namespace N1
namespace N2 {
  F3(x) = x + 3;    // F3 defined inside the namespace N2
}
namespace N2 {     // N2 can be repeated
  F4(x) = x + 4;   // F3 and F4 are now a part of N2 
}

readonly global functions

If a global function is marked as readonly, then the function is run the first time it is called anywhere in the program, and the return value is cached. If the function is called again anywhere, the cached value from the first run is returned immediately. This is useful when loading large constant values such as images or the results of database queries or REST calls, where the intention is usually to retrieve information once and then use the same copy for many calculations.

readonly Data = ComplexSlowQuery;           // Run only once
readonly Cost = Data.Quantity * Data.Price; // Use like a variable

Affinity operator

To create complex formatted text in an easy way, Nytril introduced the affinity operator. In most programming languages, two operands must be separated by a binary operator (e.g. x * y) or else there is a syntax error. In Nytril, if the compiler encounters two operands without a binary operator between them, it inserts an invisible binary 'affinity' operator and continues without error. At runtime, the type of the two operands is assessed and if an affinity exists between the two types, the operation is executed. If there is no affinity between the types, there is a runtime error.

"123" "abc"
 ⇒ "123" + "abc"

3 meters

⇒ 3 * meters

Nytril allows the programmer to add left and right affinity between objects of different types, which can create a complex sub-grammar in the language which can increase clarity.

The 'each' Operator

The each operator is used to call a function with a parameter once for every item in an array. The resulting expression is an array of function return values.

Square(x) = x * x;
Square(each [1, 2, 3, 4]);

⇒ [1, 4, 9, 16]

If a function has more than 1 parameter, the each operator can be used on more than 1 argument. This raises the dimension of the output array.

Power(x, power) = x ^ power;
Power(each [1, 2, 3], each [1, 2, 3]);

⇒ [[1, 4, 9], [1, 4, 9], [1, 8, 27]];

Most unary and binary operators can also be used in conjunction with the each operator to provide a very compact expression.

(each [1, 2, 3]) ^ (each [1, 2, 3])

⇒ [[1, 2, 3], [1, 4, 9], [1, 8, 27]];

Revisions

A revision is a collection of properties and elements inside curly braces {} that are separated by semi-colons (;}. A property is a "name: value" pair such as TextHeight: 12 pts. An element is any other expression, including variables and values returned from function calls. The properties and elements of a revision are computed at runtime, so a revision "executes" just like a scope in a function.

A revision adds elements to, and modifies the properties of, format objects in a document. A revision acts on the format object to its left, without modifying it. The combination of the left-hand operand and the right-hand revision is a new revised object that has the properties and elements of the left-hand format/revision plus any additional elements of the right-hand revision, and the added (or overridden) properties of the right-hand revision. By stacking and combining revisions, hierarchical style dictionaries can be created easily in a manner that is similar to Cascading Style Sheets or to the way word processors allow styles to inherit from each other and override properties.

The following code creates a empty Paragraph and revises it with content and properties. The placement of properties has no effect, but the placement of elements is always in order.

Paragraph {1; 2; 3; "abc"; TextColor: Colors.Blue};

⇒ 123abc

The following code creates a paragraph 'style' called Heading1 by revising the Paragraph format with several properties. It then uses the Heading1 style by revising it with a string element.

Heading1 = Paragraph {TextColor: Colors.Green; TextWeight: Bold};
Heading1 {"Chapter 1"};

Chapter 1

Revisions + arrays

Special treatment is given to elements in a revision that are arrays. In this case, all of the elements of the array become elements of the revision, without changing the array. Combining this mechanism with the each operator, described above, allows for the succinct inclusion of iterated content.

Paragraph {
  Square(each [1, 2, 3, 4, 5]);
  Separator: ", ";
};

⇒ 1, 4, 9, 16, 25

Revisions with if/else

If statements can be used in a revision to programmatically include or exclude properties and elements from a revision based on a condition.

ShowCount(count) = Paragraph {
  "The count is "; count;
  if (count > 10)
    " and it's too big.";
  else
    TextColor: Colors.Green;
};
ShowCount(5);
ShowCount(20);

⇒ The count is 5
The count is 20 and it's too big.

Revisions with loops

for, foreach, do and while loop statements can be used in a revision to iteratively add elements.

ShowNumbers(int count) = Paragraph {
  Separator: ", ";
  for (int i = 0; i < count; ++i)
    for (int j = 0; j < count; ++j)
      i * j;  // This value gets added for each pass of the inner loop
};
ShowNumbers(4);

⇒ 0, 0, 0, 0, 0, 1, 2, 3, 0, 2, 4, 6, 0, 3, 6, 9