Difference between revisions of "Guide:Core kinds"

From IokeWiki
Jump to: navigation, search
(add dict format specifier)
Line 151: Line 151:
 
"%[%s, %]\n" format([1,2,3])
 
"%[%s, %]\n" format([1,2,3])
 
   ;; => "1, 2, 3, \n"
 
   ;; => "1, 2, 3, \n"
 +
 +
;; insert a dict of values formatted the same
 +
"%:[%s => %s, %]\n" format({sam: 3, toddy: 10})
 +
  ;; => "sam => 3, toddy => 10, \n"
  
 
;; insert splatted values from a list
 
;; insert splatted values from a list

Revision as of 17:26, 19 March 2009

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.

;; 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)
)

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:

ensure(
  conn = Database open
  conn SELECT * FROM a_table,
  conn close!,
  conn reallyClose!,
  conn reallyReallyReallyClose!)

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:

;; 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"]

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:

;; 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 a dict of values formatted the same
"%:[%s => %s, %]\n" format({sam: 3, toddy: 10})
  ;; => "sam => 3, toddy => 10, \n"

;; insert splatted values from a list
"wow: %*[%s: %s %]" format([[1,2],[2,3],[3,4]])
  ;; => "wow: 1: 2 2: 3 3: 4 "

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:

;; 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]

Except for the Enumerable methods, List also defines many other methods that can be highly useful. Some examples are shown below:

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

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.

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"}

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:

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)

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.

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

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.

;; 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

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".

;; 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"

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.

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]

Filtering the contents of a collection can be done using select, which is aliased as filter and findAll.

;; 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]

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.

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

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. 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:

number = "555-12345"
m = #/({areaCode}\d{3})-({localNumber}\d{5})/ =~ number
m areaCode println
m localNumber println

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:

x = "foo"
y = "bar"

x become!(y)
x same?(y) ; => true

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.