Let's make a Statement
Updated: Oct 12, 2022
Hey! How's it going?!
I hope you are all well and safe!
This time around, I'll keep this straight to the business so we can cover as much ground as possible since we never know when I'll have time for the next post.
If you need a refresh, you can take another look at the Introduction to CM Syntaxes article before moving forward here. As promised in that post, we're now going to dig into Statements. Learn what they are and how to leverage them to create nifty solutions for our projects.
Ok, a little back and forth.
Echoing the information I provided in the last article, here's a quick refresher on what Statements are:
...a syntactic unit of an imperative programming language that expresses some action to be carried out... A statement may have internal components (e.g., expressions). - Wikipedia
Or, you could think of Statements as Expressions that do NOT return anything.
Knowing that and having already gone through a crash course on Expressions for CM syntaxes, we are more than ready to move into actual Statements. We're going to jump right into compound statements.
Since we've already covered the "Hello World" theme last time, we will be more practical now.
Say that we want a shorter, more human-readable way to run a given function on top of all the items inside a collection. We want that to be usable for both CM Sets and CM Sequences, but we definitely don't want to write two different codes.
With the use of syntax, that's trivial. Here's what it would look like:
Go ahead, copy that definition along with the following test scenario and take it for a spin:
Cool right?! We even got the method overload to work as well!
Here's a small challenge: we want to allow our syntax consumers to easily say if they want the generated code to skip null objects automatically. How would you pull that off?
As you would - correctly - assume, there's no final/unique solution for this problem. It's a matter of how much time you have to work on it and how deep you want to go into creating a solution using that time.
In any case, here's my take on the above problem. Below you will also find some notes on why I choose to code it this way and its benefits and highlights.
As you probably noticed, the changes are very small, and the previous syntaxes should still work unchanged (that's one of the goals here - don't break "old" code). Here's the test code:
As promised, let's go through a list of interesting things to look for in the above syntax definition code:
We're optimizing the generated code, so we don't have to run an unnecessary null check for a collection that doesn't require it (for example, when we're iterating through a collection that holds value types);
We're relying on a core syntax any (available at lang\basic.syntax.cm) to read through the provided parameters' list;
By leveraging the actualArgList type (defined in \base\cm\syntax\SactualArgList.boot.cm) we are actually allowing for even more parameters/customization to be added in the future without the risk of breaking existing code;
To further explore this topic, let's see how we can improve on a task that I'm sure you see a lot of different implementations and code replication in every CET project you have ever worked on.
I'm talking about collection manipulation.
At the time of this writing (CET 11.5), there are six different collection types available in CM:
Sorted map (index)
Usually, suppose we want a given logic to be usable/applicable across many ( > 1 ) classes/structures in CM. In that case, we need to either write X different variants of the same logic or - sometimes - we can hardcode some if statements (along with type checks) to get this done.
By knowing your way around syntax, we add a third option to that list. It's fair to say that, in general, good Syntax does NOT care about the underlying type. Instead, they only care about contracts and the logic applied on top of it.
This entire set of syntax will be available in my Common.CET library repo. You can check all the other helpers I've created there and suggest a new one if you have one.
For now, let's start by covering a very basic yet useful case: firstOrDefault
We want to, for a given collection, fetch the very first object that matches certain criteria. It doesn't matter if you're searching for it inside an int, Snapper, or CustomObject Collection. All it matters is the following logic:
Iterate through a given Collection of Type X;
Find the element that matches the predicate X = Y;
If you find it, return that item typed as X;
Otherwise, return default(X);
So, say we have the following array:
[ 1, 2, 3, 4 ]
Then we could potentially have the following two things happening:
When our predicate is y=3 the result would be 3.
When our predicate is y=10 the result would be 0 (or, default(int) ):
Pretty simple, right? It should be.
But - there's always a but - if you try to apply the same logic on a Collection holding a different Object Type, you will NEED a different method that will do EXACTLY the same thing. Line by line. Instruction by instruction. The only difference is that it will look/compare/return another Type instead.
Let's solve it! Hold your breath, we're diving into yet another syntax!
And here are some use cases we can apply this to:
The above examples are executed not only on top of Collections with different element types but on top of different Collection types altogether;
It's null safe;
It Stops as soon as we find what we're looking for;
It only allocates the default after failing to find the desired item inside the collection.;
Are you up for a challenge?
I know you are!
Without going into the other variants available on the Github page I provided you earlier in this article, give it a try and adjust the previous code instead of finding the first item and returning that, return a new collection with all the elements match a certain predicate.
So, in short, here's what you should create (in fact, use this as the comment you should put on the top of your method):
Find all items that fulfills a given condition or returns an empty collection if no items are found.
Go ahead; I will wait.
Core already exposes two native syntaxes with some common names for this feature. Although Core's implementation doesn't do what we want this to do. Here's what we have in there:
findAll: Generic: Get a subset of the supplied list, filtered by type; (From: \base\cm\lang\collectionHelpers.cm)
all: Return true if all elements in @coll satisfy @cond. (From: \base\cm\lang\basic.syntax.cm)
Now that you've created your very own statement, I feel we're ready to put all that we've learned in a bucket and head for the goal! Next time we'll see and explore the final code. We will explore what it does and doesn't do (and why).
I hope you liked what you've learned so far; stay safe and see you next time!