Guide:Control flow

From IokeWiki
Jump to: navigation, search

Control flow

Ioke has some standard control flow operators, like most other languages. In Ioke, all of these are regular method calls though, and they can usually be implemented in terms of Ioke. This chapter will chiefly talk about comparisons, conditionals and iteration constructs.

Comparison

There are several comparison operators in Ioke, but the most important is called the spaceship operator. This operator is <=>. It takes one argument and returns -1, 0 or 1 depending on the ordering of the receiver and the argument. If the two objects can't be compared, it returns nil. If you implement this operator and mixin Mixins Comparing, you get the operators ==, !=, <, <=, > and >= implemented in terms of the spaceship operator. There are two other common operators in Ioke. The first =~, which can also be called the match operator. It's only implemented for Regexp right now. The === operator also exists, but implements matching slightly differently for all different types of objects. It is the basis for the case-expression. The contract of comparison operators is that they should return a true value (not necessarily the true) if the comparison is true, and otherwise return either false or nil.

The contract for === should be matching or not matching. It is among other things used in Ranges to see if something is included in that range or not.

iik> 1 + 2 < 4
+> true

iik> 3 + 2 < 4
+> false

iik> "foo" <=> "fop"
+> -1

Conditionals

Ioke has two different ways of doing conditionals. The first one is the default, and is also the traditional conditional from other languages. The second version looks more like Smalltalk conditionals.

As with everything else, these conditionals are all methods, and can be overridden and changed if need be. They can also be polymorphic.

The default conditionals are called "if" and "unless". They both take an initial evaluated argument that is used to check which branch should be taken. The "if" method will execute it's second argument if the first argument is true, and the third argument if the first argument is false. The "unless" method does the inverse -- executing the second argument if the first argument is false, and the third argument if the first argument is true. One or both of the branches can be left out from the statement. If no else-part is around and the conditional part evaluates to a false value, that false value will be returned.

A few examples are in order:

if(42 < 43,
  "wow, math comparison works" println,
  "we have some serious trouble" println)

if(42 < 43,
  "wow, math comparison works",
  "we have some serious trouble") println

unless(42 < 43,
  "convoluted math" println)

It is good style to not use "unless" with an else branch. It generally tends to not be so readable that way. Remember that "if" and "unless" return their values, which means they are expressions like everything else. The middle example show that you can just call println on the result of the if-call, instead of doing it twice inside. This is also good style. Assigning the result of an if-call is likewise not a problem.

In some languages you see a pattern such as "if(foo = someExpensiveMethodCall(), foo println)", where a variable is assigned in the condition evaluation so the value doesn't have to be evaluated twice. This works in Ioke too, but there is a more idiomatic way of doing it. Both "if" and "unless" establish a lexical context, where a variable called "it" is available. This variable will be bound to the result of the conditional. So the above idiom could instead be written "if(someExpensiveMethodCall(), it println)". This is the preferred way of handling regular expression matching.

The Smalltalk inspired way of doing conditionals rest on the methods called ifTrue and ifFalse. Both of these methods are only defined on true and false, which means they are not as general as the if and unless statements. They can also be chained together, so you can write:

(42 < 43) ifTrue("wowsie!" println) ifFalse("oh noes" println)

As should be obvious from these examples, these conditionals can not return any value. They must only rely on side effects to achieve anything.

Ioke also supports the expected short circuiting boolean evaluators. They are implemented as regular methods and are available on all objects. All of the expected combinators are available, including "and", "&&", "or", "||", "xor", "nor" and "nand".

cond

Ioke doesn't have any else-if expression, which means that when you want to do several nested checks, you end up with lots of indentation. Cond is a macro that expands to that code. The code using cond will not have more indentation, which means it might be easier to read. A cond has one or more conditional expressions followed by an action part to execute if that condition is true. As soon as a condition has evaluated to true cond will not evaluate any more conditions or actions. If no conditions evaluate to true, nil will be returned, unless there is an action part following the last action-part. In other terms, if the cond-expression has an odd number of arguments, the last argument is the default case to execute if nothing else matches.

Some examples of cond:

