5 Standard Logix

Standard Logix is a Logix-defined language like any other. It is the language that the shell starts up in by default. This section describes Standard Logix and its various operators.

Standard Logix is experimental! It is likely to go through many upheavals before becoming a stable language. One of the great strengths of a meta-language like Logix is that it supports this kind of iterative, organic language design.

Standard Logix has a fairly transparent translation into Base Logix, and hence into Python. It feels very much like programming Python with a different syntax. If you don’t know Python, you are strongly recommended to learn some before programming Standard Logix.

The implementation of Standard Logix can be found in logix/std.lx (at the time of writing, just 324 lines of code)

5.1 Python Basis

Standard Logix inherits all of the operators from Base Logix (Logix’s Python-like language). In other words, pretty much everything you know from Python is available just the same in Standard Logix, unless stated otherwise in this section (e.g. the function-call syntax is different).

5.2 Function Calls

Function calls are simply a sequence of expressions:

[std]: max 3 6 4
6

Nested function calls need to be parenthesized.

[std]: max 4 5 (min 10 11)
10

There’s nothing special going on with these parentheses – they are simply grouping the call to min, just as you would use parentheses in any expression. Parentheses are not part of the function call syntax as they are in Lisp, Python and many other languages.

The function itself can be the result of an expression:

[std]: myFile.write "hello"

Zero argument functions are called with a special postfix operator ()

[std]: from random import random
[std]: print random
<built-in method random of Random object at 0x009B0368>
[std]: print random()
0.746969538583

This is not empty parentheses, it is a special operator.

5.3 Keyword Arguments

Keyword arguments are supported, with semantics equivalent to Python

[std]: result = db.search 'foo' caseSensitive=False

5.4 Lists

List syntax is like Python's.

[std]: [1,2,3]
[1, 2, 3]

List comprehensions are available.

[std]: [x**2 for x in range 5 if x != 2]
[0, 1, 9, 16]

A compact syntax is available for simple numeric ranges (it returns a Python xrange)

[std]: for x in [1..10]: print x
1
2
...
9
10

List subscripts cannot use the same syntax as Python – it would conflict with the function-call syntax, e.g.:

myList[3]

is in fact a call to the function myList with one argument, the list [3]. List subscripts instead look like this:

[std]: x = [1..10]
[std]: x/[3]
4

