UNPKG

sai-language

Version:

An object-oriented language designed to afford code comprehension and maintenance. Transpiles in-place to Javascript.

800 lines (475 loc) 26.7 kB
## Pipers __Pipers__ (or if you'd feeling particularly frunky, _piperators_) are a special kind of operator. They all are signified with a vertical pipe character `|` and then a descriptive keyword. They may be followed by one or more parameters. You can chain pipers, by putting them in sequence. You can also use pipers in a __chain__, just as you would use a member function of the object in the chain. Some pipers accept expressions, or multiple expressions, or even indented code blocks. ### | audit _collection observer_ [expr] |audit [expr] [expr] |audit (as [value ident] (, [key ident]) ) [block] [expr] |audit using [function] Audit is used to pass all values in a collection into an expression, code block or method. Audit doesn’t alter the collection, it just ‘pipes’ all values and keys/indices. set b to chain fruit |observe debug('Pre-sort') |audit debug('Fruit #${key} is ${it}') |by desc |observe debug('Post-sort') |audit debug('Fruit #${key} is ${it}') debug b > Pre-sort > Fruit #0 is Apple > Fruit #1 is Banana > Fruit #2 is Citron > Post-sort > Fruit #0 is Citron > Fruit #1 is Banana > Fruit #2 is Apple > [ 'Citron', 'Banana', 'Apple’ ] This example shows how one could add instrumentation to a process in a light-weight fashion. Neither **observe** nor **audit** alter the chained data. _A side effect of this is that **observe** can’t be used with true iterators as it’s impossible to statically observe an iterator without draining it._ ### | by _array sorting_ [expr] by [expr] (ASC/ASCENDING/DESC/DESCENDING) [expr] by ASC/ASCENDING/DESC/DESCENDING [expr] by ( as [first ident], [second ident] ) [block] [expr] by using [function] To create sorted array, use **by**. The newly resulting array will be sorted by the specified inline expression or code block. (**By** does not sort in-place; it always returns a new array.) If **by** is used on an iterable, it will **collect** all values before sorting. #### by inline When you use the inline expression form, the provided expression is used to extract a value from a single record, which is then compared against other records to determine the sort order. The **it** pronoun and its **dot** scoping prefix are available within the expression. Print a list of friends sorted alphabetically by name: set result to friends by .name debug (result thru .name).join(', ') > Ann, Doug, Ellie, Harry, Jenna, John, Marshal, Sara You may sort by more than one value by adding another **by** clause. Multiple sort clauses are handled the way a database would: if a clause provides no guidance (the values are identical), subsequent clauses are each checked in turn. Print sorted by length of name, then age: set result to friends by .name.length by .age debug (result thru '${.name}: ${.age}').join('\n') > Ann: 23 > Doug: 18 > John: 19 > Sara: 23 > Ellie: 22 > Jenna: 28 > Harry: 31 > Marshal: 21 #### by block When using **by** with a block of code, the pronouns change. You are given both records under consideration just as you would using the Javascript `.sort` method. And perhaps unsurprisingly to those who have ever seen a programming textbook, the pronouns are the letters **a** and **b**. Print a sorted list of ages by province: set result to friends by return a.province <=> b.province or a.age <=> b.age debug (result thru '${.name}: ${.province}, ${.age}').join('\n') > Doug: ON, 18 > John: ON, 19 > Marshal: ON, 21 > Sara: ON, 23 > Jenna: ON, 28 > Ellie: QC, 22 > Ann: QC, 23 > Harry: QC, 31 #### by using If you’ll recall, **has using** provides the ability to use a named function for your sorting facilitator, and **by using** allows the same thing. set ProvinceAge to task given a, b return a.province <=> b.province or a.age <=> b.age set result to friends by using ProvinceAge // same result as previous example #### desc (descending) _modifier_ .. [expr] by [expr] (desc) .. [expr] by desc Used with the **by** comprehension just like **asc** but the sort order will be highest to lowest. debug list Bob, Carol, Ted, Alice by desc > [ 'Ted', 'Carol', 'Bob', 'Alice' ] See also: **by** ### | collect _collect values from an iterator_ .. [iterator] collect If the expression on the left is an iterator, converts it to an array/list by draining the iterator; otherwise do nothing. If the iterator never ends, your system will lock up until you run out of memory. (You could use a **limit** comprehension to keep that from happening.) set Odds to process local i to 1 while i yield i set i + 2 debug Odds() collect // locks up debug Odds() limit 10 collect > [ 1, 3, 5, 7, 9, 11, 13, 15, 17, 19 ] The opposite of **collect** is **iterate**. The difference between **enlist** and **collect** is that collect will only transform an iterator, while **enlist** will transform all values. ### concat _concatenate array elements_ .. [array/iterable 1] concat [array/iterable 2] // return new array concatenating array 1 and 2 set var concat [array/iterable 2] // append array 2 to var A list operator, not a string operator, **concat** returns a list that consists of a copy of set 1 followed by a copy of set 2. Set 1 is not modified, a new array or iterable is returned. Undefined values are ignored, and scalar values are promoted to single element arrays. As follows: [1, 2] concat [3, 4] -> [1, 2, 3, 4] [1, 2] concat [[3, 4], [5, 6]] -> [1, 2, [3, 4], [5, 6]] [1, 2] concat 3 -> [1, 2, 3] [1, 2] concat {c:3, d:4} -> [1, 2, {c:3, d:4}] [1, 2] concat undef -> [1, 2] 1 concat [3, 4] -> [1, 3, 4] 1 concat 3 -> [1, 3] 1 concat {c:3, d:4} -> [1, {c:3, d:4}] 1 concat undef -> [1] {a:1, b:2} concat [3, 4] -> [{a:1, b:2}, 3, 4] {a:1, b:2} concat 3 -> [{a:1, b:2}, 3] {a:1, b:2} concat {c:3, d:4} -> [{a:1, b:2}, {c:3, d:4}] {a:1, b:2} concat undef -> [{a:1, b:2}] undef concat undef -> undef undef concat 3 -> [3] undef concat {c:3, d:4} -> [{c:3, d:4}] undef concat [3, 4] -> [3, 4] If the first set is an iterable, **concat** will return an iterable, otherwise it returns a list (draining the second list if it is an iterable). ### count _collection element count_ .. [collection] count Returns the number of elements in the collection. If the collection is an iterator, it will be drained. debug undefined count debug 1 count debug fruit count > 0 > 1 > 3 ### delete _removes elements_ delete [variable] set [variable] delete set [variable] delete [attribute(s)] ... [expr] delete [attribute(s)] When called on a variable or traits, undefines the named variable or attribute(s) of a variable. This just calls the underlying Javascript version of **delete**, with all the same rules and caveats. The only clever addition is the ability to delete multiple attributes at once by supplying a collection: set obj to: a 1, b 2, c 3, d 4, e 5, f 6 set obj delete 'a' debug obj > { b: 2, c: 3, d: 4, e: 5, f: 6 } set obj delete: 'c', 'f' debug obj > { b: 2, d: 4, e: 5 } set obj delete: b 'B is for Bonobo.', d 'D is for Dixie cups.' debug obj > { e: 5 } When **delete** operates on an array, it returns a copy of the array with the elements specified removed. debug (list 1,2,3,4,5) delete 1 > 1, 3, 4, 5 debug (list 1,2,3,4,5) delete (list 1,2) > 1, 3, 5 Elements are removed in first->last order, so sequence does matter. debug (list 1,2,3,4,5) delete (list 2,1) > 1, 4, 5 ### enlist _converts collections to an array_ .. [expr] enlist Converts various collection types into a list/array using the following rules: undefined -> undefined value -> [ value ] list -> self object -> [ [ key, value ], [ key, value ], ... ] iterable -> [ value, value, value, ... ] Note specifically that **enlist** will turn a set of key, value pairs into an array of [key,value] arrays thus no data is lost in the conversion. set data to: cats 7 dogs 12 debug data debug data enlist > { cats: 7, dogs: 12 } > [ [ 'cats', 7 ], [ 'dogs', 12 ] ] Such a result can be turned back into traits with **entrait**. The difference between **enlist** and **collect** is that **enlist** will always transform its source data into an array/list, while **collect** will only transform an iterator. ### entrait _converts collections to an object_ .. [expr] entrait Converts various collection types into traits/fields using the following rules: undefined -> undefined value -> { value: true } list -> { [0][0]: [0][1], [1][0]: [1][1], ... } object -> self iterable -> { first[0]: first[1], next[0]: next[1]... } **Entrait** is designed primarily to losslessly restore the results of enlist and iterators that produce arrays of key/value pairs. debug 'Coyote' entrait > { Coyote: true } set data to: list cats, 7 list dogs, 12 debug data debug data entrait > [ [ 'cats', 7 ], [ 'dogs', 12 ] ] > { cats: 7, dogs: 12 } **Entrait** ignores input data that does not conform to expectations; if given a list that does not contain two-element key/value lists, it will not create traits. You can use **thru** to craft suitable lists easily, however: debug fruit thru: it, true; entrait > { Apple: true, Banana: true, Citron: true, Durian: true } ### expects _object element validator_ ... task/promise/process expects [rules definition] .. [expr] expects [rules object] Used to check whether a function’s named parameters (or any arbitrary object) has certain traits, and optionally if those traits are of a particular type. #### expects clause When used as the clause in a **task**, **promise** or **process** function definition. **expects** adds run-time parameter checking to the function, passing the first argument (the one that holds named parameters) through an *expects check* based on the rules that follow. If any expectations are not met, a runtime error is thrown. Names appearing in **expects** clauses, and thus being used to check parameters, _must_ be preceeded by **$**. This is intended as a reminder that *named parameters are always referenced with the $ scoping operator*. #### expects operator Expects compares an object againt a set of rules and returns a list of rules that are **violated**. Thus, a successful test of expectations is an empty array. An array with one or more elements contains a list of the rules that were broken. #### expects rules Rules for **expects** take the form of a set of traits. Their names correspond to names of traits that must be found in the expression under test. The value, if not _true_, is interpreted as an object type; the trait must be of that type (or a child of that type). Any violations to the rules are returned in a list; that list being made up of plain objects with the following fields: .trait - name expected .expects - type expected .found - type found Here are some examples, finally: AddStudent task expects $name string, $age number @students.push copy $ .. @AddStudent name 'Sally', age '12' > Error: SAI: parameter exception in AddStudent > age should be number, but it's string Performing this same test in an ad-hoc fashion using the **expects** operator would look like this: set newStudent to: name 'Sally', age '12' set studentRules to: name 'string', age 'number' debug newStudent expects studentRules > [ { trait: 'age', expects: 'number', found: 'string' } ] N.B. the **expects** clause rule format is identical to that of a single-line **traits** definition, with the additional requirement of a **$** before every trait name. ### first _first element of an array_ .. [expr] first Returns the first value in a list or iterator. debug fruit first > Apple ### has _collection filtering_ .. [collection] has [expr] .. [collection] has [block] .. [collection] has using [function] The comprehension operator **has** indicates an expression based filter using the **it** _pronoun_ to represent the item currently under examination. Since **it** is in play, the associated **dot** scoping prefix is also active within the expression for easy access to item fields. #### has inline debug friends has (.province = 'QC') and (.cat or .dog) > [ { name: 'Ann', age: 23, cat: true, province: 'QC' } ] Any valid expression can be used in a **has** comprehension, and one can refer to values outside the expression as well. set rentAge to: ON 25, QC 21 debug friends has .age >= rentAge[.province] > [ { name: 'Ellie', age: 22, province: 'QC' }, > { name: 'Ann', age: 23, cat: true, province: 'QC' }, > { name: 'Harry', age: 31, province: 'QC' }, > { name: 'Jenna', age: 28, dog: true, province: 'ON' } ] #### has block Keyword **has** can reference a block of code directly, which makes the **it** pronoun available within that block, or be given the name of a task (or a task definition). You must **return** a _truthy_ (keep) or _falsy_ (discard) value from the block so the filter can take the according action. If you don’t return a value, all rows will be discarded. friends has return .age >= rentAge[.province] #### has using You can integrate function calls into a composed comprehension by adding the **using** keyword immediately following the comprehension keyword itself. set CanRent to task given row return row.age >= rentAge[row.province] debug friends has using CanRent ### highest _collection filter_ .. [collection] highest [expr] Returns the collection element with the highest value of the expression. Prepares the pronouns **it** and **key** for use by the expression. debug friends highest .age > { name: 'Harry', age: 31, province: 'QC' } ### into _reduce-like collection transformation_ .. into [initial sum] [expr] .. into [initial sum [code block] .. into [initial sum] using [function] Javascript features the array method `Array.prototype.reduce` which performs the reduction function, and SAI extends its applicability with the **into** compherension keyword. In addition to the item (**it**) and the item key (**key**) available in other comprehensions, an sum variable (**sum**) is used when evaluating **into** expressions. The value of the expression becomes the value of **sum** the next time the expression is evaluated. #### into inline The **sum** variable is initialized by the value following **into** — in the example below `0` — and then each row in the collection is visited and we add `.age` to it. This adds up the ages of every friend. debug friends into 0 sum + .age > 185 #### into block The **into** comprehension, like other comprehensions, has an inline version with an expression, and a long-form which takes a block of code. A more complex example. See if you can suss out how it works. debug friends into blank set sum[.province] to (self default 0) + 1 > { ON: 5, QC: 3 } Here’s a hint: **self** is a _pronoun_ used only in **set** statements. It is initialized to the previous value of the variable being changed by **set**. Another hint: **default** is an operator that evaluates to its right hand value if and only if the left hand value is _undefined_. Last hint: **blank** initializes an object with no traits; it is the SAI equivalent of Javascript’s `{}`. The comparable word for arrays/lists with no elements is **empty**; in Javascript you’d write `[]`. #### into using The addition of **using** lets you call an external function. The function must always `return` the value you wish to be used as the sum so the value can be preserved across function calls. (The block version of **into** takes care of this for you.) set ageTotal to task given accumulator, row return accumulator + row.age debug friends into 0 using ageTotal > 185 ### iterate _transform static data into an iterated form_ .. [collection] iterate .. chain [collection] iterate Ensures the collection is an iterable. If it already is an iterable, return it unchanged. If it has a **Iterate** method, call it and return that iterable. If it's an Array, produce an iterator over its values. If it's an object, produce an iterator that returns for each trait a 2-element array with trait name, trait value. If it's just a plain value, produce an iterator that returns that value. Using **iterate** in this way allows you to guarantee that processing is being done using an iteration rather than a static collection. To go the other way, use **collect**, which turns an iterable into an Array. The following example follows the one in **Iterator**: ply inventory by it.1 debug it > [ apple: 2, banana: 1 ] // undesired result ply inventory iterate by it.1 debug it > [ 'banana', 1 ] // correct result > [ 'apple', 2 ] The example wants to print out a sorted list of inventory items. The undesired result comes about because **by** needs access to the entire collection in order to sort it, so must process imperatively. Even if you give **by** an object with a **Iterate** method, which all objects and arrays have whether you put it there yourself or not, that method will not naturally be called. So, **by** converts the `inventory` object into an array so it can be sorted. The array conversion produces an array of all enumerable traits; in the case of `Tally` objects, the only enumerable trait is `bag`, and so an array that looks like `[ bag ]` is sorted (to no effect) and then printed one element at a time. When you force a call to **Iterator** with the **iterate** comprehension, you get the correct result you're expecting, as you thereby force **by** to use your custom iterator rather than a naive array conversion. The upshot is that right now SAI favours imperative collections rather than lazy iterators except when specifically using the **iterate** statement or comprehension. ### keys _get a list of object keys_ .. [collection] keys Returns the keys (or indices) of a collection’s elements. debug fruit keys debug friends first keys > [ 0, 1, 2 ] > [ 'name', 'age', 'cat', 'province'] ### last _last row of an array_ .. [collection] last Returns the last element in a collection as a stand-alone value or object. (If the collection is an iterator, it will be exhausted.) debug fruit last > Citron ### limit _select certain array/iterator elements_ .. [collection] limit [qty] .. [collection] limit [index], [qty] .. [string] limit [character count] .. [string] limit [start character], [character count] Returns the first **qty** elements in the collection if **qty** is positive. If negative, returns the last (absolute) **qty** number of elements. If an **index** is supplied, return **qty** elements starting at **index**. Negative index values are not supported for index. You always get a list back, even if just one element will be returned. limit -y last y rows limit 0 empty limit undefined everything limit +y first y rows limit -x, -y everything except last y rows starting x from end of list limit -x, 0 empty limit -x, undef last x rows limit -x, +y y rows starting x from end of list limit 0, -y everything except last y rows limit 0, 0 empty limit 0, undef everything limit 0, +y first y rows limit +x, -y everything except last y rows starting at x limit +x, 0 empty limit +x, undef everything starting at x limit +x, +y y rows starting at x If the collection is an iterable, it will only be fetched as many times as needed. (If you ask for elements offset from the end, the iterator will be exhausted because an iterator’s length can only be ascertained by exhausting it.) If the iterator is infinite and you ask for an offset from the end, you'll run out of memory as it will cache an infinite number of intermediate results order to comply with your impossible request. (Infinite iterators have no end.) #### limit with strings You can use __limit__ to extract substrings. For this purpose characters are treated as elements in the array. debug 'abcdef' limit 2,2 // cd debug 'abcdef' limit -3 // def debug 'abcdef' limit -3,2 // de debug 'abcdef' limit 1,-1 // bcde And so on. One "gotcha" is operator precedence; comprehensions have the lowest priority, so you will need to wrap them in parenthesis if using __limit__ in a logical expression. Do this: if '#!' is (line limit 0,2) // correct code Not this: if '#!' is line limit 0,2 // bad code if ('#!' is line) limit 0,2 // equivalent Another "gotcha" is the way __limit__ intentionally changes behaviour when its arguments change from negative to positive. If you pass calculated values that could become negative, __limit__ may return things you don't expect. ### lowest _filter an array_ .. [collection] lowest [expr] Returns the collection element with the lowest value of the expression. Prepares the pronouns **it** and **key** for use by the expression. debug friends lowest .age > { name: 'Doug', age: 18, province: 'ON' } ### observe _inspect an object without changing it_ .. [expr] observe [expr] .. [expr] observe ( as [value ident] ) [block] .. [expr] observe using [function] Evaluates the right expression using the left expression value as the **it** pronoun; however, **observe** always returns the original left hand expression, no matter the result of the right expression. set b to 'Fred' set c to b observe debug '${it.length} letters.' debug c > 4 letters. > Fred A warning: while you can **observe** an iterable expression, be careful that you don't drain it through observation. Heisenberg's uncertainty principle is very much at play with iterables: you can either have it or know what's in it. N.B. **observe** very useful in **chain** expressions. ### select _retrieve a subset of array/object elements_ .. [collection] select [keys] Return a new collection that is the subset of the original collection identified by the provided list of keys/indices. debug fruits select: 2, 0; > [ 'Citron', 'Apple' ] debug friends thru it select list name, age; > [ { name: 'Sara', age: 32 }, { name: 'John', age: 19 }, [ name: 'Ann', age: 23 } ] ### thru _transform a collection with a map-like operation_ .. [collection] thru [expr] .. [collection] thru ( as [it ident] (, [key ident] ) ) [code] return [new item value] .. [collection] thru using [function] Pass each element of a collection “thru’ an expression, code block, or previously defined function. The result of the expression becomes the new value in a copy of the collection. Conversion to uppercase: debug fruit thru .toUpperCase() > [ 'APPLE', 'BANANA', 'CITRON' ] A more complex formatting that could be an expression but I needed a block example: debug friends #cat thru return '${key+1}) ${.name}, age ${.age}, lives in ${.province}' > [ '1) Sara, age 23, lives in ON', '2) Jon, age 19, lives in QC', '3) Ann, age 23, lives in QC' ] Passing values **thru** a function: set rot13 to task set out to '' count $length set char to $charCodeAt(key) switch char >> 5 case 2,3 with char andb 31 if it > 26 nop elsif it > 13 set char - 13 elsif it > 0 set char + 13 set out + ~String.fromCharCode(char) return out debug fruit thru using rot13 > [ 'Nccyr', 'Onanan', 'Pvgeba' ] The function called by the **using** variant receives two parameters: set instrument to task debug $$ return $ debug fruit thru using instrument > { '0': 'Apple', '1': 0 } > { '0': 'Banana', '1': 1 } > { '0': 'Citron', '1': 2 } > [ 'Apple', 'Banana', 'Citron' ] ### update _alter multiple elements of a collection_ #### update in expressions .. [collection] update [collection] Creates a new collection that is a composite of a copy of the left collection with all of the values in the right collection overlaid. If discrete keys are available in the left or right collection, they will be used intelligently. Undefined values on the right will not be assigned. Sample data set list1 to list Apple, Banana, Citron set list2 to list Kiwi, undefined, Mango set traits1 to: a 1, b 2, c 3 set traits2 to: '1' 4, d 5 debug list1 update list2 > [ 'Kiwi', 'Banana', 'Mango' ] debug list1 update traits1 > [ 'Apple', 4, 'Citron', d: 5 ] debug traits1 update traits2 > { '1': 4, a: 1, b: 2, c: 3, d: 5 } debug traits1 update list2 { '0': 'Kiwi', '2': 'Mango', a: 1, b: 2, c: 3 } __Update__ also works with iterators and treats them like arrays. If the left side collection is **undefined**, it will be initialized **blank**. #### set update (in-place) set [variable] update [collection] Update a collection variable with a set of keys/values. When used like this in a __set__ statement, the original data is updated in-place. Works on lists and traits, and accepts lists, traits and iterators for update data. Updating a list: debug fruit > [ 'Apple', 'Banana', 'Citron' ] set fruit update: '1' 'Pear', '3' 'Guava' debug fruit > [ 'Apple', 'Pear', 'Citron', 'Guava' ] set fruit update list Grape, undefined, Melon debug fruit > [ 'Grape', 'Pear', 'Melon', 'Guava' ] Updating traits: set friend to friends.1 debug friend > { name: 'John', age: 19, cat: true, dog: true, province: 'ON' } set friend update: name 'Jon', province 'QC' debug friend > { name: 'Jon', age: 19, cat: true, dog: true, province: 'QC' } set friends update: undefined, friend debug friends [ { name: 'Sara', age: 23, cat: true, province: 'ON' }, { name: 'Jon', age: 19, cat: true, dog: true, province: 'QC' }, { name: 'Ellie', age: 22, province: 'QC' }, { name: 'Marshal', age: 21, dog: true, province: 'ON' }, { name: 'Doug', age: 18, province: 'ON' }, { name: 'Ann', age: 23, cat: true, province: 'QC' }, { name: 'Harry', age: 31, province: 'QC' }, { name: 'Jenna', age: 28, dog: true, province: 'ON' } ] If the left-side collection is undefined, **update** initializes it to **blank** before merging. ### values _return collection values stripped of keys_ .. [collection] values Returns the values of a collection’s elements. debug fruit values debug friends first values > [ 'Apple', 'Banana', 'Citron' ] > [ 'Sara', 23, true, 'ON' ]