cond(
  x == 1, "one" println,
  x == -1, "minus one" println,
  x < 0, "negative" println,
  x > 0, "positive" println,
  "zero" println
)

As you can see, it becomes quite clear what happens here. Keep in mind that cond is an expression, just like anything else in Ioke, and will return the last value evaluated.

case

A thing that you very often want to do is to check one value against several different conditions. The case expression allows this to be done succinctly. The core to the case-expression is the === method, that is used for matching. The expression takes one value, then one or more conditionals followed by actions, and then an optional default part. Once something matches, no more conditionals will be executed. The conditional part should not be a complete conditional statement. Instead it should return something that implements a fitting ===. So, a small example follows:

case(value,
  Text, "it is a text!" println,
  1..10, "it is a low number" println,
  :blurg, "it is the symbol blurg" println,
  fn(c, (c+2) == 10), "it is 8" println,
  "we don't know it!" println)

The above example shows several different things you can match against, including a lexical block. The implementation of === for a lexical block will call the block with the value and then return true or false depending on the truth-value of the result of the call to the block.

A thing that can be inconvenient in some languages is to do combinations of several of these. Say you want to check that something is a Text and matches a regular expression, or it is either 5..10 or 15..20. In most cases you will end up having to write several conditional parts for at least one of those two. But Ioke allows you to use combiners in the conditional part of a case expression. These combiners will be rewritten before executed, so a combiner called "else" will actually use the method "case:else", that in turn returns an object that responds correctly to ===. The end result is that using combiners read really well, and you can define your own by prefixing the name with "case:". There are several standard ones. Using a few of them looks like this:

