So far we have barely touched upon Hyperlambda's syntax. To understand the syntax you'll need to understand the relationship between Hyperlambda and lambda objects, where lambda objects are to Hyperlambda the equivalent of what DOM is to HTML. To understand lambda objects again, it's useful to imagine some traditional C#/Java pseudo code.
class Node
{
string Name;
object Value;
List<Node> Children;
}
The above is the type declaration of "lambda objects" or "execution trees", and literally the "machine code executable format" that Magic executes. However, since all functions can be reduced down to graph objects or tree structures, hierarchical in nature, this implies that at least in theory, you can create any executable function that you can create with any other programming language using only the above type.
As you write Hyperlambda what you are doing is literally to dynamically declare an object somehow resembling the above type. Consider the [while] loop example we started out with a couple of articles ago.
.no:int:0
while
lt
get-value:x:@.no
.:int:20
.lambda
log.info:Howdy from while
math.increment:x:@.no
The above lambda object has one implicit root Node
, which again has two children called [.no] and [while]. Our while node again has two more children called [lt] and [.lambda], and so on. A colon :
separates the node's name from its value, and 3 spaces opens up the children collection, allowing you to declare children nodes of the node above.
The gory details
The Hyperlambda parser again, simply parses the Hyperlambda as structured text, and dynamically creates a Node
structure given the specified Hyperlambda according to the above ruleset. Then when the Hyperlambda parser has built a lambda object, it passes that lambda object as its sole argument into the [eval] slot (think "function" here). The [eval] slot again, obeys by an extremely simple recursive ruleset that is as follows.
void eval(Node node)
{
foreach(var idx in node.Children)
{
if (!idx.Name.StartsWith("."))
signal(idx.Name, idx);
}
}
The above is an oversimplification to some extent, but really, that's the entire "programming language" condensed. The above signal
method again, simply invokes an interface method on a class, where the type is taken from a string ==> type Dictionary
, and instantiates the type using the internal IoC container, allowing you to declare types in the hosting programming language that are created using dependency injection.
If your head hurts a bit now, just ignore the last two paragraphs, they're mostly intended as an explanation for those looking for more gory details. However, realise that Hyperlambda is the by far simplest and easiest programming language on the planet to implement, if for no other reasons than that there's no compilation and no interpretation occurring as it is "executing", only a simple parse operation, building a graph object, which then again is recursively executed as an "execution tree". A lambda object is hence to Magic what machine code is to your CPU.
This results in a sandbox environment, where the execution is happening virtualised, through the host programming language, providing slots as a service, similarly to how virtualisation happens in other places in computing. If a hypervisor is a virtual CPU or operating system, then Hyperlambda is a "hyper-executor", where the underlying logic is implemented in the host programming language, which for Magic's purpose happens to be C# and the .Net 6 runtime. However, you never have to bother with the underlying host programming language, C# or .Net in any ways. And you could probably create a Hyperlambda execution runtime in Java, PHP or C++ if you wanted to.
Hyperlambda's typing system
Even though Hyperlambda has no OOP, or any of the complex constructs found in other traditional programming languages, it needs to deal with native types at some point. With Hyperlambda you can declare types by injecting a type declaration in between the node's name and its value, and you have in fact already seen this syntax further up in this article in our while loop example. Let's repeat the important parts once more though.
.no:int:0
The above declares a node who's name is [.no], its value is 0
, and its type declaration is int
, implying the int
type from .Net 6. There's a whole range of types in Hyperlambda, and you can see most of these below, and how they map to .Net 6 System
namespace types.
- string = System.String
- short = System.Int16
- ushort = System.UInt16
- int = System.Int32
- uint = System.UInt32
- long = System.Int64
- ulong = System.UInt64
- decimal = System.Decimal
- double = System.Double
- single = System.Float
- float = System.Float - Alias for above
- bool = System.Boolean
- date = System.DateTime
- time = System.TimeSpan
- guid = System.Guid
- char = System.Char
- byte = System.Byte
- x = magic.node.extensions.Expression
- node = magic.node.Node
This allows you to for instance declare booleans such as illustrated below.
.source:bool:true
.result
if
get-value:x:@.source
.lambda
set-value:x:@.result
.:yup!
else
set-value:x:@.result
.:nope!
The above is a simple if/else
statement setting the value of the [.result] node according to the value of the [.source] node. If you execute it in Magic's evaluator, you will see something resembling the following.
Lambda expressions
The final piece of the puzzle now remaining for you to understand before you arguably know everything you need to know about Hyperlambda is "lambda expressions". Lambda expressions are to lambda objects what jQuery or DOM selectors are to your JavaScript. They allow you to reference node-sets in your code DOM, for then to use such expressions as arguments to slots, which uses these expressions to modify your code DOM somehow. A lambda expression is extremely simple to understand though, and in fact it's only a list of enumerables, chained together, who's combined results yields one or more nodes. Below is an example of using the [for-each] slot to iterate over some node-set and logging its values.
.data
item1:Hello from Item1
item2:Hello from Item2
item3:Hello from Item3
for-each:x:@.data/*
log.info:x:@.dp/#
The first expression above can be found in our [for-each] node, and is as follows @.data/*
. This expression basically says the following.
Find the first node upwards who's name is ".data", then return all of its children nodes
It might help to realise at this point that an expression has "iterators", and each iterator is separated with its previous iterator by a /
character. Hence this expression has two iterators, one starting with the character @
, implying "find first node who's name is xxx", where "xxx" is whatever follows the @
character. The next iterator is the *
iterator saying basically "give me all children nodes of nodes yielded by the previous iterator".
The internals of the [for-each] loop again, is that it will invoke its children as a lambda object, once for each node resulting from its expression - And as it invokes its children, it will inject a [.dp] node implying "data pointer", allowing the lambda object inside of the for-each loop to access the currently iterated node by reference.
The last part is crucial to understand, since the [for-each] loop allows its lambda object to modify the nodes it is iterating over, since the nodes are passed in by reference, implying "pointer semantics". This is why we need to use the weird #
iterator inside our [for-each] loop to retrieve the value of the currently iterated node by reference, since the node we are iterating over inside our [for-each] can be found in the value of the [.dp] node itself.
There exists a whole range of different iterators in Hyperlambda, and you can find most of these below.
- * Retrieves all children of its previous result
- # Retrieves the value of its previous result as a node by reference
- - Retrieves its previous result set’s “younger sibling” (previous node)
- + Retrieves its previous result set’s “elder sibling” (next node)
- . Retrieves its previous result set’s parent node(s)
- .. Retrieves the root node
- ** Retrieves its previous result set’s descendant, with a “breadth first” algorithm
- {x} Extrapolated expression that will be evaluated assuming it yields one result, replacing itself with the value of whatever node it points to
- =xxx Retrieves the node with the “xxx” value, converting to string if necessary
- [n,n] Retrieves a subset of its previous result set, implying “from, to” meaning [n1,n2>
- @xxx Returns the first node “before” in its hierarchy that matches the given “xxx” in its name
- n (any number) Returns the n’th child of its previous result set
Wrapping up
If your head hurts now, please realise that it only becomes easier from this point and onwards. Lambda expressions are probably the by far most complex subject that exists in Hyperlambda, since they demand great visualisation skills in your mind to imagine. However, fundamentally, they're the same as DOM selectors from HTML or XPath expressions from XML.
However, did you notice that we've now covered the entire programming language called Hyperlambda, at least its core parts, and we've never spoken about variables, not even once? That is literally the point, since everything is a variable in Hyperlambda, which is the reasons why Hyperlambda can treat code like data, and modify it, the same way traditional code can (only) manipulate data.
For Hyperlambda code is a variable, that can be semantically traversed, changed, and mutated into something else, which is the point about Hyperlambda, and why the computer can create code dynamically, in ways you could not even in theory accomplish in other programming languages.
Hyperlambda wasn't created for you to understand, it was created for your COMPUTER to understand ...
Top comments (0)