Guide
Introduction
Ioke is a general purpose language. It is a strongly typed, extremely dynamic, prototype object oriented language. It is homoiconic and its closest ancestors are Io, Smalltalk, Ruby and Lisp - but it's quite a distance from all of them. It looks a lot like Io, to a limit.
Ioke is a folding language. This means it folds in on itself. You can create new abstractions covering any of the existing abstractions in the language. You can abstract over these, over and over again, until you have a language that lets you express what you want to express in a succinct and readable way. Ioke allows you to fold your code.
Ioke is targeted at the Java Virtual Machine and is tightly integrated with the platform. Why the JVM? It's available everywhere, it gives several important features such as world class garbage collectors, capable thread schedulers and an amazing JIT compiler. All of these are things that serve Ioke well, without requiring direct development resources from the Ioke team. Access to the Java platform also means access to all existing libraries and functionality, with all that entails. The JVM is just a very pragmatic choice.
You're probably reading this guide at ioke.org. That is the official home of the project, although some of the project functionality is hosted at Kenai, where such things as mailing lists and a bug tracker is available. The canonical source repository for Ioke is on GitHub.
The current version of Ioke is called Ioke P. The naming of Ioke will change regularly with major revisions. There are two different versions in play here. Ioke P is the name and version of the language and core libraries. The initial implementation for Ioke P is called ikj 0.4.0, and the version numbers are not interdependent. The next major version of Ioke will be called Ioke F, and you can find information about it in the chapter on future plans.
This programming guide -- together with the reference for your current version -- should be the complete document needed to understand Ioke P, how to program in it, how to understand the names and concepts used, and also give an initial inkling on what I think is good taste.
Note that I will use many names that aren't necessarily the same as the ones traditional programming languages use. These names will be made clear sooner or later in this document, but it might help some to jump forward to Objects, skim that bit, and then start over once words like Origin, cell and mimic make sense.
Vision
The evolution of programming languages is a steady progression of finding new ways to express abstractions naturally - in a way that doesn't stray too far away from the smaller details. A programming language has to make it possible to abstract away common things, while making it easy to customize these abstractions in very detailed ways. A programming language should be able to do this well without sacrificing readability and understandability. This tension lies at the core of programming.
How do you create a language that makes it easy to express powerful concepts in a succinct way, while still making it easy to maintain and work with after the fact, without turning it into a new compression mode? How do you make it easy for a programmer to express high level abstractions that are abstractions of abstractions of abstractions?
There are many open problems in programming language design. Concurrency is one of them, performance another. These are two areas Ioke does not address. Instead, Ioke is a remodeling of the core concepts and ideas embodied in other programming languages.
Are Lisp and Smalltalk still the most powerful languages around, or are there ways of providing more expressiveness without sacrificing understandability? Is there a way to combine all the lessons learned from languages like Ruby and Python, and patch them back into a Lisp and Smalltalk core? Is it possible to do this while taking some of the benefits of Io? Can a language be both small, regular, homoiconic, reflective and easy to understand? I hope that Ioke is just that.
Simplicity doesn't mean lack of power. Small, simple, orthogonal functionality can be more powerful than larger, complicated abstractions that don't fit together.
Io explicitly states that the goal of the language is to refocus attention on expressiveness, and with Ioke I want to take that philosophy one step further.
It's important to realize that an experiment like this doesn't necessarily have to mean the language can't be used for real projects. By wedding Ioke to the Java Virtual Machine, I make it easy to get access to good libraries and existing implementations on most platforms. In that way, Ioke can be used to create real systems, even though the ecosystem will initially be very small. And I think that this is necessary. How can you know if a language really is worthwhile or not, if you can't use it as a general purpose programming language? The Java platform makes this possible.
Getting started
Ioke is very easy to get started with. The first step is to download a package. Which one you choose depends on what platform you're on, and whether you want to build Ioke yourself, or just start using it. This guide will only cover using a prebuilt version. Go to the download page, and grab one of the distributions. At the time of writing the full version of Ioke is Ioke P ikj 0.4.0. Choose the latest download in the 0.4-series for this document to apply.
Once you have downloaded the distribution, you need to unpack it somewhere, and finally add the bin directory to your PATH environment variable. There is also a jar download that can be run directly. If you choose this option you don't get the benefits of having a home for Ioke, which in some cases might be inconvenient. Ioke can be run directly from the jar file, though.
Building Ioke
If you'd like to build Ioke from source, make sure you have a recent version of the Java Development Kit installed (1.5.0 or higher, preferrably 1.6.0) and Apache Ant. You must have the ant script reachable from your PATH variable. Then, simply check out the source code from the main repository, and build it using ant. That should run all the compilation steps and tests, and allow the bin/ioke script to run. Just proceed as if you had unpacked the distribution, adding the bin directory to the PATH.
Running scripts
To run an Ioke script, you can generally just use the ioke command:
$ ioke helloWorld.ik Hello world
You can also execute snippets of code on the command line using the -e argument to the ioke command. You can have several of these in the same line too:
$ ioke -e'"Hello world" println' -e'"Goodbye world" println' Hello world Goodbye world
When using -e, be careful about what quoting style you use, since the shell sometimes can munge up your commands if you don't surround them correctly.
The ioke command has several helpful command line options, which can change what happens during execution. These are:
- -Cdirectory
- Switch to directory before executing any files and command line scripts. This will make the directory the initial current working directory for Ioke during the execution of the JVM.
- -d
- Enable debug output.
- -e script
- Execute script, as describe above. May occur more than once on a command line.
- -h
--help
- Display help information, including descriptions of these command line options.
- -Idirectory
- Add directory to the load path of Ioke. May occur more than once on a command line.
- -JjvmOptions
- Pass on options to the JVM. This can be used to change any runtime parameters that your JVM takes. May occur more than once. The options are provided directly after the -J, so if you want to change the maximum amount of memory used, you can do that writing -J-Xmx128M.
- --copyright
- Print copyright information and exit.
- --version
- Print version information and exit
- --server
- Run the JVM in server Hotspot mode
- --client
- Run the JVM in client Hotspot mode (the default)
- --
- Mark the end of options to the ioke script, anything after this are options to be sent to the code running.
If you provide the name of a script file on the command line, it should come after all the arguments to the ioke script. Everything after the script will be added as data to the System programArguments
cell. You can use both one-line scripts with -e and specify a script file. If so, the script file will be run after the one-line scripts.
Interactive mode
If no code to execute has been specified to the ioke script, IIk - Interactive Ioke - will start. This is a REPL that allows the execution of arbitrary code in a shell that immediately displays the result. The main difference between running Ioke from a file and interactively is that the interactive prompt will show a notice of the result of the last operation after each execution. IIk will also invoke a debugger when a condition is encountered. This debugger gives you the possibility to inspect what happened more closely. The final difference with IIk is that it does not execute code directly in Ground - which the top level inside an Ioke script will do. This difference is crucial, when considering namespacing issues.
IIk will try to use Readline through JLine if your platform supports it.
IIk will be more closely described later, but just to give you a glimpse, this is how a small session could look like:
iik> "hello world" println hello world +> nil iik> 10 * 20 +> 200 iik> 3/2 +> 3/2 iik> 3/2 + 3/2 +> 3 iik> 3/2 * 3 +> 9/2 iik> foo = "hello" +> "hello" iik> foo +> "hello" iik> exit Bye.
When you see the prompt "iik>", you know that IIk is waiting for input. The result of a computation is shown after the "+>" sigil. You can exit from IIk by calling either "exit" or "quit". There is also a restart named "quit" that can be invoked to quit IIk.
Syntax
Ioke has no keywords or statements. Everything is an expression composed of a chain of messages. A piece of code is represented as a chain of messages that links to the next message. The result of one message will be the receiver of the next message, until a "." message is received. The "." message is a terminator that throws away the current receiver. A newline will serve as a "." message in the circumstances where it feels natural.
An informal BNF description of Ioke looks like this:
program ::= messageChain? messageChain ::= expression+ expression ::= message | brackets | literal | terminator literal ::= Text | Regexp | Number | Decimal | Unit message ::= Identifier ( "(" commated? ")" )? commated ::= messageChain ( "," messageChain )* brackets ::= ( "[" commated? "]" ) | ( "{" commated? "}" ) terminator ::= "." | "\n" comment ::= ";" .* "\n"
What isn't visible here is that all whitespace -- except for newlines -- will work only as separators of messages, and is otherwise ignored. That means that message sending does not use the dot, as in most other languages. A phrase such as foo().bar(quux(42)).baaz()
would be expressed as foo() bar(quux(42)) baaz()
, or more succinctly foo bar(quux(42)) baaz
in Ioke.
All the types of literals are actually turned into a message to create that literal, so the canonical form of the message chain contains no literals, just a message to create that literal. Any message can have zero or more arguments given to it. Arguments are separated with comma. If there are no arguments to a message, the parenthesis can be left off, but they need to be there if there are arguments. Mostly any combination of characters can be used as an Identifier, with some exceptions.
There used to be a parsing element called operators, but these have now been included into identifiers. They are not parsed differently at all, but the operator shuffling step will handle them differently. Specifically, operators can be used in infix, including having different precedence rules. Assignment is a specific form of operator which gets its own kind of shuffling. These are both described below.
An identifier in Ioke can be one of several things. Ioke takes the rules for Java identifiers, and adds some more to them. All Unicode letters and digits can be part of an identifier, except for the first entry. Underscores are allowed, just like in Java. Ioke also allows colons as an identifier. Exclamation mark and question mark is allowed anywhere in the identifier except for in the beginning. Identifiers can be broadly classified into identifiers and operators, where operators can be any combination of several sigils. There are also some special operators that have restrictions. These are: Opening and close brackets are not allowed, except together with its counterpart, so [ is not a valid identifier, while [] is. So is {}. () is not valid either. Two or more dots is a valid identifier. A hash sign can be followed by any operator char, but isn't parsed as an identifier by itself. Slash is not an operator char, but can be used as it except in combinations that look like regular expressions. The operator chars are: +, -, *, %, <, >, !, ?, ~, &, |, ^, $, =, @, ', ` and :. These can be combined together in any order, and any number, except for the caveats noted before. That means the available operator space is infinite, and very wide. Combinations of letters and operator characters are generally not allowed, except for the exceptions with :, ! and ?. This is to make it possible to have infix operations without spaces in some situations.
The two forms of brackets will get turned into a canonical form. Surrounding comma-separated message chains with square brackets is the same as calling the method [], giving it those message chains as argument. So [foo, bar, quux] is exactly the same as [](foo, bar, quux). The same is true for curly brackets.
Comments start with semicolon and end at the first newline. They can be used mostly anywhere, except inside of literal texts. The hash sign followed by an exclamation mark is also a comment, to allow the shebang line in Unix scripts.
How and when the actual evaluation of messages happen depend on what kind the message type is. If it's inactive, the value reflecting that cell will be returned. If it's active, the cell will be activated and the result of that activation returned. How the activation depends on what kind of code the cell contains. The various kinds of code is described more closely in the chapter about code.
Literal values
Ioke currently contains four different kinds of literals. There is a fifth quasi literal, that isn't exactly parsed as a literal, but will be evaluated differently based on its name. These literals are texts, regular expressions, integers and decimal numbers. Symbols are actually parsed as regular identifiers, but they are handled a bit differently during evaluation.
Text
A literal text in Ioke is what is generally called strings in most languages. As in most languages, text is written inside of double quotes. Any characters are valid inside of those double quotes. That includes newlines - so you can write a literal text that extends to several lines. There is an alternate syntax for text when the value contains a lot of double quotes. As in most other languages, several escapes are valid inside of a text. Escapes are preceded by the backslash, and insert the character corresponding to the escape values. These escapes are:
- \b
- Inserts the backspace character, that is represented in ASCII by the decimal value 8.
- \e
- Inserts the character that is represented in ASCII by the decimal value 27. This value is used for sending escape values to the TTYs in some operating systems.
- \t
- Inserts the TAB character - ASCII decimal 9.
- \n
- Inserts the newline character - ASCII decimal 10.
- \f
- Inserts the form feed character - ASCII decimal 12.
- \r
- Inserts the carriage return character - ASCII decimal 13.
- \"
- Inserts the double quote character - ASCII decimal 34.
- \\
- Inserts the backslash character - ASCII decimal 92.
- \[newline]
- Inserts nothing at all. Used to escape necessary newlines, without having them show up in the output text.
- \#
- Inserts a literal hash character - ASCII decimal 35.
- \uABCD
- Inserts the Unicode codepoint corresponding to the hexadecimal value of the four characters following the "u". All four hexadecimal characters need to be specified.
- \7, \12, \316
- Inserts the Unicode codepoint corresponding to the octal value of the one, two or three octal characters. The maximum value allowed is \377, and the minimum is obviously \0.
Ioke also supports an alternative text syntax that can be used when the text in question contains many scare quotes. The alternative syntax starts with #[ and ends with ]. A right bracket will have to be escaped, but scare quotes doesn't have to be.
The parsing of text will generate a message with name "internal:createText". This message will get one argument that is the raw Java String corresponding to the text.
Ioke allows automatic interpolation of arbitrary values in the same manner as Ruby. It uses the same syntax for this, which is the #{} syntax inside a text. These can be nested in any way. The elements will be parsed and sent as arguments to the message with name "internal:concatenateText". So an Ioke text such as "foo bar#{flux} will #{1+2}" will generate the message internal:concatenateText("foo bar", flux, " will ", 1+(2), ""). As you can see, there is a small amount of waste in the way this is generated -- but the simple model makes it easy to understand. It's not guaranteed that this will remain the same, although the message will definitely remain.
Some examples:
"foo"
"flax \
mux"
"one two #{three} \b four"
#[you don't really "#{1+2+3}" believe that?]
Regular expressions
Ioke has very capable regular expressions. Exactly what you can do with them can be found further down in this guide. The literal syntax allows regular expressions to be embedded in code directly. The syntax for this starts with a #/ and ends with another /. The last slash can optionally be followed by some flags that change the behavior of the expression. Regular expressions can also use an alternative syntax that starts with #r[ and ends with ]. Just as with Text, regular expressions can contain interpolation. This interpolation will be transformed into regular expressions and then combined with the outer regular expression. A few examples might be in order here:
#//
#r[]
#/foo/
#r[foo]
#/fo+/x
#r[fo+]x
#/bla #{"foo"} bar/
#r[bla #{"foo"} bar]
The first example is an empty regular expression. The second is an expression matching the word "foo". The third expression matches an "f" followed with one or more "o". It also allows extended regular expression syntax, due to the x flag. The flags supported in Ioke are x, i, u, m and s. The meaning of these match the meaning of corresponding Ruby flags. Regular expressions allow most of the same escapes as Ioke text. Specifically, these escapes are supported: b, t, n, f, r, /, \ and newline. Unicode and octal escapes also work. The fourth example shows the insertion of a literal text inside of a regular expression.
Ioke regular expressions will be transformed into a call to internal:createRegexp. This message expects two Java strings, one with the actual pattern, and one with the flags.
Integers
Ioke supports arbitrarily sized numbers. It also contains a numerical tower that can be more closely explored in the reference documentation. The numerical tower is based in Number. Number Real mimics Number. Number Rational mimics Number Real, and so does Number Decimal. Finally, Number Integer and Number Ratio both mimics Number Rational. The interesting parts of this tower is Number Integer, which corresponds to integers, Number Ratio, which is any ratio between two integers, and Number Decimal, which corresponds to decimal numbers. These are arbitrarily sized and exact. There are no floats or doubles in Ioke. There is also a potential place for Number Complex at the same layer as Number Real, although complex numbers are not currently implemented. There are also plans for implementing a unit system further down the line. Number Infinity represents the singleton infinity object.
Literal integers can be written using either decimal or hexadecimal notation. Hexadecimal notation begins with 0x or 0X and are then followed by one or more hexadecimal letters. They can be either upper or lower case. A decimal literal number is written using one or more decimal letters, but nothing else.
There is no literal to create ratios - these can only be created by division of integers. Negative numbers have no literal syntax, but preceding a number with a minus sign will call the message - on the number and generate the negative value.
A literal integer will be transformed into a call to internal:createNumber, which takes one native Java String from which to create the number.
Some examples:
1234444444444444444444444444444444444444235234534534
0
0xFFFFF
Decimals
Literal decimal values can be written either using exponential notation, or using a decimal dot. A decimal dot notation can be combined with exponential notation. Exponential notation starts with a number or a decimal number, followed by lower or upper case E, followed by an optional sign, and then followed by one or more decimal letters.
A literal decimal will be transformed into a call to internal:createDecimal, which takes one native Java String from which to create the decimal.
Some examples:
0.0
1E6
1E-32
23.4445e10
Symbols
Symbols aren't exactly syntax, but they aren't exactly messages either. Or rather, they are messages that will evaluate to the symbol that represent themselves. Symbol is a kind in Ioke. There are two kinds of symbols - the first one is simple symbols that can be parsed as is. The second is symbols that can't be parsed as is. Symbols are preceded by a colon and then directly followed by the symbol text. If it can't be parsed correctly, the value should be surrounded by quotes, and this will be turned into a call to the method :, which takes the text as argument. That means that you can actually get dynamic symbols by calling the : method.
Some examples:
:foo
:flaxBarFoo
:""
:"mux mex mox \n ::::::::"
Operator shuffling
One exception to the way message handling works in Ioke is operators. All the so called operators in this section is possible to call directly in message passing position too -- but to make it possible to use them in a more natural way, the parsing step will handle them a bit differently, and then do a shuffling step that actually takes operator precedence into account. So all the common operators will generally work as you expect them to -- although I recommend adding parenthesis when something is possibly unclear.
Ioke has a slightly larger amount of operators than most other languages. Most of these are currently unused, but they are certainly available for use for any purpose the programmer wants to use it for. Many adherents of other languages (Java, I'm looking at you) claim that operator overloading is evil. I don't believe that is true, seeing as how it works so well in Ruby, so Ioke instead allow you quite large freedom with regards to operators.
The precedence rules for regular operators can be found in the cell 'Message OperatorTable operators', which is a regular Dict that can be updated with new values. The new values will obviously not take effect until the current code has run, and a new parse is started.
Note that the below is only the operators that have defined precedence rules. As noted in the section on syntax, you can use any operator you want really. It is easy to add new precedences to the table, either temporarily or permanently.
At the time of writing, the available operators - in order of precedence - are these:
- !
- #
- $
- ?
- ~
- **
- %
- *
- /
- +
- -
- ∩
- ∪
- <<
- >>
- <
- <=
- <=>
- <>
- <>>
- >
- >=
- ≤
- ≥
- ⊂
- ⊃
- ⊆
- ⊇
- !=
- !~
- ==
- ===
- =~
- ≠
- &
- ^
- |
- &&
- ?&
- ?|
- ||
- !>
- !>>
- #>
- #>>
- $>
- $>>
- %>
- %>>
- &&>
- &&>>
- &>
- &>>
- **>
- **>>
- *>
- *>>
- +>
- +>>
- ->
- ->>
- ..
- ...
- />
- />>
- <->
- =>
- =>>
- ?>
- ?>>
- @>
- @>>
- ^>
- ^>>
- |>
- |>>
- ||>
- ||>>
- ~>
- ~>>
- ∘
- %=
- &&=
- &=
- **=
- *=
- +=
- -=
- /=
- <<=
- >>=
- ^=
- and
- nand
- nor
- or
- xor
- |=
- ||=
- <-
- import
- return
And as mentioned above, all of these can be used for your own purpose, although some of them already have reserved meanings. This document will cover most of the used operators, while the rest can be found in the reference.
Since this operator shuffling happens, that also means that an Ioke program has a canonical inner form that can differ from the source text. When you use introspection of any kind, you will get back that canonical form which might not look exactly like you expected. Similarly, if you ask some code to print itself, it will use the canonical form instead of the operator skin. Macros that modify message chains should work against the canonical form, and nothing else.
What an operator does depends on the result of sending the message of that name to the receiver, just like regular messages. In fact, to Ioke there really isn't any difference, except that the parsing takes special notice about operators and assignment operators.
Assignment shuffling
Much like with regular operators, trinary - assignment - operators are subject to a kind of shuffling. This shuffling differs from regular operator shuffling, in that it will shuffle around two things - the left hand side and the right hand side. This is true for every assignment operator except for the unary ones, which will only reshuffle one message.
A few examples might make the translation easier to perceive. The first item is the readable form, while the second form is the canonical form:
foo = 1 + 2
=(foo, 1 +(2))
Ground foo *= "text"
Ground *=(foo, "text")
bar foo(123) = 42
bar =(foo(123), 42)
flux++
++(flux)
These examples show some more advanced details -- specifically the fact that assignment operators generally work on "places", not on names or cells. This will be more explored in the chapter on assignment. The important thing to notice from the above examples is that for most assignments two things will be rearranged. For the unary operators only one thing will be moved.
Just as with regular operators, the assignment operators have information in the 'Message OperatorTable' cell. The specific cell is 'Message OperatorTable trinaryOperators', and it matches an assignment operator to either the integer 1, or the integer 2. Everything with 1 will be matched as being unary assignment.
The currently available assignment operators are:
- =
- ++
- --
- +=
- -=
- /=
- **=
- *=
- %=
- &=
- &&=
- |=
- ||=
- ^=
- <<=
- >>=
Just as with regular operators, what an assignment operator does depend on what the result is from sending the message of that name to the receiver object, just like with any type of message.
Inverted operators
In addition to the regular binary operators and the trinary assignment operators, Ioke also sports inverted operators. These aren't actually used anywhere in the core distribution, but they might be useful at some time or another. The basic idea is that sometimes you want to have the right hand side of an expresssion become the receiver of an operator call, and the left hand side become the argument to the operator. Inverted operators allow this.
As with both the binary and trinary operators, you can find and update information about inverted operators in the cell 'Message OperatorTable invertedOperators'. To make this a little less abstract, let us look at two simple examples and what they translate into:
"foo" :: [1, 2, 3, 4] map(asText)
;; will be translated to
[1, 2, 3, 4] map(asText) ::("foo")
;; provided we have an inverted
;; operator called 'doit'
abc foo quux doit another time
;; will be translated to
another time doit(abc foo quux)
Execution model
The way an Ioke program works is very simple. Everything executes based on two things. The first is the context, or the ground, and the second is the receiver. The first message sent in each message chain will have the ground as receiver. The default ground in Ioke source files is an object called Ground
. This object is in the mimic chain for most regular objects created in Ioke, which means that things defined at the top level will generally be available in most objects. Inside of methods and blocks, the ground will be different. Exactly in what way is defined by the type of code executing.
Every message in a chain will be sent to the receiver of that message. That receiver is the result of the last message, or the current ground if there was no previous message, or if that previous message was a terminator. So Ioke code like foo bar(flux bar) quux
involves 5 different messages.
- The message
foo
is sent toGround
, which is the current ground and also the default receiver. - The message
bar
is sent to the result of thefoo
message. The value returned will be activated. - The cell
bar
contains a method in this case, and that method expects one argument, so that forces evaluation of the arguments. - The message
flux
is sent toGround
, since it's the ground and there is no prior message inside of an argument list. - The message
bar
is sent to the result of theflux
message. - The result of the
bar
message is used as the argument value given to the outsidebar
method. - The message
quux
is sent to the result of the initialbar
message. - The result of the
quux
message is thrown away, unless this code is part of a larger piece of code.
This description generally describes what happens in the case of this code. The more general control flow is this:
- A message is encountered
- If the message is a symbol message, the corresponding symbol will be returned.
- Otherwise the name of the message will be looked up in the receiver, or in the receivers mimics.
- If the name is found and is not activatable, the value of that name (the cell) is returned.
- If the name is found and is activatable, it will be activated, with the current ground, receiver and message sent to the activatable object.
- If the name is not found, a second search is done for the name
pass
. If a pass is found, use that instead of the name of the original message, and go back to 4. - If a pass is not found, signal a
Condition Error NoSuchCell
condition.
Exactly what happens when an object is activated depends on what kind of code gets activated. It's really up to the method, block or macro to handle evaluation of arguments in any way it likes - including not evaluating them. For a description of the default models available, see the chapter on code.
Objects
The object model of Ioke is quite simple. Everything in Ioke is an object that follows these same rules. An object is something with an identity. It can have zero or more mimics, and zero or more cells. An object can also have a documentation text. Some objects can have a native data component. This acts more or less like a hidden cell that contains information that can't be directly represented in Ioke - for example the actual text in a Text. Or the actual number in a Number. Or the actual regular expression in a Regexp. These objects are the core types that contain primitive information.
A cell is the main way of representing data in Ioke. A cell has a name and a value. Every value in Ioke is a cell - every time you send a message, a cell is looked up for the value of that cell. Cells can contain any kind of data. In other languages, cells are generally called properties or slots. They are quite close to instance variables that also can contain methods. Cells can be added and removed at any time during runtime.
A mimic could also be called the parent of the object. Ioke is a prototype based language, which means that there is no distinction between classes of objects, and the objects themselves. In fact, any object can be used as the "class" of a new object. The word for that is mimicking, since the word "class" loses it's meaning in this kind of language. It's most common for an object to mimic one other object, at least initially. It's impossible to create an object that doesn't mimic anything, but you can remove all mimics for an object after the fact. You can also add more mimics. This turns out to be useful to represent shared functionality in the manner of Ruby mixins, for example. The actual effect of a mimic is that when a cell can't be found in the current object, all mimics will be searched for that cell (depth-first). So all cells available in an object's mimic is available to the object too. This is the inheritance part of Object-Oriented Programming.
In many places you will find the word "kind" being used. A Kind is by convention an object that is used primarily to use as a mimic for other objects. The convention is that kinds are named with an initial upper case letter, while everything else starts with a lower case letter. The assignment process of Ioke also uses this convention to automatically set a cell called "kind" on any object that gets assigned to a name matching this convention.
The rest of this chapter will discuss the kinds that are the basis of the object system.
Base
The kind called Base is the top of the mimic chain. It's not generally useful in itself as it only defines the bare minimum of cells to make it possible to add new cells to it, mimic it, and so on. But if you want an object that is possible to use but not include most of the other stuff, Base is place to begin. Be careful when defining methods in Base, since it doesn't have access to most of the namespace. In fact, it doesn't even know about its own name. Base can act as a kind of blank slate, if needed, but it's probably easier to just create a regular object and remove all mimics from it after the fact.
Base defines these cells:
- kind
- returns the kind of the object, which is "Base".
- notice
- returns the short notice of the object, which is "Base". Refer to Introspection for more information about notice.
- =
- Takes two values, the first a place and the second a value, and assigns the place named to that value. Refer to Assignment for more information about it.
- ==
- Compares this object against the argument. Returns true if they are the same, otherwise false.
- cell
- Takes one argument that should be the name of a cell that exists, and returns the value of the cell unactivated.
- cell=
- Sets a cell to a specific value. Used to set cells that can't be set using the regular assignment model. Refer to Assignment for more information about it.
- cell?
- Takes one argument that should be the name of a cell to check if it exists in this objects mimic chain.
- cellNames
- Returns a List containing the names of all cells this object contains.
- cells
- Returns a Dict with all cells this object contains. The key is the name and the value is the cell value.
- cellOwner
- Returns the closest mimic that has a cell with the name given as argument to the message. A condition will be signalled if you try to find the owner of a cell that doesn't exist in this mimic tree. This method will only return the closest cell owner for the named cell. It will not use "pass", so it's the responsibility of pass-implementers to make it return a correct result for those names.
- cellOwner?
- Takes the name of a cell and returns true if the receiver of the message defines a cell by that name, otherwise false. Note that there can be more than one cell owner in a message chain. This just returns true if the current receiver is the closest one.
- removeCell!
- Removes the named cell from the current object. This means that if the current cell shadowed cells in mimics, those can be called again. It only removes a cell if the receiver is the owner of that cell. Otherwise it is an error to call this method.
- undefineCell!
- Makes it impossible to find a cell from the receiver. In all ways it looks like this cell doesn't exist in the mimic chain at all, even if mimics define several implementations of it. The use of undefining can make an object conceptually totally clean from cells, although it might be hard to use the objec after that. An interesting side-effect of the way these methods work is that removeCell! can be used to remove the undefine. So if you call removeCell! with a cell name and a receiver that has been called with undefine earlier, that undefine-status will be removed, and access to mimic versions of the cell will be possible again. Look at the specs for a better understanding.
- documentation
- Returns the documentation text for this object, or nil if no documentation exists for it.
- documentation=
- Sets the documentation text for this object.
- mimic
- Returns a newly created object that has the receiver as mimic. This is the magic way of creation new objects in Ioke. It is also the ONLY way to do it.
- hash
- The base implementation of hash coding - currently the default implementation just return an identity dependent hash code.
- identity
- Returns the object receiving the message.
All of these methods are described further in the reference.
Ground
As mentioned above, Ground is the default ground/context for evaluation. Ground IokeGround and JavaGround, and IokeGround mimics Base and DefaultBehavior. IokeGround is special in that this is the place where all top level kinds are defined. If you want to create a top level kind, you should put it in IokeGround. If you take a look in IokeGround, you will see that it contains cells for Text, Dict, List, Base, Origin, itself and many other. Ioke doesn't have any global state at all, but IokeGround is as close as it gets. IokeGround and Ground should in most cases not be mimicked directly.
JavaGround is the place where all Java integration support is integrated into Ioke.
Origin
Origin should be the place where most objects in Ioke start from. It is specifically created to be the origin of objects. As such it doesn't contain many cells for itself, but it mimics Ground and has access to everything from Base, DefaultBehavior and Ground in that way. When adding new more or less global functionality, Origin is probably the best place to put it. Currently, the only cells Origin contains is for purposes of printing itself.
Origin also happens to be the point where initialization is defined. This is really done as an aspect on 'mimic'. If you want an object to be able to be initialized every time a new mimic of it is created, just create a method called initialize in your kind. It will be called by the mimic-aspect. Any arguments given to mimic will be ignored and passed along to initialize. An example:
Foo = Origin mimic
Foo initialize = method("New foo created!" println)
Foo mimic
Foo mimic
Foo initialize = method(arg1, key:, self value = [arg1, key])
Foo mimic(42, key: 15)
Foo mimic(key: "blarg", 42)
There is nothing special with the initialize method, so if you want more initialization to happen in a deep hierarchy, you will have to use super-calls and so on.
DefaultBehavior
DefaultBehavior is a mixin - meaning it should never be the sole mimic of an object. Mixins are generally not grounded in Base, and doesn't contain most of the things you would expect from an object. DefaultBehavior contain almost all the general methods you use when programming Ioke. It contains the internal methods to create values from literals, and most other functionality specified in this document. In short, DefaultBehavior is the work horse, and you should have a pretty good reason to not have it in the mimic chain of an object. Since Ground mimics DefaultBehavior, any object you create from Origin, will have DefaultBehavior in its mimic chain.
The actual implementation of DefaultBehavior is divided into several smaller mixins that are all mixed in to DefaultBehavior. These give more focused pieces of behavior. They are, in alphabetical order:
-
DefaultBehavior Aspects
-
DefaultBehavior Assignment
-
DefaultBehavior BaseBehavior
-
DefaultBehavior Boolean
-
DefaultBehavior Case
-
DefaultBehavior Conditions
-
DefaultBehavior Definitions
-
DefaultBehavior FlowControl
-
DefaultBehavior Internal
-
DefaultBehavior Literals
-
DefaultBehavior Reflection
The recommended way to add new global behavior to Ioke is to either add a cell to one of these, or create a new mixin and mix it in to the appropriate place. If you're adding new flow control features, mixing these in DefaultBehavior FlowControl
might be appropriate, for example.
nil, true, false
The three values nil, true and false are the only values that are considered kinds, even though they start with lower case letters. They are not like the other kinds in the other important way either - these values can not be mimicked, and you will get a condition if you try it. The reason is that Ioke's basic boolean system revolves around these values. It is not entirely certrain that these values will forever be the only boolean values, but for now they are. nil should be used to represent the absence of a value, including the absence of a reasonable return value. false is the quintessential false value, and true is the quintessential true value. The value true isn't strictly necessary since any value except for nil and false are true. This notion of truthness mimics Ruby. The cells nil, true and false are defined in Ground, and they can actually be overridden or changed - but I don't recommend it. I can guarantee lots of chaos and non-working programs from doing it. More info on how these values interact can be found in the section on Comparison.
Assignment
Superficially, Ioke's assignment model is quite simple. But there exists some subtleties too. One of the main reasons for this is that assigning a cell that doesn't exist will create that cell. Where it gets created is different based on what kind of context the assignment happens in. The main difference here is between a method activation context, or a lexical block context.
Ioke also supports assignment of places, which makes assigning much more flexible. A third feature of Ioke assignment is that it will check for the existence of an assignment method before assigning a specific name. This chapter will make all these things clear, and show some examples.
Ioke can also do destructuring assignment, which means you can assign more than one value at the same time. Destructuring features nesting and can also apply places, which gives it a lot of flexibility.
Let's start with a small example of simple assignment:
foo = Origin mimic
foo x = 42
foo y = 13
foo x += 2
The first line creates a new Origin mimic, and then assigns that to the name foo. Since this code executes at the top level, "foo" will be a new cell created in Ground. The second line creates a new cell called "x" inside the "foo" object. It gets assigned the value 42. The third line creates a "y" cell, and the fourth line sends the += message, which will first call +, and then assign using =. So at the end of this program, "foo" will contain two cells: "x" with value 44, and "y" with value 13. As mentioned above, cells get created the first time they are assigned to. If you need to create a cell in a specific object, just namespace it. For example, if you want to make sure that you create a cell in Ground, just do "Ground foo = 42".
Inside of a method, the situation is exactly the same. If you assign something, it will be assigned in the current context, which is the local activation context (meaning it's the place where local variables are available). There are two situations where this doesn't hold true. The first one is within the special method "do". This method will take any code as argument and execute that with the receiver of the "do" message as the ground/context of the code inside it. That means "do" is a good way to create new cells inside an object.
This is a bit academic, so lets take a look at an example:
Foo = Origin mimic
Foo x = method(
;; this creates a local variable in the method activation
foo = 42
)
Foo = Origin mimic
Foo do(
;; this creates the cell foo inside of Foo
foo = 42
)
Here you can see a method defined called x. This method will just create a new local cell, which means calling the method will not make any difference on its receiver at all. The call to "do" in contrast will immediately execute the code inside it, and this code will create the cell "foo" inside of "Foo".
The second exception to the general rule is when executing inside of a lexical context. A lexical context is basically established inside of a block, but can also be created transparently when sending code to a method. A lexical block will try to not create new cells. When you assign a cell without a specific place to assign it, a lexical block will first see if there is any cell with that name further out, and if so it will make the assignment there instead. Only when no such cell exists, a new cell will be created in the lexical context. This code shows this in action:
x = 42
fn(x = 43. y = 42) call
x ;; => 43
y ;; => Condition Error NoSuchCell
The "fn" message creates a new lexical block. The chapter on code will talk more about this. But as you can see, this block assigns 43 to the cell "x", and 42 to the cell "y". But since the cell "y" doesn't exist, it will only be created inside the lexical context, while "x" exists outside, and will be assigned a new value instead. The basic idea is that code like this should behave like you expect it to behave.
The canonical form of assignment is a bit different from the way you usually write code in Ioke. The section on the syntax of assignments talked a bit about this. Specifically, something like "foo = 42" will get translated into "=(foo, 42)". That also means that assignment is just a regular method call, and can be overridden or removed just like any other method. That is exactly how both lexical context, and local method context make it possible to have different logic here. This is true for all assignment operators.
All assignment operators take as their first argument the place to assign to. This place will be unevaluated. Only the second argument to an assignment will be evaluated. In most cases, a place is the same thing as a cell name, but it doesn't have to be. Let's look at the case of assigning a cell with a strange name. Say we want to assign the cell with the no name. We can do it like this:
cell("") = 42
What happens here is a bit subtle. Since the left hand side of the assignment takes arguments, the "=" method figures out that the assignment is not to a simple cell name, but to a place. The parsing step will change "cell("") = 42" into "=(cell(""), 42)". Notice here that the argument comes along into the specification of the place. When this happens, the assignment operator will not try to create or assign a cell - instead it will in this case call the method cell=. So "cell("") = 42" will ultimately end up being the same as "cell=("", 42)". This way of transforming the name will work the same for all cases, so you can have as many arguments as you want to the place on the left hand side. The equals sign will be added to the method name, and a message will be sent to that instead.
This makes assignment of places highly flexible, and the only thing you need to do is implement methods with the right names. This feature is used extensively in Lists and Dicts to make it easy to assign to specific indexes. So, say we have a list called x. Then this code: "x[13] = 42" will be transformed into "x =([](13), 42)" which will in turn be transformed into "x []=(13, 42)". Ioke lists also has an at= method, so you can do "x at(13) = 42" which will call at=, of course.
The second transformation that might happen is that if you try to assign a cell that has an assigner, you will call that assigner instead of actually assigning a cell. So, for example, if you do "foo documentation = 42", this will not actually create or assign the cell "documentation". Instead it will find that Base has a cell called "documentation=", and instead send that message. So the prior code would actually be equivalent to "foo documentation=(42)".
All of these assignment processes together make it really easy to take control over assignment, while still making it very obvious and natural in most cases.
Destructuring assignment
The easiest example of destructuring assignment looks like this:
(x, y) = (42, 44)
Note that the parenthesis are necessary both on the left hand and right hand side. This will assign x to 42 and y to 44 in the current context, following the assignment rules given above. This assignment will happen in parallel, which means you can do the obvious swapping of values in one operation:
(x, y) = (y, x)
This also works for more than two simultaneous assignments.
The right hand side of an expression like this is expected to be a regular value that can be converted into a tuple. This include all Enumerable objects, since asTuple is defined there. That means you can also do something like this:
(x, y) = [42, 44]
If the destructurings doesn't match up, this is an error. Something like
(x, y) = [42, 44, 46]
will signal a condition Condition Error DestructuringMismatch
. This might not always be convenient. Say you don't care about the rest of the arguments and want to extract the two first elements no matter what, you can do that by ignoring the rest - this is done with the underscore:
(x, y, _) = 1..100
You can also use the underscore to ignore specific locations in other places. These will only ignore one element though:
(x, _, y, _) = 1..100
x should == 1
y should == 3
You can nest destructurings. All of the above mechanisms work correctly while doing that. So for example you can do this:
(x, y, (q, p, _), z) = (10, 42, 1..100, 55)
x should == 10
y should == 42
q should == 1
p should == 2
z should == 55
Finally, all of the above work with places as well as with regular names. This means you can for example do list indexed assignment inside one of these elements:
x = (1..10) asList
x ([5], [0], [2]) = (2, 3, 4)
x should == [3,2,2,4,5,2,6,7,8,9,10]
Let
Sometimes you really need to change the value of something temporarily, but then make sure that the value gets set back to the original value afterwards. Other situations often arise when you want to have a new name bound to something, but maybe not for a complete method. This might be really useful to create closures, and also to create localized helper methods. For example, the Ioke implementations for Enumerable use a helper syntax macro. This macro is bound temporarily, using a let form. This ensures that the syntax doesn't stay around and pollute the namespace.
A let in Ioke can do two different things, that on the surface look mostly the same but are really very different operations. The first one is to introduce new lexical bindings, and the second is to do a dynamic rebind of a specific place. The easiest way of thinking about it is that the lexical binding introduces a local change or addition to the available names you're currently using, while a dynamic rebinding will change the global state temporarily, and then set it back.
This sounds really academic, so let us go for some examples. We begin with lexical bindings.
;; put everything in a method to show explicit scope
foo = method(x, y,
x println ; => argument value of x
let(x, 14,
x println ; => 14
)
x println ; => argument value of x
y println ; => argument value of y
y = 13
y println ; => 13
let(y, 14,
y println ; => 14
)
y println ; => 13
z println ; will signal condition
let(z, 42,
z println ; => 42
)
z println ; will signal condition
)
Here a new method is created that has two arguments, x and y. The first let-expression will create a new scope where a binding from x to 14 is established. This binding is valid until the end of the let-form (but it can be changed, doing an assignment will set the value to something else, but only until the end of the let form). The same thing is true with y. We can change the value of y outside of the let form. That changes the actual argument variable. But a let form that binds y will only have it active for a limited time. Finally, a let form can also create totally new variables, as when creating z.
I didn't show any example of it, but the first part of a let-name can be any kind of place, not just a simple name. Anything you can use with =, can be used as a name for let. So you could do something like let(cell(""), 42, nil) if you wanted to.
OK, so that's lexical binding. What about dynamic rebinding? The main difference in a dynamic binding is that the scope you work in is something that is referencable from other scopes. In most cases this will be global places, but not necessarily. You can also rebind cells inside of other objects with the dynamic binding feature.
bar = method(
let(Origin foo, method("haha" println),
"x" foo
Origin foo
[1,2,3] foo
)
let(Text something, 42,
"abc" something println ; => 42
)
"abc" something println ; will signal condition
let(Text inspect, "HAHA",
"foo bar qux" inspect println ; => "HAHA"
)
"foo bar qux" inspect println ; => #["foo bar qux"]
let(Text asRational, method(42),
(3 + "haha") println ; => 45
)
)
This example actually changes things quite a lot. The first and second examples introduce new cells into existing places, uses them and then doesn't do anything. The third example actually overrides an existing cell in Text - inspect - and then uses it inside of the let code. Finally, after the let block is over, we see that the original method is back. The fourth example shows that our changes with let actually are global. There is no asRational on Text, but we add it temporarily and can then use it in arithmetic with numbers. This is once again a temporary change that will disappear afterwards.
Ioke's let-form is incredibly powerful, and it allows very nice temporal and localized changes. Of course, it's a power that can be abused, but it gives lots of interesting possibilities for expression.
{{:Guide:Control flow}