|
|
(5 intermediate revisions by 3 users not shown) |
Line 5: |
Line 5: |
| == Comparison == | | == 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 isn't implemented for most objects. This operator doesn't do much right now, but is planned to be the basis of a Ruby-like case-statement further down the line. 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. | + | 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. | | 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. |
| | | |
− | <pre>iik> 1 + 2 < 4 | + | <source lang="ioke">iik> 1 + 2 < 4 |
− | +> true | + | +> true |
| | | |
− | iik> 3 + 2 < 4 | + | iik> 3 + 2 < 4 |
− | +> false | + | +> false |
| | | |
− | iik> "foo" <=> "fop" | + | iik> "foo" <=> "fop" |
− | +> -1</pre> | + | +> -1</source> |
| | | |
| == Conditionals == | | == Conditionals == |
Line 28: |
Line 28: |
| A few examples are in order: | | A few examples are in order: |
| | | |
− | <pre>if(42 < 43, | + | <source lang="ioke">if(42 < 43, |
− | "wow, math comparison works" println, | + | "wow, math comparison works" println, |
− | "we have some serious trouble" println) | + | "we have some serious trouble" println) |
| | | |
− | if(42 < 43, | + | if(42 < 43, |
− | "wow, math comparison works", | + | "wow, math comparison works", |
− | "we have some serious trouble") println | + | "we have some serious trouble") println |
| | | |
− | unless(42 < 43, | + | unless(42 < 43, |
− | "convoluted math" println)</pre> | + | "convoluted math" println)</source> |
| 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. | | 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. |
| | | |
Line 44: |
Line 44: |
| 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: | | 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: |
| | | |
− | <pre>(42 < 43) ifTrue("wowsie!" println) ifFalse("oh noes" println)</pre> | + | <source lang="ioke">(42 < 43) ifTrue("wowsie!" println) ifFalse("oh noes" println)</source> |
| As should be obvious from these examples, these conditionals can not return any value. They must only rely on side effects to achieve anything. | | As should be obvious from these examples, these conditionals can not return any value. They must only rely on side effects to achieve anything. |
| | | |
Line 55: |
Line 55: |
| Some examples of cond: | | Some examples of cond: |
| | | |
− | <pre>cond( | + | <source lang="ioke">cond( |
− | x == 1, "one" println, | + | x == 1, "one" println, |
− | x == -1, "minus one" println, | + | x == -1, "minus one" println, |
− | x < 0, "negative" println, | + | x < 0, "negative" println, |
− | x > 0, "positive" println, | + | x > 0, "positive" println, |
− | "zero" println | + | "zero" println |
− | )</pre> | + | )</source> |
| 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. | | 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. |
| | | |
Line 68: |
Line 68: |
| 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: | | 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: |
| | | |
− | <pre>case(value, | + | <source lang="ioke">case(value, |
− | Text, "it is a text!" println, | + | Text, "it is a text!" println, |
− | 1..10, "it is a low number" println, | + | 1..10, "it is a low number" println, |
− | :blurg, "it is the symbol blurg" println, | + | :blurg, "it is the symbol blurg" println, |
− | fn(c, (c+2) == 10), "it is 8" println, | + | fn(c, (c+2) == 10), "it is 8" println, |
− | "we don't know it!" println)</pre> | + | "we don't know it!" println)</source> |
| 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. | | 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: | | 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: |
| | | |
− | <pre>case(value, | + | <source lang="ioke">case(value, |
− | and(Text, #/o+/), "it's a text with several oos" println, | + | and(Text, #/o+/), "it's a text with several oos" println, |
− | or(5..10, 15..20), "numberific" println, | + | or(5..10, 15..20), "numberific" println, |
− | else, "oh no!" println)</pre> | + | else, "oh no!" println)</source> |
| Combiners can be combined with each other and nested, so you could do and(foo, or(1, 2, 3), not(x)) if you want. | | Combiners can be combined with each other and nested, so you could do and(foo, or(1, 2, 3), not(x)) if you want. |
| | | |
Line 104: |
Line 104: |
| == Iteration == | | == 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 canbe 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. For now I'm reserving the name if I would like to add comprehensions at some point. | + | 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 === | | === loop === |
Line 110: |
Line 110: |
| 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: | | 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: |
| | | |
− | <pre>loop("hello" println) | + | <source lang="ioke">loop("hello" println) |
| | | |
| x = 0 | | x = 0 |
| loop( | | loop( |
− | if(x > 10, break) | + | if(x > 10, break) |
| x++ | | x++ |
− | )</pre> | + | )</source> |
| 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. | | 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. |
| | | |
Line 123: |
Line 123: |
| 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. | | 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. |
| | | |
− | <pre>x = 0 | + | <source lang="ioke">x = 0 |
− | while(x < 10, | + | while(x < 10, |
| x println | | x println |
| x++ | | x++ |
− | )</pre> | + | )</source> |
| | | |
| === until === | | === until === |
| | | |
− | The until-loop works the same as the while-loop, except it expects it's condition argument to evaluate to false. It will stop iterating when the conditional is true for the first time. | + | 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. |
| | | |
− | <pre>x = 0 | + | <source lang="ioke">x = 0 |
| until(x == 10, | | until(x == 10, |
| x println | | x println |
| x++ | | x++ |
− | )</pre> | + | )</source> |
| | | |
| === times === | | === times === |
Line 143: |
Line 143: |
| 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. | | 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. |
| | | |
− | <pre>3 times("hello" println) | + | <source lang="ioke">3 times("hello" println) |
| | | |
| 4 times(n, | | 4 times(n, |
− | "#{n}: wow" println)</pre> | + | "#{n}: wow" println)</source> |
| 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". | | 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". |
| | | |
Line 157: |
Line 157: |
| If one argument is given, it should be a message chain. This message chain will be applied to each element. | | If one argument is given, it should be a message chain. This message chain will be applied to each element. |
| | | |
− | <pre>[:one, :two, :three] each(inspect println) | + | <source lang="ioke">[:one, :two, :three] each(inspect println) |
| | | |
| ;; the above would execute: | | ;; the above would execute: |
| :one inspect println | | :one inspect println |
| :two inspect println | | :two inspect println |
− | :three inspect println</pre> | + | :three inspect println</source> |
| 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. | | 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. | | 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. |
| | | |
− | <pre>[2, 4, 6] each(x, (x*x) println)</pre> | + | <source lang="ioke">[2, 4, 6] each(x, (x*x) println)</source> |
| Here, the name "x" will be used as the name of each element of the list in turn, while executing the code. | | 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. | | 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. |
| | | |
− | <pre>[2, 4, 6] each(i, x, "#{i}: #{(x*x)}" println)</pre> | + | <source lang="ioke">[2, 4, 6] each(i, x, "#{i}: #{(x*x)}" println)</source> |
| The above code would print: | | The above code would print: |
| | | |
− | <pre>0: 4 | + | <source lang="ioke">0: 4 |
| 1: 16 | | 1: 16 |
− | 2: 36</pre> | + | 2: 36</source> |
| + | |
| + | === seq === |
| + | |
| + | There are two different iterator protocols in Ioke. The first one is based on <code>each</code> as described in the previous section. The second protocol is slightly more general and is based on external iterators. The <code>seq</code> method is expected to return a Sequence object. This need to have two methods, <code>next?</code> and <code>next</code>. The first one returns true if <code>next</code> can be called again and false otherwise. The <code>next</code> method returns the next object in the sequence. This protocol can be used to implement <code>each</code>. If you have a <code>seq</code> method you can mixin <code>Mixins Sequenced</code>. This automatically makes your object Enumerable, gives you an <code>each</code> method and add several convenience methods. The methods on <code>Mixins Sequenced</code> and <code>Sequence</code> will be described further down. |
| | | |
| === break, continue === | | === break, continue === |
Line 185: |
Line 189: |
| 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. | | 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. |
| | | |
− | <pre>while(true, | + | <source lang="ioke">while(true, |
− | break(42))</pre> | + | break(42))</source> |
| This code will immediately return 42 from the while-loop, even though it should have iterated forever. | | This code will immediately return 42 from the while-loop, even though it should have iterated forever. |
| | | |
− | <pre>i = 0 | + | <source lang="ioke">i = 0 |
− | while(i < 10, | + | while(i < 10, |
| i println | | i println |
| if(i == 5, | | if(i == 5, |
Line 196: |
Line 200: |
| continue) | | continue) |
| i++ | | i++ |
− | )</pre> | + | )</source> |
| This code uses continue to jump over a specific number, so it will only print 0 to 5, and 7 to 9. | | This code uses continue to jump over a specific number, so it will only print 0 to 5, and 7 to 9. |
| | | |
| == Comprehensions == | | == 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 is 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. | + | 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: | | The following example does three nested iterations and returns all combinations where the product of the number is larger than 100: |
| | | |
− | <pre>for( | + | <source lang="ioke">for( |
− | x <- 1..20, | + | x <- 1..20, |
− | y <- 1..20, | + | y <- 1..20, |
− | z <- 1..20, | + | z <- 1..20, |
| val = x*y*z, | | val = x*y*z, |
− | val > 100, | + | val > 100, |
− | [x, y, z, val])</pre> | + | [x, y, z, val])</source> |
| 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. | | 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. | | 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. |
− |
| |
− | === Code ===
| |
− |
| |
− | Many of the things you do in Ioke will directly manipulate code. Since the messages that make up code is really easy to get hold of, this manipulation comes easy too. Ioke takes the Lisp philosophy of "code is data" to heart. The basic unit of a piece of code is a Message. A Message has a name, a next and prev pointer, and any number of arguments. When you manipulate a message, the argument list will contain messages too - and if the next or prev pointers are not nil, they will point to other messages. It serves well to remember that except for the message itself, all code will be evaluated in the context of a receiver and a ground. The ground is necessary because arguments to be evaluated need to be run in some specific context, even though the current receiver is not the same as the ground.
| |
− |
| |
− | The current types of code can be divided into three different categories. These are methods, macros and blocks. Native methods are all of the kind JavaMethod, but can have any kind of semantics - including semantics that look like macros. Most native methods do have the same semantics as regular methods, however.
| |
− |
| |
− | ==== Methods ====
| |
− |
| |
− | A method in Ioke is executable code that is activatable. A method can take arguments of several different types. The arguments to a method will always be evaluated before the code in the method starts to execute. An Ioke method is defined using the "method" method. All Ioke methods have the kind DefaultMethod. This leaves the room open to define other kinds of methods, if need be. DefaultMethod's could be implemented using macros, but at this point they aren't. A DefaultMethod can have a name - and will get a name the first time it is assigned to a cell.
| |
− |
| |
− | It is really easy to define and use a simple method. The easiest case is to define a method that is empty. This method will just return nil:
| |
− |
| |
− | <pre>m = method()
| |
− | m ;; call the method</pre>
| |
− | Since methods are activatable, when you name a cell that contains a method, that method will be invoked. To stop that behavior, use the "cell" method.
| |
− |
| |
− | The definition of a method can take several different pieces. These are a documentation string, definitions of positional required arguments, definitions of positional optional arguments, definitions of keyword arguments, definition of a rest argument, definition of a keyword rest argument and the actual code of the method.
| |
− |
| |
− | Let's take these one by one. First, if the the first element of a call to "method" is a literal text, and there is at least one more argument in the definition, then that text will be the documentation text for the method:
| |
− |
| |
− | <pre>;; a method that returns "foo"
| |
− | m = method("foo")
| |
− |
| |
− | ;; a method that returns nil, but
| |
− | ;; has the documentation text "foo"
| |
− | m = method("foo", nil)</pre>
| |
− | A method can take any number of required positional arguments. These will be checked when a method is called, and if not enough -- or too many -- arguments are provided, an error will be signalled.
| |
− |
| |
− | <pre>m = method(x, x println)
| |
− | m = method(x, y, z,
| |
− | x * y + z)</pre>
| |
− | The first method takes one argument and prints that argument. The second method takes three arguments and return the product of the two first added to the third.
| |
− |
| |
− | A method can also have optional positional arguments. In that case the optional arguments must follow the required arguments. Optional arguments need to have a default value -- in fact, that is how you distinguish them from required arguments. The arity of method calls will still be checked, but using minimum and maximum values instead. The default value for an argument should be code that can be executed in the context of the running method, so a default value can refer to earlier positional arguments. A default value can also do quite complex things, if need be, although it's not really recommended.
| |
− |
| |
− | <pre>;; takes zero or one arguments
| |
− | m = method(x 42, x println)
| |
− |
| |
− | ;; takes one to three arguments
| |
− | m = method(x, y 42, z 25,
| |
− | x*y + z)</pre>
| |
− | The syntax for optional arguments is to just write a space after the name of the argument, and then write the code to generate the default value after it.
| |
− |
| |
− | A method can also have keyword arguments. Keyword arguments are checked, just like regular arguments, and you can't generally give keyword arguments to a method not expecting it. Nor can you give unexpected keyword arguments to a method that takes other keywords. Keyword arguments can never be required. They can have default values, which will default to nil if not provided. They can be defined anywhere among the arguments -- the only reason to reorder them is that default values of other optional arguments can use prior defined keyword arguments.
| |
− |
| |
− | A keyword argument is defined just like a regular argument, except that it ends in a colon.
| |
− |
| |
− | <pre>m = method(foo:, bar: 42,
| |
− | foo println
| |
− | bar println
| |
− | )</pre>
| |
− | Just as with regular optional arguments, you supply the default value of the keyword argument after a space. The cells for the keyword arguments will be the same as their names, without the ending colon. The above code would print nil and 42 if no arguments were specified. It's important to remember that keyword arguments and positional arguments do not interact -- except for when calculating default values. When assigning values it's always possible to see what is positional and what is a keyword argument.
| |
− |
| |
− | Ioke methods can collect positional arguments into a list. This allow methods to take variable number of arguments. The rule is that all other positional arguments are first calculated, and the remaining positional arguments will be added to the rest argument. If no positional arguments are available, the rest argument will be empty. A rest argument is defined by preceding it with a plus sign in the argument definition. For clarity a rest argument should be defined last in the list, although it doesn't exactly matter anyway.
| |
− |
| |
− | <pre>m = method(+rest,
| |
− | rest println)
| |
− |
| |
− | m = method(x, y 42, +rest,
| |
− | rest println)</pre>
| |
− | The above code defines one method that only takes one rest argument. That means the method can take any number of arguments and all of them will be collected into a list. The second method takes one required argument, one optional argument and any number of extra arguments. So if four arguments are given, the rest argument will contain two.
| |
− |
| |
− | The final type of argument is keyword rest arguments. Just like positional rest arguments, a keyword rest argument can collect all keywords given to a method, no matter what. If a keyword rest argument is used, no conditions will be signalled if an unknown keyword is given to a method. If other keywords are defined, these keywords will not show up in the keyword rest argument. The keyword rest argument is defined by preceding the name with a +: sigil, and the keyword rest argument will be a Dict instead of a list. The keys will be symbols but without the ending colon.
| |
− |
| |
− | <pre>m = method(+:krest,
| |
− | krest println)
| |
− |
| |
− | m = method(x, y:, +rest, +:krest,
| |
− | [x, y, rest, krest])</pre>
| |
− | The above code first creates a method that can take any number of keyword arguments but nothing else. The second method takes one required positional argument, one keyword argument, rest arguments and keyword rest arguments, and returns a new list containing all the arguments given to it.
| |
− |
| |
− | The final argument to the method method should always be the code to execute. This code will be executed in the context of a receiver, that is the object the method is activated on. A method execution also happens in the context of the method activation context, where local variables are stored. This activation context contain some predefined variables that can be used. These are "self", "@", "currentMessage" and "surroundingContext". Both "self" and "@" refer to the receiver of the method call. "currentMessage" returns the message that initiated the activation of the method, and "surroundingContext" returns the object that represents the context where this method was called from. Both "self" and "@" can be used to specify that something should be assigned to the receiver, for example.
| |
− |
| |
− | <pre>createNewCell = method(
| |
− | @foo = 42
| |
− | )</pre>
| |
− | The method create above will create assign the value 42 to the cell "foo" on the object the method was called on.
| |
− |
| |
− | When calling a method, you specify positional arguments separated with commas. You can provide keyword arguments in any order, in any place inside the braces:
| |
− |
| |
− | <pre>;; the method foo takes any kind of argument
| |
− | foo
| |
− | foo()
| |
− | foo(1, 2, 3)
| |
− | foo(blarg: 42, 2, 3, 4)
| |
− | foo(quux: 42*2)</pre>
| |
− | To give a keyword argument, you just write it exactly like you define keyword arugments - a name followed by a colon.
| |
− |
| |
− | Sometimes it can be useful to be able to take a list of values and give them as positional arguments. The same can be useful to do with a dict of names. You can do that using splatting. This is done by preceding a list or a dict with an asterisk. This will result in the method getting the values inside of it as if the arguments were given directly. You can splat several things to the same invocation.
| |
− |
| |
− | <pre>dc = {foo: 42, bar: 13}
| |
− | ls = [1, 2, 3, 4]
| |
− | ls2 = [42, 43, 44]
| |
− |
| |
− | foo(*dc)
| |
− | ;; the same as:
| |
− | foo(foo: 42, bar: 13)
| |
− |
| |
− | foo(*ls)
| |
− | ;; the same as:
| |
− | foo(1, 2, 3, 4)
| |
− |
| |
− | foo(*ls2, 111, *dc, *ls)
| |
− | ;; the same as:
| |
− | foo(42, 43, 44, 111, foo: 42, bar: 13, 1, 2, 3, 4)</pre>
| |
− | If you try to splat something that can't be splatted, a condition will be signalled.
| |
− |
| |
− | ==== Macros ====
| |
− |
| |
− | The main difference between a macro and a method in Ioke is that the arguments to a macro is not evaluated before they are sent to the macro. That means you have to use macros to send raw message chains in an invocation. In most languages, this kind of feature is generally called call-by-name. When a macro gets called, it will get access to a cell called "call" which is a mimic of the kind Call. This gives access to information about the call and makes it possible to evaluate the code sent as arguments, check how many arguments are supplied, and so on.
| |
− |
| |
− | A macro is created using the "macro" cell on DefaultBehavior. This will return a mimic of DefaultMacro. Since macros can't define arguments, it's a bit easier to describe than mathods, but the things that can be done with macros is also a bit more interesting than what can be achieved with methods. One important thing to keep in mind is that most macros can not receive splatted arguments. In most cases keyword arguments aren't available either - but they could be faked if needed. Macros should generally be used to implement control structures and things that need to manipulate code in different ways.
| |
− |
| |
− | Just like a method, a macro gets evaluated on a specific receiver. It also gets the same kind of method activation context, but the contents of it is a bit different. Specifically, the context for a macro contains cells named "self", "@", "currentMessage", "surroundingContext" and "call". It's the "call" cell that is most important. It is a mimic of Call, and Call defines several important methods for manipulating the call environment. These are:
| |
− |
| |
− | ; arguments
| |
− | : This method returns a list containing the unevaluated arguments given to this message. Any kind of manipulation can be done with these arguments.
| |
− | ; ground
| |
− | : Returns the ground in which the call was initiated. This is necessary to evaluate arguments in their own environment.
| |
− | ; message
| |
− | : The currently executing message. This is the same as the "currentMessage" cell in the macro activation context.
| |
− | ; evaluatedArguments
| |
− | : Returns a list containing all arguments, evaluated according to the regular rules (but not handling splatting or keywords).
| |
− | ; resendToMethod
| |
− | : Allows a specific message to be resent to another method, without manually copying lots of information.
| |
− | These methods are a bit hard to understand, so I'll take some examples from the implementation of Ioke, and show how macros are used here.
| |
− |
| |
− | <pre>Mixins Enumerable map = macro(
| |
− | "takes one or two arguments. if one argument is given,
| |
− | it will be evaluated as a message chain on each element
| |
− | in the enumerable, and then the result will be collected
| |
− | in a new List. if two arguments are given, the first one
| |
− | should be an unevaluated argument name, which will be
| |
− | bound inside the scope of executing the second piece of
| |
− | code. it's important to notice that the one argument
| |
− | form will establish no context, while the two argument form
| |
− | establishes a new lexical closure.",
| |
− |
| |
− | len = call arguments length
| |
− | result = list()
| |
− | if(len == 1,
| |
− | code = call arguments first
| |
− | self each(n, result << code evaluateOn(call ground, cell(:n))),
| |
− |
| |
− | code = LexicalBlock createFrom(call arguments, call ground)
| |
− | self each(n, result << code call(cell(:n))))
| |
− | result)</pre>
| |
− | The code above implements map, one of the methods from Enumerable. The map method allows one collection to be mapped in a predefined way into something else. It can take either one or two arguments. If one argument is given, that is a message chain to apply, and then collect the results. If two arguments are given, the first is the argument name to use, and the second is the code to execute for each entry.
| |
− |
| |
− | The first step is to figure out how many arguments have been given. This is done by checking the length of the "call arguments" cell. If we have a length of one, we know that the first argument is a piece of code to apply, so we assign that argument to a cell called "code". Now, "code" will be a mimic of Message, and Message has a method called "evaluateOn", that can be used to fully evaluate a message chain. And that's exacty what we do for each element in the collection we are in. The result of evaluateOn is added to the result list. We use "call ground" to get the correct ground for the code to be evaluated in.
| |
− |
| |
− | If we get two arguments, it's possible to take a shortcut and generate a lexical block from those arguments, and then use that. So we call "LexicalBlock createFrom" and send in the arguments and the ground, and then call that piece of code once for each element in the collection.
| |
− |
| |
− | It is a bit tricky to figure out how macros work. I recommend looking at the implementations of some of the core Ioke methods/macros, since these use much of the functionality.
| |
− |
| |
− | ==== Blocks ====
| |
− |
| |
− | A lexical block allows the execution of a piece of code in the lexical context of some other code, instead of in a dynamic object scope. A lexical block does not have a receiver. Instead, it just establishes a new lexical context, and executes the code in that. The exact effect that has on assignments has been described earlier.
| |
− |
| |
− | A lexical block can be created using either the "fn" or the "fnx" methods of DefaultBehavior. The main difference between the two is that a block created with "fnx" will be activatable, while something created with "fn" will not. Lexical blocks handle arguments exactly the same way as methods, so a lexical block can take optional arguments, keyword arguments, rest arguments and so on. Both "fn" and "fnx" also take optional documentation text.
| |
− |
| |
− | A block can be invoked using the "call" method. A block can also be activated just like a method, if it is activatable. The default is to use "fn" to create inactive blocks though, since blocks are generally used to pass pieces of code around.
| |
− |
| |
− | <pre>x = fn(z, z println)
| |
− | y = fnx(z, z println)
| |
− |
| |
− | x call(42)
| |
− | y(42)</pre>
| |
− | A lexical block is a regular kind of object that can be assigned to any cell, just like other objects. Lexical blocks mimic LexicalBlock, and blocks don't have names. In contrast to methods and macros, no extra cells will be added to the activation context for a lexical block.
| |
− |
| |
− | ==== Lecros ====
| |
− |
| |
− | A macro works exactly like a method, in that it always has a receiver, and that receiver is available inside the macro as 'self' and '@'. In some circumstances it can be really useful to have a macro that behaves like a lexical block instead - being lexical so it can use cells defined outside of the definition of the macro. These macros won't have access to 'self' or '@', since they don't have a receiver in that way. Where such a macro is called is only based on namespacing.
| |
− |
| |
− | Ioke supports these kind of macros. They are all mimics of the kind LexicalMacro, and they are created using the method 'lecro'. A LexicalMacro is activatable by default, but a non-activatable lecro can be created using lecrox. The 'lecro' method takes the same arguments as 'macro', and the only real difference is the way it handles outside cells and the receiver value. A lecro also has a cell called outerScope that can be used if you need to explicitly access something in the outer name space - such as call.
| |
− |
| |
− | ==== Syntax ====
| |
− |
| |
− | Ioke supports loads of stuff with the standard macro, but sometimes these macros are a bit too low level for commonly used operations. Syntax is one of those cases you can achieve with macros, but you don't really want too. Many features in Ioke S is implemented using syntax.
| |
− |
| |
− | You can define syntax using the 'syntax' method. This returns a mimic of DefaultSyntax. You can use the same kind of cells in a syntax as you can in a macro. What is different with syntax is that syntax can only return one of two things. The first is nil, and the second is a message chain. A syntax will only be executed once at every point in the message chains, because after a syntax executes the first time, it will replace itself with thte result of that evaluation. If that evaluation returns nil, syntax will just remove itself from the message chain.
| |
− |
| |
− | You can use this for many things, but one of the more useful things you can do is translate a high level declarative definition of something into a low level executable version. That is exactly how for comprehensions are implemented.
| |
− |
| |
− | The syntactic macros are fairly advanced, and takes some time to grok. They are incredibly useful though, and they are used all over the standard library to achieve all manner of interesting things. Take a look there and things should hopefully become clearer. It's also a must to read the section on message chain manipulation and quoting later on to make syntax macros readable.
| |
− |
| |
− | ==== Destructuring ====
| |
− |
| |
− | A common problem with macros is that you want to take several different combinations of arguments, and do different things depending on how many you get. Say you might want to take one code argument, but also two optional arguments that should be evaluated. All of that code turns out to be highly repetetive, so Ioke contains a collection of syntax macros that make it easier to write these things. These are collectively called destructuring syntax.
| |
− |
| |
− | Let us say we have a macro that either [code], [evaluatedArgument, code], or [evaluatedArgument, code, evaluatedArgument]. The stuff that should happen is totally different for each of these cases. With a regular macro the code would look something like this:
| |
− |
| |
− | <pre>foo = macro(
| |
− | len = call arguments length
| |
− | case(len,
| |
− | 1,
| |
− | code = call arguments[0]
| |
− | ; do something with the code
| |
− | ,
| |
− | 2,
| |
− | arg1 = call argAt(0)
| |
− | code = call arguments[1]
| |
− | ; do something with the code and arg
| |
− | ,
| |
− | 3,
| |
− | arg1 = call argAt(0)
| |
− | code = call arguments[1]
| |
− | arg2 = call argAt(2)
| |
− | ; do something with the code and args
| |
− | ))</pre>
| |
− | As you can see it's really a lot of code to see what happens here, and it is very imperative in style. But, if I instead use dmacro - which is the destructuring version of macro - it looks like this:
| |
− |
| |
− | <pre>foo = dmacro(
| |
− | [code]
| |
− | ; do something with the code
| |
− | ,
| |
− | [>arg1, code]
| |
− | ; do something with the code and arg
| |
− | ,
| |
− | [>arg1, code, >arg2]
| |
− | ; do something with the code and args
| |
− | )</pre>
| |
− | dmacro will automatically check the length and extract the different arguments. The right arrow before the names of arg1 and arg2 marks that these should be evaluated. And what is more, dmacro will generate code that also generates a good condition if no argument matching works out. If you give zero arguments to the first version, it will fail silently. The dmacro will complain immediately. The dmacro destructuring syntax actually supports several more ways of ripping arguments apart. You can find this information in the doks for dmacro. Also, there are equivalent versions of dmacro for lecro, lecrox and syntax, called dlecro, dlecrox and dsyntax. They do the same thing, except they act like lecros or syntax instead.
| |
− |
| |
− | ==== Message chains ====
| |
− |
| |
− | In many cases a macro will take code that is not wrapped up inside of a method, macro or block. These pieces of code is called message chains, since their representation will be to a raw Message mimic. The chains are quite flexible, since they can be taken apart, modified and put together again. They can also be unevaluated and used as data definitions of some kind. That's how the argument handling to methods are implemented, for example. Since the call to "method" can be seen as a regular call to a macro, the argument descriptions are actually just unevaluated message chains that are picked apart to tease out the argument names. The same technique is applicable in any macro usage.
| |
− |
| |
− | The term message chain fragment is also used to specifically mean a message chain that is meant to be put together with something and evaluated. Picture a daisy chain that gets added at the end of another chain and then executed. That's what happens if you execute something like "[1, 2, 3] map(*2)". In this case the call to "*" with the argument 2 will be a message chain fragment that will be put together with a new receiver before execution.
| |
− |
| |
− | To handle syntax correctly - but also to generally handle manipulation of message chains - it is important to know about the available methods to do this. I have added quite a lot of nice stuff that makes it easy to work with message chains.
| |
− |
| |
− | First, messages are actually Enumerable, so you can use any Enumerable methods on them. The enumeration always starts at the receiver. It will not proceed into arguments, just following the next-pointer. To create a new message or message chain, there are several helpful methods and operators. The first method is called 'message' and takes an evaluated name and returns a new message with that name. 'Message from' takes one argument that will not be evaluated and returns a message chain corresponding to that argument. 'Message fromText' parses text and returns the message chain for it. 'Message wrap' takes an evaluated argument and returns a message that will always return that value. As will be mentioned later, Message has 'next=' and 'prev=' methods that you can use to set the next and previous pointers. Message also has 'appendArgument' and 'prependArgument' that allow you to add new arguments to the message arguments.
| |
− |
| |
− | The most used versions for creating message chains are short cuts for the above. Let us begin with creation. Instead of 'Message from' you can use '. That is a single quote mark. The message after that will be unevaluated and returned as a message chain. If you use a `, a backtick, that is equivalent to 'Message wrap'. And then we have '', that is two single quotes after each other. This message is generally called metaquote or quasiquote. It works the same as ', except that it will find any place where ` is used and insert the value of evaluating the message after the ` and insert that into the current message chain. Finally, '' will replace a `` with a literal ` message.
| |
− |
| |
− | You can add new arguments to a message by using the << operator. This operator returns the receiver.
| |
− |
| |
− | If you want to chain together a message chain, using next= and prev= is pretty tedious. You can instead use the -> operator. This will chain together the left hand side and the right hand side messages, and return the right hand side message.
| |
− |
| |
− | I think it is time for some examples:
| |
− |
| |
− | <pre>; create a new message with name foo
| |
− | x = 'foo
| |
− |
| |
− | ; add two arguments to the foo message
| |
− | arg = '(bar quux)
| |
− | (x << arg) << 'baz
| |
− |
| |
− | ; what we have done so far could be done with:
| |
− | x = '(foo(bar quux, baz))
| |
− |
| |
− |
| |
− | y = 'blurg
| |
− | ; chain together x and y
| |
− | x -> y
| |
− |
| |
− | ; the above is equivalent to
| |
− | if(y prev,
| |
− | y prev next = nil)
| |
− | x next = y
| |
− | y prev = x
| |
− |
| |
− | val = 42
| |
− |
| |
− | ; insert the message chain in x
| |
− | ''(foo bar(`val) `x)
| |
− |
| |
− | ; the above will return the same as
| |
− | '(foo bar(42) foo(bar quux, baz))</pre>
| |
− | To understand these operators, you need to have a clear understanding of how the internals of message chains work. Once that clicks, these should be fairly straight forward to understand.
| |
− |
| |
− | === Introspection and reflection ===
| |
− |
| |
− | Ioke supports quite fancy introspection and reflection capabilities. In Ioke, reflection includes the ability to change data dynamically, as well as introspect on it. Since the message based structure of a program is available at runtime, most things can be changed dynamically. The internal structure of a program is also very easy to inspect.
| |
− |
| |
− | All objects in Ioke have some core methods that are used to look at them. Some come from Base, and some come from DefaultBehavior. The rest of the reflection and metaprogramming capabilities belong to the Message kind.
| |
− |
| |
− | ===== kind =====
| |
− |
| |
− | Should be a text that gives the full name of the kind this object is closest mimic to. Except for nil, true and false, this will return a text that starts with a capital letter.
| |
− |
| |
− | <pre>Foo = Origin mimic
| |
− | Foo mimic kind println
| |
− |
| |
− | Foo Bar = Origin mimic
| |
− | Foo Bar mimic kind println</pre>
| |
− | This code will first print "Foo", and then print "Foo Bar", since an object assigned to a cell with a capital initial letter will get a new kind value.
| |
− |
| |
− | ===== kind? =====
| |
− |
| |
− | Takes one text argument and returns true if the object has that kind anywhere in its mimic chain.
| |
− |
| |
− | <pre>Foo = Origin mimic
| |
− | foo = Foo mimic
| |
− | foo kind?("foo") ;; false
| |
− | foo kind?("Foo") ;; true
| |
− | foo kind?("Text") ;; false
| |
− | foo kind?("Origin") ;; true
| |
− | foo kind?("Ground") ;; true
| |
− | foo kind?("DefaultBehavior") ;; true
| |
− | foo kind?("Base") ;; true</pre>
| |
− | As you can see in this example, "kind?" can return true for several different texts.
| |
− |
| |
− | ===== notice =====
| |
− |
| |
− | When looking at objects, there are two ways to get information about them, notice and inspect. If you want a brief description of an object where it's important that the description doesn't take up much space, notice should be used. If an exhaustive description is needed, inspect should be used instead. For some objects these both return the same thing, but for Origin mimics, the difference is large.
| |
− |
| |
− | <pre>Foo = Origin mimic
| |
− | Foo x = "blarg"
| |
− | Foo y = 42
| |
− | Foo notice ;; Foo_0x7CBDE6</pre>
| |
− | The default notice for Origin mimics will combine the kind and the unique hex id for the object, and create a text of that. The "notice" method should be overridden to provide better information in most cases.
| |
− |
| |
− | ===== inspect =====
| |
− |
| |
− | In contrast to notice, inspect is used to get exhaustive information. If a composite object is asked for its inspect, a quite large dump of information will often be shown. For a new object the output will be smaller, but usually still larger than the notice for it.
| |
− |
| |
− | <pre>Foo = Origin mimic
| |
− | Foo x = "blarg"
| |
− | Foo y = 42
| |
− | Foo inspect println</pre>
| |
− | This will print:
| |
− |
| |
− | <pre> Foo_0x7CBDE6:
| |
− | kind = "Foo"
| |
− | x = "blarg"
| |
− | y = 42</pre>
| |
− | If another representation makes more sense for inspection, "inspect" should definitely be overridden by custom objects.
| |
− |
| |
− | ===== uniqueHexId =====
| |
− |
| |
− | This method is used to return a text that contains a unique hex identity for an object. This text is guaranteed to be unique within a virtual machine for any object, and is the mechanism that "inspect" and "notice" uses.
| |
− |
| |
− | ===== cellSummary =====
| |
− |
| |
− | Calling "inspect" on any Origin mimic will dispatch to "cellSummary", which displays all the cell information about a specific object.
| |
− |
| |
− | ===== cell =====
| |
− |
| |
− | The "cell" method can be used for two different things. The first one is to get access to a value without activating it, and the second is to get access to a cell based on a name that you don't know at the time you're writing the program. The "cell" method takes one argument that is the name of the cell to fetch. The fetching works the same as regular cell lookup, except that activation doesn't happen. This means that a condition will still be signalled if you try to get something that doesn't exist.
| |
− |
| |
− | <pre>x = 42
| |
− | cell(:x) ;; 42
| |
− |
| |
− | x = method()
| |
− | cell(:x) ;; the method object
| |
− |
| |
− | name = :foo
| |
− | cell(name) ;; the value of foo</pre>
| |
− | ===== cell= =====
| |
− |
| |
− | Just as with "cell", "cell=" can be used to set cells that you don't know the name of at the time of writing the program. As discussed in the chapter on assignment, cell= can also be used to set cells that can't be set in the regular way due to naming strangeness.
| |
− |
| |
− | ===== cell? =====
| |
− |
| |
− | If you're not sure if a cell exists, using "cell?" is the way to find out. Give it a name and it returns true or false depending on if that cell exists.
| |
− |
| |
− | ===== cellNames =====
| |
− |
| |
− | If you want to get the names of all the cells an object contains, you can get that with cellNames. By default, cellNames will only return the names of cells that belong to the receiver, but if an argument of true is sent to the call, the resulting list will contain the names of all cells of all mimics too.
| |
− |
| |
− | <pre>x = Origin mimic
| |
− | x f = 42
| |
− | x cellNames ;; return [:f]
| |
− |
| |
− | x cellNames(true) ;; return a long list, including :f</pre>
| |
− | ===== cells =====
| |
− |
| |
− | Just like cellNames return the names of all the cells, the "cells" method will return a Dict of all the cells with their values. And just like cellNames, cells take an optional boolean argument on whether to include the cells of mimics too.
| |
− |
| |
− | <pre>x = Origin mimic
| |
− | x f = 42
| |
− | x cells ;; return {f: 42}
| |
− |
| |
− | x cells(true) ;; return a large dict, including f: 42</pre>
| |
− | ===== mimics =====
| |
− |
| |
− | Returns a list of all direct mimics of the receiver.
| |
− |
| |
− | ===== mimics? =====
| |
− |
| |
− | Takes one object as argument and returns true or false depending on if the argument is anywhere in the receiving objects mimic chain.
| |
− |
| |
− | ===== message =====
| |
− |
| |
− | Takes one symbol argument and creates a new Message mimic with that argument as its name.
| |
− |
| |
− | ===== Message code =====
| |
− |
| |
− | Returns a text that describes the code this message chain represents. It will hide some of the internal shuffling, but operators will be displayed using canonical form.
| |
− |
| |
− | ===== Message formattedCode =====
| |
− |
| |
− | Returns a text that is formatted and indented in a canonical way. This method is used to generate the documentation code for DokGen, among other things.
| |
− |
| |
− | ===== Message evalArgAt =====
| |
− |
| |
− | Takes the index of the argument to evaluate, and the ground to evaluate it on - returns the result of evaluating the argument.
| |
− |
| |
− | ===== Message fromText =====
| |
− |
| |
− | Takes one text argument that should contain Ioke code, and returns the parsed message chain from that code, without evaluating it.
| |
− |
| |
− | ===== Message doText =====
| |
− |
| |
− | Takes one text argument that should contain Ioke code, and returns the result of evaluating that code in the current context.
| |
− |
| |
− | ===== Message filename =====
| |
− |
| |
− | Returns the filename where the receiving message was defined.
| |
− |
| |
− | ===== Message line =====
| |
− |
| |
− | Returns the line number where the receiving message was defined.
| |
− |
| |
− | ===== Message position =====
| |
− |
| |
− | Returns the position in the line where the receiving message was defined.
| |
− |
| |
− | ===== Message name =====
| |
− |
| |
− | Returns the name of the message. The name of the message is what you generally talk about when saying you send a message. It can also be called the selector in other languages.
| |
− |
| |
− | ===== Message name= =====
| |
− |
| |
− | Update the message with a new name. From this point on the message will only have that name, so doing this on a message that is part of an existing message chain will change the behavior of that code:
| |
− |
| |
− | <pre>msg = Message fromText("2 + 1")
| |
− | msg next name = "-"
| |
− | msg code ;; "2 -(1)"</pre>
| |
− | ===== Message prev =====
| |
− |
| |
− | Returns the prev pointer of this message, or nil if no prev pointer exists.
| |
− |
| |
− | ===== Message prev= =====
| |
− |
| |
− | Sets the prev pointer of a message. The new value should be either nil or another message.
| |
− |
| |
− | ===== Message next =====
| |
− |
| |
− | Returns the next pointer of this message, or nil if no next pointer exists.
| |
− |
| |
− | ===== Message next= =====
| |
− |
| |
− | Sets the next pointer of a message. The new value should be either nil or another message.
| |
− |
| |
− | ===== Message sendTo =====
| |
− |
| |
− | Sends a message to an object. It's important to realize that sendTo will not evaluate the whole message chain -- it will only send one message with arguments to an object.
| |
− |
| |
− | ===== Message keyword? =====
| |
− |
| |
− | Returns true if this message is a keyword message, and false otherwise.
| |
− |
| |
− | There are many more methods that can be used to do interesting introspection and reflection in Ioke. The reference documentation includes them all. Most of the interesting stuff can be found on the Message kind.
| |
− |
| |
− | ===== send =====
| |
− |
| |
− | In many cases you want to activate something activatable in a cell, with a specific receiver. You can do this using send. You can send messages to most objects. Send takes one argument that should evaluate to the name of the message to send, and then sends along all other arguments given to send to the new message. These arguments will remain unevaluated, and will be evaluated at the leisure of the final method activated.
| |
− |
| |
− | === Aspects ===
| |
− |
| |
− | In many circumstances you might want to do several different things in a specific method. That generally goes against good software engineering, that says every piece of code should only have one responsibility. Aspect oriented programming tackles this by allowing you to slice code along a different dimension compared to the regular object oriented paradigm. Ioke has a small version of this, that allow you to add advice to cells. This advice can be before, after or around. All of them will get access to the arguments given in the original message, but after-advice also gets access to the value returned from the main cell. Finally, around-advice also gets the responsibility to invoke the original cell, so an around-advice could choose to not invoke it at all, or it could invoke it with different arguments, or maybe invoke it several times.
| |
− |
| |
− | Ioke currently only allows you to add or remove advice. To be able to remove advice, you need to name it when adding it. Unnamed advice can not be removed. Advice can be wrapped. It will be applied in an outside-in approach, where the most recently added advice will execute first.
| |
− |
| |
− | The return value of before and after advice doesn't matter, but the return value of around-advice will be interpreted as the new return value of the message send.
| |
− |
| |
− | To manipulate advice in Ioke, you need to describe what to manipulate. This description will return a Pointcut, that can then be used to inject advice at that point. Pointcuts can be defined in several ways. The easiest is to define it in terms of a cell name. Using this approach is the only way to define advice for non-existent cells.
| |
− |
| |
− | To create a Pointcut, call before, after or around on the receiver where the Pointcut should belong. The arguments specify what should be matched by the pointcut. You can see some examples of pointcuts here:
| |
− |
| |
− | <pre>; matches cells foo and bar
| |
− | Text before(:foo, :bar)
| |
− |
| |
− | X = Origin mimic
| |
− | ; will not match anything
| |
− | X around(matching: :anyFromSelf)
| |
− |
| |
− | X foo = 123
| |
− | ; will now match foo
| |
− | X around(matching: :anyFromSelf)
| |
− |
| |
− | ; any cell name matching the regexp
| |
− | X after(matching: #/foo/)
| |
− |
| |
− | ; matches any at all, except foo
| |
− | X before(matching: :any, except: :foo)
| |
− | ; matches any at all, except anything that matches foo
| |
− | X before(matching: :any, except: #/foo/)
| |
− |
| |
− | ; use a block for matching
| |
− | X around(matching: fn(x, x == :blurg))
| |
− |
| |
− | ; use a list to provide alternatives
| |
− | X before(matching: [#/foo/, #/bar/])</pre>
| |
− | As you can see from these examples, the pointcuts can be fairly advanced and specific in what they match. The option to send in a block makes it possible to check any property while matching.
| |
− |
| |
− | OK, once you have a Pointcut, there are three different methods you can call. These are <<, add, and remove!. The first one adds an unnamed advice, the second adds a named advice and the third one removes a named advice. Advice is any object that can be activated or called, so a method, a block, a macro or a syntax is fine.
| |
− |
| |
− | It's time to see what it looks like to add advice. Before-advice is the easiest kind. The important thing to remember is that the code will get the same arguments as the original call, which means it will also signal an error if the arguments doesn't match.
| |
− |
| |
− | <pre>X before(:foo) << method(x, "got #{x}" println)
| |
− | X before(matching: :anyFromSelf) << macro(
| |
− | "called #{call message name}" println)</pre>
| |
− | This code doesn't do anything strange at all.
| |
− |
| |
− | Next up we have after-advice. The only difference here is that after-advice automatically gets a cell that is set to adviceResult, that can used inside the method.
| |
− |
| |
− | <pre>X after(:foo) << method(+args,
| |
− | "foo resulted in: #{aspectResult}" println)</pre>
| |
− | Remember that the result value of before and after advice doesn't count. It is thrown away. But these methods can still affect things by side effects. They can also be used for validation. A condition signalled inside of an advice would have the same effect as if done in the method itself - namely interrupting the flow of control.
| |
− |
| |
− | The final advice is around advice. These are different in two ways. First, they get access to a cell called aspectCall, which can be used to invoke the real cell (and the next chain of advice, of course). The second difference is that the result of the around advice will be the return value from the cell. So, you can imagine the around-advice executing instead of the original code. If you forget to invoke it, the original cell won't be invoked at all.
| |
− |
| |
− | <pre>X around(:non_existant) << method(
| |
− | 42)
| |
− |
| |
− | X around(:foo) << macro(arg1, arg2,
| |
− | "before" println
| |
− | res = aspectResult(arg2, arg1)
| |
− | "got: #{res}" println
| |
− | 42)</pre>
| |
− | The first piece of code show that you can actually use an around advice around something that doesn't exist. But if you do call aspectCall inside of it, that will generate a NoSuchCell condition, of course. In the second example we first log some information, then invoke the original code with switched argument order, saves away the result, prints the result, and finally returns 42. There you can see most of the things that can be done inside of an around macro.
| |
− |
| |
− | The aspect system in Ioke is implemented in Ioke itself, and is fairly small at the moment. The guiding principle behind it is that it shouldn't have an impact on code that doesn't use it. It is a highly useful feature that makes it possible to decompose code substantially. As an example of a common place for aspect usage is initialization. This is defined as an around-advice that looks like this:
| |
− |
| |
− | <pre>Origin around(:mimic) << method(+rest, +:krest,
| |
− | newMimic = aspectCall
| |
− | if(newMimic cell?(:initialize),
| |
− | newMimic initialize(*rest, *krest))
| |
− | newMimic)</pre>
| |
− | Note first that the around advice takes any kind of arguments, but doesn't send them along to mimic. Instead it checks if there is an initialize-cell, and in that case invokes it with the arguments given to the mimic call. It finally returns the original result.
| |
− |
| |
− | === Importing ===
| |
− |
| |
− | To import new code into Ioke, the method "use" should be used. The terminology is that you give "use" a name of a module to include, and if that module has already been included it will not be evaluated again. If it has not been included, "use" will try to find the module using several different heuristics. It will add an ".ik" to the name and try to look up a file of that name - but it will also try to look it up without a ".ik", if a full file name has been given. Several different places will be searched, and the exact order and what will be searched can be found by calling the "System loadPath" method. This will return a list of all the places to search for modules. This list can be modified to add more directories to the load path.
| |
− |
| |
− | If a module of the specified name can't be found, a condition will be signalled.
| |
− |
| |
− | === Core kinds ===
| |
− |
| |
− | Ioke obviously contains lots of different data types, but there are some that are much more important than others. In this chapter I'll take a look at these and talk about how to work with them correctly.
| |
− |
| |
− | ==== Conditions ====
| |
− |
| |
− | One of the major parts of Ioke is the condition system. Unlike most other programming languages, Ioke doesn't have exceptions. Instead it has conditions, where errors are a specific kind of condition. The condition system comprises several different things. Specifically, the condition system uses the kinds Condition, Restart, Handler and Rescue. Restarts are mostly orthogonal to the rest of the system.
| |
− |
| |
− | The way the condition system works is this. When something happens, a program can elect to signal a condition. This can be done using "signal!", "warn!" or "error!". Both "warn!" and "error!" uses "signal!" under the covers, but do some other things as well. A condition will always mimic Condition. Each of these three methods can be called in three different ways. First, you can call them with a text argument. In that case the signalled condition will be the default for that type. (The default for "signal!" is Condition Default. The default for "warn!" is Condition Warning Default and the default for "error!" is Condition Error Default). A new mimic of the default condition will be created, and a cell called text will be set to the text argument. The second variation is to give an instance of an existing condition to one of the methods. In that case that condition will be signalled unmodified. Finally, the third version gives a condition mimic and one or more keyword arguments with data to set on that condition. In that case a mimic of the condition will be created, and then cells with data set based on the arguments.
| |
− |
| |
− | If a signal is not handled, nothing happens.
| |
− |
| |
− | If a warning is not handled, a message will be printed with the text of that warning.
| |
− |
| |
− | If an error is not handled, the debugger will be invoked - if a debugger is available. Otherwise the program will be terminated.
| |
− |
| |
− | A Rescue allows conditions to unwind the stack to the place where the rescue is established. Combining rescues and conditions looks a lot like regular exception handling in other programming languages.
| |
− |
| |
− | A Handler on the other hand will run code in the dynamic context of the place where the condition was signalled. A handler can invoke restarts to handle an error state at the place it happened, and as such doesn't have to actually unwind the stack anywhere. Any number of handlers can run - the last handler to run will be either the last handler, the handler that activates a restart, or the last handler before a valid rescue for that condition.
| |
− |
| |
− | A restart is a way to allow ways of getting back to a valid state. Take the example of referring to a cell that doesn't exist. Before signalling a condition, Ioke will set up restarts so you can provide a value to use in the case a cell doesn't exist. This restart will use that new value and continue execution at the point where it would otherwise have failed.
| |
− |
| |
− | A restart can have a name. If it doesn't have a name it can only be used interactively. You can use findRestart to get hold of the closest restart with a given name. You can invoke a given restart with invokeRestart, which takes either the name of a restart or a restart mimic. Finally, you can get all available restarts using availableRestarts.
| |
− |
| |
− | Both handlers, rescues and restarts are established inside a call to the bind macro. All arguments to this macro need to be either handlers, rescues or restarts, except for the last argument which should be the code to execute.
| |
− |
| |
− | You create a new handler by calling the method "handle". You create a new rescue by calling the method "rescue". You create a new restart by calling the method "restart". These all take funky arguments, so refer to the reference to better understand how they work.
| |
− |
| |
− | This small example doesn't necessarily show the power of conditions, but it can give an idea about how it works.
| |
− |
| |
− | <pre>;; to handle any problem
| |
− | bind(
| |
− | rescue(fn(c, nil)), ;; do nothing in the rescue
| |
− |
| |
− | error!("This is bad!!")
| |
− | )
| |
− |
| |
− | ;; to print all conditions happening, but not do anything
| |
− | bind(
| |
− | handle(fn(c, c println)),
| |
− |
| |
− | signal!("something")
| |
− | signal!("something more")
| |
− |
| |
− | warn!("A warning!!")
| |
− | )
| |
− |
| |
− | ;; rescue either of two conditions
| |
− | C1 = Condition Error mimic
| |
− | C2 = Condition Error mimic
| |
− |
| |
− | bind(
| |
− | rescue(C1, C2, fn(c, "got an error: #{c}" println)),
| |
− |
| |
− | error!(C1)
| |
− | )
| |
− |
| |
− |
| |
− | ;; invoke a restart when no such cell is signaled
| |
− | bind(
| |
− | handle(Condition Error NoSuchCell, fn(c, invokeRestart(:useValue, 42))),
| |
− |
| |
− | blarg println) ;; will print 42
| |
− |
| |
− |
| |
− | ;; establish a restart
| |
− | bind(
| |
− | restart(something, fn(+args, args println)),
| |
− |
| |
− | invokeRestart(:something, 1, 2, 3)
| |
− | )</pre>
| |
− | The code above shows several different things you can do with the condition system. It is a very powerful system, so I recommend trying to understand it. It lies at the core of many things in Ioke, and some parts will not make sense without a deep understanding of conditions. For more information on what such a system is capable of, look for documentation about the Common Lisp condition system, which has been a heavy influence on Ioke. Also, the debugger in IIk uses the condition system to implement its functionality.
| |
− |
| |
− | There are many conditions defined in the core of Ioke, and they are used by the implementation to signal error conditions of different kinds. Refer to the reference to see which conditions are available.
| |
− |
| |
− | Finally, one thing that you might miss if you're used to exceptions in other languages, is a construct that makes it possible to ensure that code gets executed, even if a non-local flow control happens. Don't despair, Ioke has one. It is called ensure, and works mostly like ensure in Ruby, and finally in Java. It takes one main code argument, followed by zero or more arguments that contain the code to always make sure executes. It looks like this:
| |
− |
| |
− | <pre>ensure(
| |
− | conn = Database open
| |
− | conn SELECT * FROM a_table,
| |
− | conn close!,
| |
− | conn reallyClose!,
| |
− | conn reallyReallyReallyClose!)</pre>
| |
− | This code uses a hypothetical database library, opens up a connection, does something, and then in three different ensure blocks tries to ensure that it really is closed afterwards. The return value of the ensure block will still be the return value of the last expression in the main code. Non local flow control can happen inside of the ensure block, but exactly what will happen is undefined -- so avoid it, please.
| |
− |
| |
− | ==== Text ====
| |
− |
| |
− | In Ioke, the equivalent of Strings in other languages are called Text. This better describes the purpose of the type. Ioke Text is immutable. All operations that would change the text returns a new object instead. If you are used to Java strings or Ruby strings, then most operation available on Ioke Texts will not come as a surprise.
| |
− |
| |
− | To create a new Text, you use the literal syntax as described in the syntax chapter. You can use interpolation to include dynamic data.
| |
− |
| |
− | You can do several things with Ioke text. These examples should show some of the methods:
| |
− |
| |
− | <pre>;; repeat a text several times
| |
− | "foo" * 3 ;; => "foofoofoo"
| |
− |
| |
− | ;; concatenate two texts
| |
− | "foo" + "bar" ;; => "foobar"
| |
− |
| |
− | ;; get the character at a specific index
| |
− | "foo"[1] ;; => 111
| |
− |
| |
− | ;; get a subset of text
| |
− | "foo"[1..1] ;; => "o"
| |
− | "foo"[0..1] ;; => "fo"
| |
− | "foo"[0...1] ;; => "f"
| |
− |
| |
− | "foxtrot"[1..-1] ;; => "oxtrot"
| |
− | "foxtrot"[1...-1] ;; => "oxtro"
| |
− |
| |
− | ;; is a text empty?
| |
− | "foo" empty? ;; => false
| |
− | "" empty? ;; => true
| |
− |
| |
− | ;; the length of the text
| |
− | "foo" length ;; => 3
| |
− | "" length ;; => 0
| |
− |
| |
− | ;; replace the first occurrence of something with something else
| |
− | "hello fox fob folk" replace("fo", "ba")
| |
− | ;; => "hello bax fob folk"
| |
− |
| |
− | ;; replace all occurrences of something with something else
| |
− | "hello fox fob folk" replaceAll("fo", "ba")
| |
− | ;; => "hello bax bab balk"
| |
− |
| |
− | ;; split around a text
| |
− | "foo bar bax" split(" ")
| |
− | ;; => ["foo", "bar", "bax"]</pre>
| |
− | The Text kind contains lots of useful functionality like this. The purpose is to make it really easy to massage text of any kind.
| |
− |
| |
− | One important tool for doing that is the "format" method. This is a mix between C printf and Common Lisp format. At the moment, it only contains a small amount of functionality, but it can still be very convenient. Specifically you can print each element in a list directly by using format, instead of concatenating text yourself.
| |
− |
| |
− | The "format" method takes format specifiers that begin with %, and then inserts one of its arguments in different ways depending on what kind of format specifier is used.
| |
− |
| |
− | Some examples of format follow:
| |
− |
| |
− | <pre>;; insert simple value as text
| |
− | "%s" format(123) ;; => "123"
| |
− |
| |
− | ;; insert value right justified by 6
| |
− | "%6s" format(123) ;; => " 123"
| |
− |
| |
− | ;; insert value left justified by 6
| |
− | "%-6s" format(123) ;; => "123 "
| |
− |
| |
− | ;; insert two values
| |
− | "%s: %s" format(123, 321) ;; => "123: 321"
| |
− |
| |
− | ;; insert a list of values formatted the same
| |
− | "%[%s, %]\n" format([1,2,3])
| |
− | ;; => "1, 2, 3, \n"
| |
− |
| |
− | ;; insert splatted values from a list
| |
− | "wow: %*[%s: %s %]" format([[1,2],[2,3],[3,4]])
| |
− | ;; => "wow: 1: 2 2: 3 3: 4 "</pre>
| |
− | ==== Numbers ====
| |
− |
| |
− | As mentioned in the section on syntax, Ioke supports decimal numbers, integers and ratios. A Ratio will be created when two integers can't be divided evenly. A Ratio will always use the GCD. In most cases Ratios, Decimals and Integers can interact with each other as would be expected. The one thing that might surprise people is that Ioke doesn't have any inexact floating point data type. Instead, decimals are exact and can have any size. This means they are well suited to represent such things as money, since operations will always have well defined results.
| |
− |
| |
− | All expected math works fine on Ioke numbers. The reference for numbers more closely specify what is possible. One thing to notice is that the % operator implements modulus, not remainder. This might be unintuitive for some developers. What that means is that it is not an error to ask for the modulus of 0: "13 % 0", since there is no division necessary in this operation.
| |
− |
| |
− | In addition to the above, Integers have the "times" method described earlier. It can also return the successor and predecessor of itself with the "pred" and "succ" methods.
| |
− |
| |
− | ==== Lists ====
| |
− |
| |
− | Ioke has lists that expand to the size needed. These lists can be created using a simple literal syntax. They can contain any kind of element, including itself (although don't try to print such a list). Ioke lists mix in the Enumerable mixin, which gives it quite powerful capabilities.
| |
− |
| |
− | A list can be created using the "list" or "[]" methods:
| |
− |
| |
− | <pre>;; an empty list
| |
− | []
| |
− |
| |
− | ;; the same list created in two different ways
| |
− | list(1, 2, 3)
| |
− | [1, 2, 3]
| |
− |
| |
− | ;; a list with different elements
| |
− | [1, "one", :one]</pre>
| |
− | Except for the Enumerable methods, List also defines many other methods that can be highly useful. Some examples are shown below:
| |
− |
| |
− | <pre>l = [1, 2, 3]
| |
− |
| |
− | ;; add two lists together
| |
− | l + l ;; => [1, 2, 3, 1, 2, 3]
| |
− |
| |
− | ;; return the difference of two lists
| |
− | l - [2] ;; => [1, 3]
| |
− |
| |
− | ;; add a new value to a list
| |
− | l << 42
| |
− | l == [1, 2, 3, 42] ;; => true
| |
− |
| |
− | ;; get a specific element from the list
| |
− | l[0] ;; => 1
| |
− |
| |
− | ;; -1 returns the last, -2 the next to last
| |
− | l[-1] ;; => 42
| |
− |
| |
− | ;; an index outside the boundaries return nil
| |
− | l[10] ;; => nil
| |
− |
| |
− | ;; assign a new value
| |
− | l[3] = 40
| |
− | l == [1,2,3,40] ;; => true
| |
− |
| |
− | l[-1] = 39
| |
− | l == [1,2,3,39] ;; => true
| |
− |
| |
− | ;; assign an out of bounds value
| |
− | l[10] = 13
| |
− | l == [1,2,3,39,nil,nil,nil,nil,nil,nil,13]
| |
− | ;; => true
| |
− |
| |
− | ;; at and at= is the same as [] and []=
| |
− | l at(0) ;; => 1
| |
− |
| |
− | ;; empty the list
| |
− | l clear!
| |
− | l == [] ;; => true
| |
− |
| |
− | ;; follows the each protocol
| |
− | l each(println)
| |
− |
| |
− | ;; is empty?
| |
− | l empty? ;; => true
| |
− |
| |
− | ;; does it include an element?
| |
− | l include?(:foo) ;; => false
| |
− |
| |
− | ;; the last element
| |
− | l last ;; => nil
| |
− | [1, 2] last ;; => 2
| |
− |
| |
− | ;; the length
| |
− | [1, 2] length ;; => 2
| |
− |
| |
− | ;; first value
| |
− | [1, 2] first ;; => 1
| |
− |
| |
− | ;; rest except for first
| |
− | [1, 2, 3] rest ;; => [2, 3]
| |
− |
| |
− | ;; returns a new sorted list
| |
− | [3, 2, 1] sort ;; => [1, 2, 3]
| |
− |
| |
− | ;; sorts in place
| |
− | l = [3, 2, 1]
| |
− | l sort!
| |
− | l == [1, 2, 3] ;; => true</pre>
| |
− | ==== Dicts ====
| |
− |
| |
− | A Dict is a dictionary of key-value mappings. The mappings are unordered, and there can only ever be one key with the same value. Any kind of Ioke object can be used as a key. There is no problem with having the same value for different keys. The default implementation of Dict uses a hash-based implementation. That's not necessarily always true for all dicts. The iteration order is not necessarily stable either, so don't write code that depends on it.
| |
− |
| |
− | Creating a dict is done using either the "dict" or the "{}" methods. Both of these expect either keyword arguments or mimics of Pair. If keyword arguments, these keywords will be used as symbol keys. That's the most common thing, so it makes sense to have that happen automatically. Dicts also try to print themselves that way.
| |
− |
| |
− | <pre>dict(1 => 2, 3 => 4)
| |
− |
| |
− | ;; these two are the same
| |
− | dict(foo: "bar", baaz: "quux")
| |
− | dict(:foo => "bar", :baaz => "quux")
| |
− |
| |
− | {1 => 2, 3 => 4}
| |
− |
| |
− | ;; these two are the same
| |
− | {foo: "bar", baaz: "quux"}
| |
− | {:foo => "bar", :baaz => "quux"}
| |
− |
| |
− | ;; the formats can be combined:
| |
− | {1 => 2, foo: 42, "bar" => "qux"}</pre>
| |
− | The literal Pair syntax (using =>) will not necessarily instantiate real pairs for this.
| |
− |
| |
− | Dicts mix in Enumerable. When using each, what will be yielded are mimics of Pair, where the first value will be the key and the second will be value. Just like Lists, Dicts have several useful methods in themselves:
| |
− |
| |
− | <pre>d = {one: "two", 3 => 4}
| |
− |
| |
− | ;; lookup with [], "at" works the same
| |
− | d[:one] ;; => "two"
| |
− | d[:two] ;; => nil
| |
− | d[3] ;; => 4
| |
− | d[4] ;; => nil
| |
− |
| |
− | ;; assign values with []=
| |
− | d[:one] = "three"
| |
− | d[:new] = "wow!"
| |
− |
| |
− | d == {one: "three", 3 => 4, new: "wow!"}
| |
− |
| |
− | ;; iterate over it
| |
− | d each(value println)
| |
− |
| |
− | ;; get all keys
| |
− | d keys == set(:one, :new, 3)</pre>
| |
− | ==== Sets ====
| |
− |
| |
− | If you want an object that work like a mathematical set, Ioke provides such a kind for you. There is no support for literal syntax for sets, but you can create new with the set method. A set can be iterated over and it is Enumerable. You can add and remove elements, and check for membership.
| |
− |
| |
− | <pre>x = set(1,2,3,3,2,1)
| |
− |
| |
− | x map(*2) sort ; => [2, 4, 6]
| |
− |
| |
− | x === 1 ; => true
| |
− | x === 0 ; => false
| |
− |
| |
− | x remove!(2)
| |
− | x === 2 ; => false
| |
− |
| |
− | x << 4
| |
− | x === 4 ; => true</pre>
| |
− | ==== Ranges and Pairs ====
| |
− |
| |
− | Both ranges and pairs tie two values together. They also have literal syntax to create them, since they are very useful in many circumstances.
| |
− |
| |
− | A Range defines two endpoints. A Range is Enumerable and you can also check for membership. It's also convenient to send Ranges to the "List []" method. A Range can be exclusive or inclusive. If it's inclusive it includes the end value, and if it is exclusive it doesn't.
| |
− |
| |
− | An addition to Ioke S is the possibility of inverted ranges. If the first value is larger than the second value, then the range is inverted. This puts slightly different demands on the objects inside of it. Specifically, if you want to iterate over the elements, the kind you're using need to have a method called 'pred' for predecessor, instead of 'succ' for successor. Membership can still be tested, as long as <=> is defined. So you can do something like this: ("foo".."aoo") === "boo". It's mostly useful for iterating in the opposite direction, like with 10..1, for example.
| |
− |
| |
− | <pre>;; literal syntax for inclusive range
| |
− | 1..10
| |
− |
| |
− | ;; literal syntax for exclusive range
| |
− | 1...10
| |
− |
| |
− | ;; check for membership
| |
− | (1..10) === 5 ;; => true
| |
− | (1..10) === 10 ;; => true
| |
− | (1..10) === 11 ;; => false
| |
− |
| |
− | (1...10) === 5 ;; => true
| |
− | (1...10) === 10 ;; => false
| |
− | (1...10) === 11 ;; => false
| |
− |
| |
− | ;; get the from value
| |
− | (1..10) from == 1 ;; => true
| |
− | (1...10) from == 1 ;; => true
| |
− |
| |
− | ;; get the to value
| |
− | (1..10) to == 10 ;; => true
| |
− | (1...10) to == 10 ;; => true
| |
− |
| |
− | ;; is this range exclusive?
| |
− | (1..10) exclusive? ;; => false
| |
− | (1...10) exclusive? ;; => true
| |
− |
| |
− | ;; is this range inclusive?
| |
− | (1..10) inclusive? ;; => true
| |
− | (1...10) inclusive? ;; => false</pre>
| |
− | A Pair represent a combination of two values. They don't have to be of the same kind. They can have any kind of relationship. Since Pairs are often used to represent Dicts, it is very useful to refer to the first value as the "key", and the second value as "value".
| |
− |
| |
− | <pre>;; literal syntax for a pair
| |
− | "foo" => "bar"
| |
− |
| |
− | ;; getting the first value
| |
− | ("foo" => "bar") first ;; => "foo"
| |
− | ("foo" => "bar") key ;; => "foo"
| |
− |
| |
− | ;; getting the second value
| |
− | ("foo" => "bar") second ;; => "bar"
| |
− | ("foo" => "bar") value ;; => "bar"</pre>
| |
− | ==== Enumerable ====
| |
− |
| |
− | One of the most important mixins in Ioke is Mixins Enumerable - the place where most of the collection functionality is available. The contract for any kind that wants to be Enumerable is that it should implement each in the manner described earlier. If it does that it can mixin Enumerable and get access to all the methods defined in it. I'm not going to show all the available methods, but just a few useful examples here. Note that any method name that ends with Fn takes a block instead of a raw message chain.
| |
− |
| |
− | Almost all methods in Enumerable take variable amounts of arguments and do different things depending on how many arguments are provided. The general rule is that if there is only one argument, it should be a message chain, and if there are two or more arguments the last one should be code, and the rest should be names of arguments to use in that code.
| |
− |
| |
− | Mapping a collection into a another collection can be done using map or mapFn. These are aliased as collect and collectFn too.
| |
− |
| |
− | <pre>l = [10, 20, 30, 40]
| |
− |
| |
− | ;; mapping into text
| |
− | l map(asText) ;; => ["10", "20", "30", "40"]
| |
− | l map(n, n asText) ;; => ["10", "20", "30", "40"]
| |
− |
| |
− | ;; exponentiation
| |
− | l map(**2) ;; => [100, 400, 900, 1600]
| |
− | l map(n, n*n) ;; => [100, 400, 900, 1600]</pre>
| |
− | Filtering the contents of a collection can be done using select, which is aliased as filter and findAll.
| |
− |
| |
− | <pre>;; with no arguments, return all true things
| |
− | [nil, false, 13, 42] select ;; => [13, 42]
| |
− |
| |
− | l = [1, 2, 3, 4, 5, 6]
| |
− |
| |
− | ;; all elements over 3
| |
− | l select(>3) ;; => [4, 5, 6]
| |
− | l select(n, n>3) ;; => [4, 5, 6]</pre>
| |
− | A very common operation is to create one object based on the contents of a collection. This operation has different names in different languages. In Ioke it is called inject, but it is aliased as reduce and fold.
| |
− |
| |
− | <pre>l = [1, 2, 3, 4, 5]
| |
− |
| |
− | ;; inject around a message chain
| |
− | l inject(+) ;; => 15
| |
− | l inject(*) ;; => 120
| |
− |
| |
− | ;; with one arg
| |
− | l inject(n, *n) ;; => 120
| |
− | l inject(n, +n*2) ;; => 29
| |
− |
| |
− | ;; with two args
| |
− | l inject(sum, n, sum*n) ;; => 120
| |
− | l inject(sum, n, sum*2 + n*3) ;; => 139
| |
− |
| |
− | ;; with four args
| |
− | l inject(1, sum, n, sum*n) ;; => 120
| |
− | l inject(10, sum, n, sum*n) ;; => 1200</pre>
| |
− | ==== Regexps ====
| |
− |
| |
− | Regular expressions allow the matching of text against an abstract pattern. Ioke uses the JRegex engine to implement regular expressions. This means Ioke supports quite advanced expressions. Exactly what kind of regular expression syntax is supported can be found at [http://jregex.sf.net http://jregex.sf.net]. This section will describe how you interact with regular expressions from Ioke.
| |
− |
| |
− | There are two kinds that are used when working with regular expression. First, Regexp, which represent an actual pattern. The second is Regexp Match, which contains information about a regular expression match. It is this match that can be used to extract most information about where and how a complicated expression matched.
| |
− |
| |
− | The standard way of matching something is with the match method. This method is aliased as =~, which is the idiomatic usage. It takes anything that can be converted to a text and returns nil if it fails to match anything, or a Regexp Match if it matches.
| |
− |
| |
− | You can create a new Regexp from a text by using the Regexp from method. You can quote all meta characters in a text by using the Regexp quote method. Finally, if you just want to get all the text pieces that match for a regular expression, you can use Regexp allMatches. It takes a text and returns a list of all the text pieces where the Regexp matched.
| |
− |
| |
− | You can also investigate a Regexp, by asking for its pattern (the pattern method), its flags (the flag method), and the named groups it defines. The names of the named groups can be inspected with the names method.
| |
− |
| |
− | A Regexp Match is specific to one specific match. You will always get a new one every time you try to match against something. A match has a target, which is the original text the regular expression was matched against. So if I do #/ .. / =~ "abc fo bar", then the whole "abc fo bar" is the target. You get the target from a match by using the target method. A match can also be asked for the named groups the regular expression it was matched against support. This is also done with the names method. Match also has two methods beforeMatch and afterMatch, that returns the text before and after the match. The match method returns the text comprising the actual match. The captures method will return a list of all the captured groups in this match. The asList method returns the same things as captures, except it also includes the full match text.
| |
− |
| |
− | To figure out the indices where groups start or end, you can use the start or end methods. These take an optional index that defaults to zero, where group zero is the full match. They can also take a text or symbol that should be the name of a named group. The offset method returns a pair of the beginning and end offset of the group asked for. It works the same as start and end, with regards to what argument it takes.
| |
− |
| |
− | You can use the [] method on Match to extract the one or several pieces of matches. If m is a match, then all of these are valid expressions: m[0]. m[1..3]. m[:foo]. m[-2], where the ranges return several groups, and the negative index returns indexed from the end of the list of captures.
| |
− |
| |
− | Finally, a Regexp Match implements pass. It does this in such a way that if you use named groups, you can extract the value for that group by calling a method named the same as that group name. An example of this:
| |
− |
| |
− | <pre>number = "555-12345"
| |
− | m = #/({areaCode}\d{3})-({localNumber}\d{5})/ =~ number
| |
− | m areaCode println
| |
− | m localNumber println</pre>
| |
− | ==== FileSystem ====
| |
− |
| |
− | The FileSystem kind allows access to functionality in the file system. It is the entry point to any manipulation of files and directories, and it can also be used to get listings of existing files. The reference includes good information about the existing methods in the FileSystem kind.
| |
− |
| |
− | ==== Other things ====
| |
− |
| |
− | Ioke supports transforming an object into another object through the use of the method become!. After an object becomes another object, those two objects are indistinguishable from Ioke. This makes it really easy to do transparent proxies, futures and other things like that. To use it, do something like this:
| |
− |
| |
− | <pre>x = "foo"
| |
− | y = "bar"
| |
− |
| |
− | x become!(y)
| |
− | x same?(y) ; => true</pre>
| |
− | In Ioke, objects can also be frozen. When an object is frozen it cannot be modified in any way. Any try at modifying a frozen object will result in a condition. This can be really helpful to track down problems in code, and make assertions about what should be possible. You freeze an Ioke object by calling freeze! on it. You can unfree it by calling thaw! on it. You can also check if something is frozen by calling frozen? on it.
| |
− |
| |
− | === Libraries ===
| |
− |
| |
− | Ioke ships with several small libraries that are useful for different tasks. The main ones are IIk, ISpec and DokGen, and these will be documented a bit more in this chapter. All of them are considered a core part of Ioke since the functionality they provide is tied to the distribution.
| |
− |
| |
− | ==== IIk ====
| |
− |
| |
− | IIk is the interactive Ioke prompt, which will run if you give no arguments to the ioke script. At the IIk prompt you can execute mostly all the same kind of code that could execute inside of an Ioke script file. The main difference is that this code will not be assigned to Ground, but instead will run in another context that is specific for the purposes of IIk.
| |
− |
| |
− | IIk provides the possibility to exit using either "exit" or "close". Both of these will in fact signal a condition of kind "IIk Exit".
| |
− |
| |
− | IIk will use readline if possible. That means that you can do the regular shell editing most other REPLs have support for, including using the up and down keys to scroll through earlier executed code.
| |
− |
| |
− | IIk has a simple debugger that will be invoked when a condition is not handled. This debugger allows you to execute any kind of code and also to invoke restarts. As an example, the code below refers to a cell that doesn't exist. A condition is signalled and the debugger invoked. I choose to invoke the restart named useValue and provide the value 42. That value is returned and I'm back at the IIk prompt. In the next line I do the same thing again with the same name. This time I choose the storeValue restart and provide the value 43. And after that there exists a cell with the name foo and the value 43.
| |
− |
| |
− | <pre>Iik> foo
| |
− | *** - couldn't find cell 'foo' on 'Ground_0x26E9F9' (Condition Error NoSuchCell)
| |
− |
| |
− | foo [<init>:1:0]
| |
− |
| |
− | The following restarts are available:
| |
− | 0: storeValue (Store value for: foo)
| |
− | 1: useValue (Use value for: foo)
| |
− | 2: abort (restart: abort)
| |
− | 3: quit (restart: quit)
| |
− |
| |
− | dbg:> 1
| |
− | dbg::newValue> 42
| |
− | +> 42
| |
− |
| |
− |
| |
− | +> 42
| |
− |
| |
− | iik> foo
| |
− | *** - couldn't find cell 'foo' on 'Ground_0x26E9F9' (Condition Error NoSuchCell)
| |
− |
| |
− | foo [<init>:1:0]
| |
− |
| |
− | The following restarts are available:
| |
− | 0: storeValue (Store value for: foo)
| |
− | 1: useValue (Use value for: foo)
| |
− | 2: abort (restart: abort)
| |
− | 3: quit (restart: quit)
| |
− |
| |
− | dbg:> 0
| |
− | dbg::newValue> 43
| |
− | +> 43
| |
− |
| |
− |
| |
− | +> 43
| |
− |
| |
− | iik> foo
| |
− | +> 43</pre>
| |
− | The Ioke debugger is quite powerful. If you were to execute any other Ioke code, that code will actually be executed in the context of the place where the condition was signalled. So it is possible to fix more complicated things in this manner too.
| |
− |
| |
− | ==== ISpec ====
| |
− |
| |
− | ISpec is a minimal port of the Ruby RSpec framework for behavior-driven development. It supports the bare minimum to allow testing of Ioke itself. The current Ioke test suite is completely written in ISpec, and it seems to be a capable environment.
| |
− |
| |
− | The ispec command line tool takes one argument -- "-f" to specify which format to print in. The default is "p" for progress, which only shows one dot for each test run. The "s" alternative shows the longer spec format. To run all tests in a directory with spec format:
| |
− |
| |
− | <pre>ispec -fs test_dir</pre>
| |
− | This command will find all files ending in _spec.ik and run the specs defined in them. If a single file is specified, only the tests in that file will run. If more than one directory or file is specified on the command line, all the tests will be run together. Provided the spec files all use the ispec module, the files can be run directly with the ioke-command too.
| |
− |
| |
− | A full test file utilizing most of the parts of ISpec looks like this:
| |
− |
| |
− | <pre>use("ispec")
| |
− |
| |
− | describe(Foo,
| |
− | it("should have the correct kind",
| |
− | Foo should have kind("Foo")
| |
− | Foo kind should == "Foo"
| |
− | Foo kind should match(#/Fo+/)
| |
− | )
| |
− |
| |
− | it("should be possible to mimic",
| |
− | m = Foo mimic
| |
− | m should have kind("Foo")
| |
− | m should not be same(Foo)
| |
− | m should mimic(Foo)
| |
− | )
| |
− |
| |
− | describe("aMethod",
| |
− | it("should not return nil",
| |
− | Foo aMethod should not be nil
| |
− | )
| |
− |
| |
− | it("should return a number that can be multiplied",
| |
− | (Foo aMethod * 2) should == 12
| |
− | )
| |
− | )
| |
− |
| |
− | describe("aBadMethod",
| |
− | it("should signal a condition",
| |
− | fn(Foo aBadMethod) should signal(Condition Error BadBadBed)
| |
− | )
| |
− | )
| |
− | )</pre>
| |
− | This code first makes sure to use ISpec, then describes Foo. The describe method takes either kinds or texts describing what's under test. This can be nested arbitrarily deep. A test is defined with the "it" method, which takes a text describing the test first, and the implementation of the test as the second argument. If the second argument is left out, the test is considered pending.
| |
− |
| |
− | Assertions are done using the "should" method. This returns an expectation that can check several different things against the original receiver. Using == is the simplest expectation and checks that a value equals another. By adding the not method call inbetween, the expectation is inversed. There are some predefined expectations. Except for ==, these are mimic, match and signal. The signal expectation makes sure that a condition is signalled in the code. The match expectation will check a text value against a regular expression. The mimic expectation checks whether an object mimics another.
| |
− |
| |
− | The words "be" and "have" are ignored in the expectations. They are so called fluff words - that are only there to make it more readable.
| |
− |
| |
− | If an expectation receives a message it doesn't know about, it uses pass to check a dynamic property. For example, something like "foo should be same(x)" will end up calling "same?" with x as an argument. The same thing happens in the above code to check for kind. There exists no kind expectation. Instead the kind checking will go check for "kind?".
| |
− |
| |
− | ==== DokGen ====
| |
− |
| |
− | DokGen is the tool that is used to generate the reference documentation for Ioke. The goal is that it will be a general purpose tool for any Ioke application. It extracts the documentation information from defined objects and then generates an HTML structure from it that can be easily navigated. If specs are available for objects and methods, it will try to incorporate these together with the documentation. At the moment, there is no way to run dokgen on a subset of Ioke code, but that should be available very soon now. At the moment, running the dokgen script will create a directory called "dok" which contains the full documentation. It will use all specs it can find in the directory test.
| |
− |
| |
− | === What is missing ===
| |
− |
| |
− | Sadly, Ioke S is not finished. Several things are missing before it's complete. The core mechanics of the language I'm feeling fairly confident in, but the core libraries are generally only partially implemented. They are much more complete than for the Ioke 0 release, but there still exist large holes in it.
| |
− |
| |
− | Another major part -- maybe even THE major part -- is Java integration. At the moment it is not possible to call or interact with Java libraries. That need to happen, and it will happen after the S release. Concurrency will also be part of the work after the S release.
| |
− |
| |
− | === Future plans ===
| |
− |
| |
− | I have many plans for the future of Ioke. The next major versions of Ioke will be called Ioke E and Ioke P, respectively. Exactly what they will contain we'll have to see, but these are things I need to have, large and small:
| |
− |
| |
− | * Java integration
| |
− | * Concurrency primitives of some kind
| |
− | This list is in no way exhaustive, but it is what currently comes to mind.
| |
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:
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.
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.