There must not be a space between the / and the [. Slices are similar:

[std]: "Hi there"/[3:]
'There'
[std]: "Hi there"/[::-1]
'erehT iH'

Logix introduces a data-type called an flist – a ‘field list’. It is a list that also contains named elements. It is somewhat like a combination of a list and a dict. In Standard Logix, if a list literal contains named fields, an flist is returned instead of a list.

[std]: [1, 2, a=3, b=4]
[1, 2, a=3, b=4]

The list comprehension is similarly extended to support flists.

[std]: names = 'a', 'b'
[std]: values = 1, 2
[std]: [n=v for n, v in zip names values]
[a=1, b=2]

This always produces an flist with no elements. A list and an flist can be added together (+ operator) to overcome this.

5.5 Dictionaries

Syntax for dictionaries differs from Python (although, confusingly, dicts still display using Python syntax – hey, it’s alpha!)

[std]: ['a':1, 'b':2]
{'a':1, 'b':2}

The empty dict is written:

[std]: [:]
{}

Logix also has dict comprehensions – just add a colon to the regular list comprehension syntax:

[std]: [ord x: x for x in "hello"]
{104: 'h', 108: 'l', 101: 'e', 111: 'o'}

As with Python, setting and getting dict entries uses the same syntax as list indexing.

[std]: d = [ord x: x for x in "hello"]
[std]: d/[101]
'e'

For the common case where the keys of a dict are names, a convenience operator is provided.

[std]: d = dict a=1 b=2
[std]: d/a
1

The divide operator is renamed to div (it is still infix).

5.6 Object Attributes

The traditional ‘.’ operator is available, but with some additional smarts. Firstly it is a smartspace operator. Smartspace is a Logix feature that allows white-space around operators to effect the scope of the operator. The motivation for using smartspace is that without it, OO expressions in Standard Logix can be awkward. Consider for example the following Python expression:

# A Python expression
db.find(department).getStaff(name).emailAddr

In Standard Logix, you might write that

((db.find department).getStaff name).emailAddr

Which is much less readable than the Python version. Would it look better without the parentheses?

# Incorrect Standard Logix
db.find department.getStaff name.emailAddr

That looks all wrong – it looks like you are trying to do the following.

db.find (department.getStaff) (name.emailAddr)

In fact, that is how it is parsed (we’ll see the correct way to write it momentarily).

By using a smartspace operator, OO statements are much easier to read. When there is no space around the ‘.’, parentheses are implied. For example, these two are equivalent:

text.setFont userprefs.standardFont

text.setFont (userprefs.standardFont)

Whereas the previous example must be written:

db.find department .getStaff name .emailAddr

The space before the ‘.’ causes the expression to be parsed the same as the original Python. This style may be unfamiliar, but it quickly becomes very easy to both write and read.

The ‘.’ operator has some extra features. The equivalent of getattr can be achieved like this

[std]: # equivalent to: getattr obj x
[std]: obj.(x)
[std]: # equivalent to: getattr obj x None
[std]: obj.(x, None)

It is relatively common to need to test on an object field, where the object reference may be None, like this:

[std]: if account and account.active: ...

Standard Logix provides a convenience operator .? for this situation. The following is entirely equivalent:

[std]: if account.?active: ...

5.7 Sequence Operators

Some of the basic operators have variations for convenient operations on sequences.

seq.*f                     is equivalent to    [x.f for for x in seq]

seq/*[key]           is equivalent to         [x/[key] for x in seq]

seq/*foo                is equivalent to         [x/foo for x in seq]

a ,* b ,* c         is equivalent to         zip a b c

5.8 Defining Functions

Function definitions look like Python without the parentheses or commas:

[std]: def lifeChangingFunction a b: return a+b

The return is in fact not required. Without it, the result of the function is the result of the last statement in the function body.

[std]: def lifeChangingFunction a b: a+b
[std]: def myMax a b: if a > b: a else: b

5.8.1 Argument Preconditions

Standard Logix allows argument preconditions to be defined inside the argument list. A precondition can either be

[std]: def reverse s(str): s/[::-1]
[std]: reverse 'foo'
'oof'
[std]: reverse 12
ArgPredicateError Traceback (most recent call last)
d:\desktop\livelogix\python\<console> in <interactive>()
d:\desktop\livelogix\python\<console> in reverse(s)
ArgPredicateError: reverse: arg-predicate failed for 's' (got 12)

Using Logix’s multiple language capabilities, the argument precondition is actually an expression in a special purpose language. This language is currently not very developed – it has only one operator. The ? operator is used to indicate that None is a valid argument. E.g. this function must be passed an Employee:

[std]: def promote employee(Employee): ...

While this one can be passed an Employee or None

[std]: def promote employee(Employee?): ...

(note that in many languages, including Java, the null value matches any type. In Python and hence in Logix, None is an instance of NoneType and matches no other type)

In the future there will be other operators, e.g. for conjoining and disjoining argument predicates.

5.9 Lightweight Lambdas

There is a nice lightweight syntax for anonymous functions:

{ <arguments> | <expression> }

For example

[std]: map {x|x**2} (range 5)
[0, 1, 4, 9, 16]

In the common case where there is only a single argument, the argument spec can be omitted. The single argument is called it.

[std]: map {it**2} (range 5)
[0, 1, 4, 9, 16]

(The same syntax is used in the Groovy scripting language for the Java VM - http://groovy.codehaus.org/)

A lambda function with multiple statements can be created by combining the statements with either the ; operator or the do operator.

The lambda operator supports argument preconditions.

5.10 Variable Function Arguments

Just as in Python, functions can receive variable arguments and variable keywords:

[std]: def f *args **kws: print args, kws

The calling syntax has to be slightly different to Python’s, to avoid ambiguity with the * and ** operators.

[std]: f 1 2 *:myList **:myDict

5.11 Testing Lists: forany and forall

The forall operator evaluates true if the test holds for all elements of a list:

if bark > bite forall bark, bite in dogs:
    print "You don't scare me!"

Notice that as with list comprehensions, list elements can be extracted into any valid assignable place (here we unpacked a pair into two variables).

The forany operator has deeply mysterious semantics that I doubt anyone will be able to fathom:

if weight > link.maxWeight forany link in chain:
    chain.break()

forall will stop iterating as soon as a test evaluates false. forany will stop iterating as soon as any test evaluates true.

5.12 Building Lists: listfor and listwhile

List comprehensions are great for succinctly building lists within expressions, but sometimes they don’t suffice, particularly if a temporary value is needed.

The listfor and listwhile operators are just like for and while, except they have a result:  a list made from the result of the loop body for each iteration (the result of the loop body is the result of the last expression). The operators also have an if clause similar to that found in list comprehensions.

Say you wanted to do:

[std]: x = [line.split()[0] + "," + line.split()[1]
            for line in myfile]

With list comprehensions, you can't avoid calling split twice. With listfor it looks like

[std]: x = listfor line in myfile:
     :         fields = line.split()
     :         fields[0] + "," + fields[1]

5.13 Scanning Lists: valfor and breakwith

It is common to use a for loop to search a list for a particular value, break-ing out of the list when the value is found. Python provides a very useful else clause that can be used to provide a default when the value is not found:

# Name of first employee in sales department
for e in employees:
    if e.department == 'sales':
        result = e.name
else:
    result = None

This however, is typical ‘inside out’ imperative programming – it is not clear that the whole construct is simply assigning a value to result.

For this situation, Logix provides valfor, to be used in conjunction with breakwith.

result = valfor e in employees:
             if e.department == 'sales'
                 breakwith e.name
         else:
             None

The construct is useful, but the keywords valfor and breakwith are rather clumsy – they are liable to change.

5.14 Function Pipelines

Python programmers have generally found list comprehensions to provide a nicely readable alternative to the map function, especially when multiple steps are required. For example, without list comprehensions, an expression like

[len(str(x)) for x in l]

requires something like

map len (map str l)

or

map {len (str it)} l

Neither are very pleasing to read. Logix provides another alternative which is both clear and concise: an infix map operator. With this operator, expressions read like a ‘pipeline’.

l *> str *> len

In other words “pipe l through str, and then len”.

In a similar vain, ?> filters a list according to a predicate function, and .> simply applies a function to a single value. Here is a pipe that uses all three.

names ?> {it.endswith '.dat'} *> str.lower .> ", ".join

Such expressions have the advantage that the order of operations reads left to right. The equivalent list comprehension reads backwards:

[", ".join (str.lower f)
 for f in names if f.endswith ".dat"]

5.15 Timing code

A minor but handy feature is provided for timing code. The do operator can be given a timed clause, with a message.

[std]: do timed "big loop": for x in range 10**7:
big loop block time: 2.328000

5.16 String Literals

Standard Logix includes a great idea for indentation-friendly multi-line string literals by Greg Ewing ( http://nz.cosc.canterbury.ac.nz/~greg).

longString = """|Eveything after the | is included.
                |Nothing needs to be escaped.
                |Leave an empty line at the end
                |to terminate with a newline (like this...)
                |
                "

The double-quote at the end is optional – it keeps emacs syntax highlighting on track!