case(value,
  and(Text, #/o+/), "it's a text with several oos" println,
  or(5..10, 15..20), "numberific" println,
  else, "oh no!" println)

Combiners can be combined with each other and nested, so you could do and(foo, or(1, 2, 3), not(x)) if you want.

The available combiners are these:

and
Returns a matcher that returns true if all the arguments return true when calling ===. This is short circuiting.
or
Returns a matcher that returns true if any of the arguments return true when calling ===. This is short circuiting.
not
Takes one argument and returns the false if calling === on the argument returns true, and the other way around.
nand
The nand operation applied to === combiners.
nor
The nor operation applied to === combiners.
xor
The xor operation applied to === combiners.
else

otherwise

Returns a matcher that always returns true. This is useful to make the default argument read better.

Iteration

Ioke supports most of the expected control flow operations for iteration. The one thing that is missing is the for-loop. Since the for-loop encourages low level stepping, and can be replaced by other kinds of operations, I don't see any reason in having it in Ioke. In fact, the for-statement in Ruby is generally considered bad form too. And if someone really wants a for-loop it's really easy to implement. The name 'for' is also currently taken for list comprehensions.

loop

For creating infinte loops, the "loop"-method is the thing. It will just take a piece of code and execute it over and over again until some non-local flow control rips the execution up. Using it is as simple as calling it:

loop("hello" println)

x = 0
loop(
  if(x > 10, break)
  x++
)

The first example will loop forever, printing hello over and over again. The second example will increment a variable until it's larger then 10, and then it will break out of the loop.

while

The Ioke while loop works exactly like while-loops in other languages. It takes one argument that is a condition to reevaluate on each iteration, and another argument that is the code to evaluate each iteration. The result of the while-loop is the result of the last executed expression in the body.

x = 0
while(x < 10,
  x println
  x++
)

until

The until-loop works the same as the while-loop, except it expects its condition argument to evaluate to false. It will stop iterating when the conditional is true for the first time.

x = 0
until(x == 10,
  x println
  x++
)

times

A very common need is to iterate something a certain number of times. The Number Integer kind defines a method called "times" that does exactly this. It's got two forms - one with one argument and one with two arguments. With one argument, it will just run the argument code the specified number of times, and with two arguments the first argument should be the name of a cell to assign the current iteration value to and the second is the code to execute.

3 times("hello" println)

4 times(n,
  "#{n}: wow" println)

The first example will print hello three times, while the second example will count up from 0 to 3, printing the number followed by "wow".

each

For most iteration needs, you want to traverse a collection in some way. The standard way of doing this is with the "each"-method. It's defined on all central collection classes and is also the basis of the contract for Mixins Enumerable. The contract for each has three different forms, and all should be implemented if you decide to implement the each method.

The each method should -- as the name implies -- do something for each entry in the collection it belongs to. So calling each on a set would do something with each entry, etc. Exactly what that is depends on how many arguments are given to "each".

If one argument is given, it should be a message chain. This message chain will be applied to each element.

[:one, :two, :three] each(inspect println)

;; the above would execute:
:one inspect println
:two inspect println
:three inspect println

Another way of saying it is that the message chain will be executed using each element of the collection as receiver, in turn. The return value will be thrown away in this case, so to achieve anything, the code need to mutate data somewhere.

The second -- and most common -- form, takes two arguments. The first argument should be the name of a cell to assign each element to, and the second argument should be the code to execute. Under the covers, this form will establish a new lexical context for the code to run in. As with the first version, each return value will be trown away.

[2, 4, 6] each(x, (x*x) println)

Here, the name "x" will be used as the name of each element of the list in turn, while executing the code.

The final form of each takes three arguments, where the first is the name of a cell to assign the current index, and the other two arguments are the same as the above.

[2, 4, 6] each(i, x, "#{i}: #{(x*x)}" println)

The above code would print:

0: 4
1: 16
2: 36

seq

There are two different iterator protocols in Ioke. The first one is based on each as described in the previous section. The second protocol is slightly more general and is based on external iterators. The seq method is expected to return a Sequence object. This need to have two methods, next? and next. The first one returns true if next can be called again and false otherwise. The next method returns the next object in the sequence. This protocol can be used to implement each. If you have a seq method you can mixin Mixins Sequenced. This automatically makes your object Enumerable, gives you an each method and add several convenience methods. The methods on Mixins Sequenced and Sequence will be described further down.

break, continue

When executing loops it is sometimes important to be able to interrupt execution prematurely. In this cases the break and continue methods allow this for "loop", "while" and "until". Both break and continue work lexically, so if you send code to another method that uses these methods, they will generally jump out of a lexically visible loop, just like expected.

The break method takes an optional value to return. If no value is provided it will default to nil. When breaking out of a loop, that loop will return the value given to break. The continue method will not break out of the execution, but will instead jump to the beginning and reevaluate the condition once again.

while(true,
  break(42))

This code will immediately return 42 from the while-loop, even though it should have iterated forever.

i = 0
while(i < 10,
  i println
  if(i == 5,
    i = 7
    continue)
  i++
)

This code uses continue to jump over a specific number, so it will only print 0 to 5, and 7 to 9.

Comprehensions

Ioke's Enumerable mimic makes it really easy to use higher order operations to transform and work with collections of data. But in some cases the code for doing that might not be as clear as it could be. Comprehensions allow a list, set or dict to be created based on a more abstract definition of what should be done. The specific parts of a comprehension are generators, filters and the mapping. The generators are what data to work on, the filters chooses more specifically among the generated data, and the mapping decides what the output should look like.

The following example does three nested iterations and returns all combinations where the product of the number is larger than 100:

for(
  x <- 1..20, 
  y <- 1..20, 
  z <- 1..20, 
  val = x*y*z, 
  val > 100, 
  [x, y, z, val])

This code neatly shows all things you can do in a comprehension. The final argument will always be the output mapping, which in this case is a list of the three variables, and their product. The generator parts is first a name, the <- operator followed by an expression that is Enumerable. You can also see that one of the expressions is an assignment, that can be used later. Finally, there is a conditional that limits what the output will be. The more or less equivalent expression using Enumerable methods would be 1..20 flatMap(x, 1..20 flatMap(y, 1..20 filter(z, x*y*z > 100) map([x,y,z,x*y*z]))). In my eyes, the for-comprehension is much more readable.

There are two variations on this. The first one is when you want the output to be a Set of things instead of a List. The code is exactly the same, except instead of using for, you use for:set. There is also a for:dict version, for more esoteric usages.