User Tools

Site Tools


at:tutorial:modular

Differences

This shows you the differences between two versions of the page.

Link to this comparison view

Both sides previous revision Previous revision
Next revision
Previous revision
at:tutorial:modular [2007/05/15 19:33]
tvcutsem *
at:tutorial:modular [2013/05/17 20:24] (current)
tvcutsem adjusted
Line 1: Line 1:
 ====== Modular Programming ====== ====== Modular Programming ======
- 
-<note> 
-This Tutorial is still under heavy construction! 
-</note> 
  
 In this tutorial chapter, we introduce AmbientTalk's language features that have to do with writing //modular// programs. The term "modular" can be taken quite broadly in this context. By "modular" programming, we here mean programs hierarchically structured into multiple files and objects composed from multiple so-called "trait" objects. We also describe AmbientTalk's lightweight support for classifying objects and how this enables exception handling based on "types". In this tutorial chapter, we introduce AmbientTalk's language features that have to do with writing //modular// programs. The term "modular" can be taken quite broadly in this context. By "modular" programming, we here mean programs hierarchically structured into multiple files and objects composed from multiple so-called "trait" objects. We also describe AmbientTalk's lightweight support for classifying objects and how this enables exception handling based on "types".
Line 61: Line 57:
 The downside of such as scheme is that the absolute path name of the ''math.at'' file is hardcoded in the AmbientTalk application. If we would later decide to move the file, our AmbientTalk code using that file would be broken! To avoid these problems, AmbientTalk provides the ''lobby'' object. The ''lobby'' is an object defined in the top-level scope whose slots are bound to namespace objects. The names of the slots, and the directory paths of the namespace objects bound to them are not provided in AmbientTalk directly, but rather when starting the AmbientTalk interpreter. The downside of such as scheme is that the absolute path name of the ''math.at'' file is hardcoded in the AmbientTalk application. If we would later decide to move the file, our AmbientTalk code using that file would be broken! To avoid these problems, AmbientTalk provides the ''lobby'' object. The ''lobby'' is an object defined in the top-level scope whose slots are bound to namespace objects. The names of the slots, and the directory paths of the namespace objects bound to them are not provided in AmbientTalk directly, but rather when starting the AmbientTalk interpreter.
  
-The [[at:tutorial:iat|interactive ambienttalk shell]] (''iat'') has a command-line parameter called the **object path**. This argument is a list of "name=path" entries separated by colons. For example, in order to create a namespace object called ''lib'' which is bound to ''/home/ambienttalkuser/examples'', it suffices to start ''iat'' as follows:+The [[at:tutorial:iat|interactive ambienttalk shell]] (''iat'') has a command-line parameter called the **object path**. This argument is a list of "name=path" entries separated by colons on Mac/Linux or semicolons on Windows. For example, in order to create a namespace object called ''lib'' which is bound to ''/home/ambienttalkuser/examples'', it suffices to start ''iat'' as follows:
  
 <code> <code>
Line 68: Line 64:
  
 Whenever a new actor is created by the AmbientTalk interpreter, it uses the entries in the object path to initialize the ''lobby'' object of the actor. Hence, by starting up the interpreter as shown above, executing ''lobby.lib'' in any actor returns a reference to a namespace object bound to the ''examples'' directory. This effectively removes absolute path names from AmbientTalk source code and even enables you to quickly reconfigure AmbientTalk code to use other libraries by loading the code using another object path. Whenever a new actor is created by the AmbientTalk interpreter, it uses the entries in the object path to initialize the ''lobby'' object of the actor. Hence, by starting up the interpreter as shown above, executing ''lobby.lib'' in any actor returns a reference to a namespace object bound to the ''examples'' directory. This effectively removes absolute path names from AmbientTalk source code and even enables you to quickly reconfigure AmbientTalk code to use other libraries by loading the code using another object path.
 +
 +Think of AmbientTalk's object path as the equivalent of Java's classpath.
  
 <note> <note>
Line 121: Line 119:
  
 In this section, we describe how the ''import'' statement can also be used to compose multiple objects. In this respect, objects can be regarded as //traits//. [[http://www.iam.unibe.ch/~scg/Research/Traits|Traits]] are a model of object-oriented software composition, similar to (but more advanced than) mixins. A trait can be regarded as a kind of abstract class, which does not define any state, which //provides// a set of methods and which //requires// a set of methods in order to be instantiated. In a class-based language, a class may //use// one or more traits. The class takes on the role of compositor and is responsible for ensuring that, once all traits have been imported, each trait its required interface is provided by the class or other imported traits. A trait is almost literally a piece of code which can be "copy-pasted" into a class. In this section, we describe how the ''import'' statement can also be used to compose multiple objects. In this respect, objects can be regarded as //traits//. [[http://www.iam.unibe.ch/~scg/Research/Traits|Traits]] are a model of object-oriented software composition, similar to (but more advanced than) mixins. A trait can be regarded as a kind of abstract class, which does not define any state, which //provides// a set of methods and which //requires// a set of methods in order to be instantiated. In a class-based language, a class may //use// one or more traits. The class takes on the role of compositor and is responsible for ensuring that, once all traits have been imported, each trait its required interface is provided by the class or other imported traits. A trait is almost literally a piece of code which can be "copy-pasted" into a class.
 +
  
 ==== import as trait composition ==== ==== import as trait composition ====
Line 164: Line 163:
   };   };
   def each: clo {   def each: clo {
-    start.to: end do: clo+    start.to: end-1 do: clo
   };   };
 }; };
Line 183: Line 182:
 </code> </code>
  
-So, ''import'' defines small methods which delegate the request to the original trait. The use of delegation is crucial here: it means that within the context of the trait, ''self'' is bound to the object using the trait. Hence, when ''reject:'' is invoked on a ''Range'' object, ''Enumerable'''s ''self.each:'' will refer to the correct implementation of ''Range''.+So, ''import'' defines small methods which delegate the request to the original trait (using AmbientTalk's support for [[:at:tutorial:objects#first-class_delegation|explicit delegation]]). The use of delegation is crucial here: it means that within the context of the trait, ''self'' is bound to the object using the trait. Hence, when ''reject:'' is invoked on a ''Range'' object, ''Enumerable'''s ''self.each:'' will refer to the correct implementation of ''Range''. 
 + 
 +Note that in AmbientTalk, a trait does not need to explicitly specify the set of methods which it "requires" from its composite. However, for documentation purposes, it is often very useful to explicitly state the methods on which a trait depends. One may do so by defining those methods in the trait, and annotating them with the ''@Required'' type tag (which is predefined in the ''lobby.at.lang.types'' module). These methods will //not// be imported when the trait is used by a composite object (otherwise they would cause a conflict with the real implementation methods in the composite). Below is an example of how the ''Enumerable'' trait can be modified to explicitly state its required methods: 
 + 
 +<code> 
 +def Enumerable := object: { 
 +  def collect: clo { /* as before */ }; 
 +  def detect: pred { /* as before */ }; 
 +  def reject: pred { /* as before */ }; 
 +  def each: clo @Required; 
 +
 +</code>
  
 ==== Resolving conflicts: exclusion and aliasing ==== ==== Resolving conflicts: exclusion and aliasing ====
Line 194: Line 204:
 // do not import the slots collect: and detect: // do not import the slots collect: and detect:
 import Enumerable exclude collect:, detect: import Enumerable exclude collect:, detect:
-// do not import collect: and import reject: as remove +// do not import collect: and import reject: as remove: 
-import Enumerable alias reject: := remove exclude collect:+import Enumerable alias reject: := removeexclude collect:
 </code> </code>
  
 If the compositor defines an alias for an imported slot, it is good practice to ensure that the compositor has (or imports) a slot with the original name as well. That way, if the trait object performs a self-send to invoke one of its own methods, it will find a matching slot in the compositor. If the compositor aliases a slot and does not define the slot itself, a lookup by the trait of the original slot name would fail. If the compositor defines an alias for an imported slot, it is good practice to ensure that the compositor has (or imports) a slot with the original name as well. That way, if the trait object performs a self-send to invoke one of its own methods, it will find a matching slot in the compositor. If the compositor aliases a slot and does not define the slot itself, a lookup by the trait of the original slot name would fail.
  
-===== Classifying objects using stripes =====+<note> 
 +''import'' adds two names to the ''exclude'' clause by default: 
 +  * ''super'' (the parent object) because this slot is present in //all// AmbientTalk objects. 
 +  * ''~'' (the current namespace), because this slot is present in //all// namespace objects. 
 + 
 +Moreover, all methods of the trait object annotated with ''@Required'' are automatically excluded upon ''import''
 +</note> 
 + 
 +===== Classifying objects using type tags =====
  
 In class-based languages, classes are a useful tool for categorising objects. Each object is an instance of some class, and sometimes it is useful to be able to ask to which class an object belongs. However, it is well-known by proponents of object-oriented programming that explicitly referring to the class of an object breaks that object's encapsulation. The reason is that a class plays other roles besides object classification. Classes also define an //implementation// for the object's methods. Hence, knowing the class of an object often boils down to knowing the exact implementation of an object, which no longer makes the code polymorphic. This, in turn, means that the code becomes less reusable and more easily breaks when the software evolves over time (because of the addition/removal of classes). In class-based languages, classes are a useful tool for categorising objects. Each object is an instance of some class, and sometimes it is useful to be able to ask to which class an object belongs. However, it is well-known by proponents of object-oriented programming that explicitly referring to the class of an object breaks that object's encapsulation. The reason is that a class plays other roles besides object classification. Classes also define an //implementation// for the object's methods. Hence, knowing the class of an object often boils down to knowing the exact implementation of an object, which no longer makes the code polymorphic. This, in turn, means that the code becomes less reusable and more easily breaks when the software evolves over time (because of the addition/removal of classes).
  
-Nevertheless, the ability to classify objects according to //abstract// "types" (we don't use the term "classes" to avoid any confusion) is sometimes useful, especially when the "type-test" cannot be properly refactored into a polymorphic message send. To this end, AmbientTalk provides //stripes//. A //stripe// is simply a brand, a tag, a marker which can be attached to an object. The terminology stems from an system called //StripeTalk// which was used to experiment with different kinds of object classification schemes. Stripes are defined using the ''defstripe'' keyword. A stripe is identified by its name. For example, we may want to distinguish indexable from non-indexable objects:+Nevertheless, the ability to classify objects according to //abstract// "types" (we don't use the term "classes" to avoid any confusion) is sometimes useful, especially when the "type-test" cannot be properly refactored into a polymorphic message send. To this end, AmbientTalk provides //type tags//. A //type tag// is simply a brand or marker which can be attached to an object. Type tags are defined using the ''deftype'' keyword. A type tag is identified by its name. For example, we may want to distinguish indexable from non-indexable objects:
  
 <code> <code>
-defstripe Indexable;+deftype Indexable;
 </code> </code>
  
-Since a stripe is an abstract type or category, it makes sense to define "substripe" (subtyperelations on them. Also, since stripes do not carry an implementation, there is no problem in defining a stripe to be a substripe of one or more other stripes. For example, we may want to state that a sortable object is always enumerable as well as ordered:+Since a type tag is an abstract type or category, it makes sense to define subtype relations on them. Also, since type tags do not carry an implementation, there is no problem in defining a type tag to be a subtype of one or more other type tags. For example, we may want to state that a sortable object is always enumerable as well as ordered:
  
 <code> <code>
-defstripe Enumerable; +deftype Enumerable; 
-defstripe Ordered; +deftype Ordered; 
-defstripe Sortable <: Enumerable, Ordered;+deftype Sortable <: Enumerable, Ordered;
 </code> </code>
  
-When defining an object, the object can be striped (taggedwith one or more stripes.+When defining an object, the object can be tagged with one or more type tags.
  
 <code> <code>
 def Array := object: { def Array := object: {
   ...   ...
-stripedWith: [ Indexable, Sortable ]+taggedAs: [ Indexable, Sortable ]
 </code> </code>
  
-Finally, the most useful operation defined on stripes is the "stripe test": it allows objects to test whether an object is tagged with the appropriate stripe.+Finally, the most useful operation defined on type tags is the "type test": it allows objects to test whether an object is tagged with the appropriate type tag.
  
 <code> <code>
-is: Array stripedWith: Indexable+is: Array taggedAs: Indexable
 >> true >> true
-is: Array stripedWith: Ordered+is: Array taggedAs: Ordered
 >> true >> true
-is: Array stripedWith: Set+is: Array taggedAs: Set
 >> false >> false
 </code> </code>
  
-The stripe test determines whether an object //or one of its parents// is striped with the given stripe //or a substripe// of the stripe.+The type test determines whether an object //or one of its parents// is tagged with the given type tag //or a subtype// of the type tag.
  
-The stripes with which an object has been tagged can be retrieved by means of the ''stripesOf:'' primitive:+The type tags with which an object has been tagged can be retrieved by means of the ''tagsOf:'' primitive:
  
 <code> <code>
-stripesOf: Array +tagsOf: Array 
->> [ <stripe:Indexable>, <stripe:Sortable> ]+>> [ <type tag:Indexable>, <type tag:Sortable> ]
 </code> </code>
  
 <note> <note>
-Stripes can best be compared to empty Java interface types. Such empty interfaces are sometimes used in Java purely for the purposes of marking an object. Examples are ''java.io.Serializable'' and ''java.lang.Cloneable''. An empty interface type can be implemented by any class (object) and hence only serves the purpose of distinguishing objects by type (by means of ''instanceof'' in Java).+Type tags can best be compared to empty Java interface types. Such empty interfaces are sometimes used in Java purely for the purposes of marking an object. Examples are ''java.io.Serializable'' and ''java.lang.Cloneable''. An empty interface type can be implemented by any class (object) and hence only serves the purpose of distinguishing objects by type (by means of ''instanceof'' in Java).
 </note> </note>
  
-==== Native Stripes ==== 
  
-The module ''/.at.stripes'' shipped with the system library contains the stripe definitions of the native data types of the interpreter. These stripes can be used to perform type tests on objects, for example:+==== Native Type Tags ==== 
 + 
 +The module ''/.at.lang.types'' shipped with the system library contains the type definitions of the native data types of the interpreter. These type tags can be used to perform type tests on objects, for example:
  
 <code> <code>
-is: 1 stripedWith: /.at.stripes.Number+is: 1 taggedAs: /.at.lang.types.Number
 >> true >> true
-is: "foo" stripedWith: /.at.stripes.Text+is: "foo" taggedAs: /.at.lang.types.Text
 >> true >> true
 </code> </code>
  
-The stripe ''/.at.stripes.Isolate'' can be used to mark an object as an isolate. Isolate objects are similar to serializable Java objects. They are explained when introducing concurrent programming in [[at:tutorial:actors|a later chapter]].+The type ''/.at.lang.types.Isolate'' can be used to mark an object as an isolate. Isolate objects are similar to serializable Java objects. They are explained when introducing concurrent programming in [[at:tutorial:actors|a later chapter]].
  
-==== Stripes as annotated message sends ====+==== Type tags as annotated message sends ====
  
-In AmbientTalk, messages are objects as well. Quite often, it is useful to stripe a message with a tag that indicates how the message should be processed. AmbientTalk provides syntax for such annotations, as follows:+In AmbientTalk, messages are objects as well. Quite often, it is useful to tag a message with a type tag that indicates how the message should be processed. AmbientTalk provides syntax for such annotations, as follows:
  
 <code> <code>
-obj.m(a,b,c)@stripe +obj.m(a,b,c)@tag 
-obj.m(a,b,c)@[stripe1stripe2]+obj.m(a,b,c)@[tag1tag2]
 </code> </code>
  
-In the [[at:tutorial:actors|chapter on actors]] we use this feature to distinguish purely asynchronous message sends from so-called //future-type// message sends. If the message send is annotated with the ''FutureMessage'' stripe, the asynchronous send returns a future, otherwise it returns ''nil''.+In the [[at:tutorial:actors|chapter on actors]] we use this feature to distinguish purely asynchronous message sends from so-called //future-type// message sends. If the message send is annotated with the ''FutureMessage'' type tag, the asynchronous send returns a future, otherwise it returns ''nil''. 
  
 ===== Exception Handling ===== ===== Exception Handling =====
Line 286: Line 306:
 </code> </code>
  
-The first argument is a closure to execute, delineating a dynamic piece of code to protect with the given exception handler. The third argument is a one-argument closure, invoked when an exception is caught of the right kind. The second argument is a stripe. By default, exceptions are handled based on their stripes. A hierarchy of stripes is used to classify exceptions. Exception handling in AmbientTalk is quite conventional: when a method is raised, the call stack is unwound until an exception handler installed using ''try:catch:using:'' is found whose stripe argument is a superstripe of the raised exception'stripe. If no matching handlers are found, the exception continues unwinding the call stack until either a handler is found or the stack is entirely unwound. In the latter case, a stack trace is printed.+The first argument is a closure to execute, delineating a dynamic piece of code to protect with the given exception handler. The third argument is a one-argument closure, invoked when an exception is caught of the right kind. The second argument is a type tag. By default, exceptions are handled based on their type tags. A hierarchy of type tags is used to classify exceptions. Exception handling in AmbientTalk is quite conventional: when a method is raised, the call stack is unwound until an exception handler installed using ''try:catch:using:'' is found whose type tag argument is a supertype of the raised exception'type. If no matching handlers are found, the exception continues unwinding the call stack until either a handler is found or the stack is entirely unwound. In the latter case, a stack trace is printed.
  
 Raising an exception is done by means of the ''raise:'' primitive: Raising an exception is done by means of the ''raise:'' primitive:
Line 296: Line 316:
 </code> </code>
  
-Note that a new instance of an object named ''XDivisionByZero'' is raised, not the stripe ''DivisionByZero'' itself. An exception is any object that is striped with (a substripe of) the ''lobby.at.stripes.Exception'' stripe. By convention, an exception should have the fields ''message'' and ''stackTrace''. The exception module found under ''lobby.at.exceptions'' contains an auxiliary function named ''createException'' which takes a stripe as argument and returns a new prototype exception object. For example:+Note that a new instance of an object named ''XDivisionByZero'' is raised, not the type tag ''DivisionByZero'' itself. An exception is any object that is tagged with (a subtype of) the ''lobby.at.lang.types.Exception'' type tag. By convention, an exception should have the fields ''message'' and ''stackTrace''. The exception module found under ''lobby.at.lang.exceptions'' contains an auxiliary function named ''createException'' which takes a type tag as argument and returns a new prototype exception object. For example:
  
 <code> <code>
-defstripe DivisionByZero <: lobby.at.stripes.Exception; +deftype DivisionByZero <: lobby.at.lang.types.Exception; 
-def XDivisionByZero := lobby.at.exceptions.createException(DivisionByZero);+def XDivisionByZero := lobby.at.lang.exceptions.createException(DivisionByZero);
 </code> </code>
  
Line 321: Line 341:
   // this is a first-class handler object   // this is a first-class handler object
   def canHandle(exc) {   def canHandle(exc) {
-    is: exc stripedWith: DivisionByZero+    is: exc taggedAs: DivisionByZero
   };   };
   def handle(exc) {   def handle(exc) {
     system.println(exc.message);     system.println(exc.message);
   };   };
-})+taggedAs: [/.at.lang.types.Handler])
 </code> </code>
  
Line 339: Line 359:
   calculateSomething();   calculateSomething();
 } catch: DivisionByZero using: { |e| } catch: DivisionByZero using: { |e|
-  system.println(e.message):+  system.println(e.message);
 } catch: NoSolution using: { |e| } catch: NoSolution using: { |e|
   calculateSomethingElse();   calculateSomethingElse();
Line 348: Line 368:
  
 Care has to be taken that handlers are listed in increasing order of "generality". If the most general handler (e.g. a handler for ''Exception'') is listed first, other handlers will not have been consulted and will never trigger. Care has to be taken that handlers are listed in increasing order of "generality". If the most general handler (e.g. a handler for ''Exception'') is listed first, other handlers will not have been consulted and will never trigger.
 +
 +===== Escaping Continuations =====
 +
 +It is often useful to be able to abort the control flow within a method prematurely. In traditional imperative programming languages, this is done by means of a ''return'' statement (or the ''^'' in Smalltalk). AmbientTalk inherits a similar control structure from the Self programming language. Rather than introducing the notion of a ''return'' statement, AmbientTalk uses the more general concept of an //escaping continuation// to achieve a similar effect. Consider the following code which tests whether a given table ''tbl'' contains an element ''elt'':
 +
 +<code>
 +def contains(tbl, elt) {
 +  { |return|
 +    1.to: tbl.length do: { |i|
 +      if: (tbl[i] == elt) then: {
 +        return(true)
 +      }
 +    };
 +    false
 +  }.escape()
 +}
 +</code>
 +
 +When ''escape()'' is invoked on a block closure, the effect is to apply that block closure to a special function object (the escaping continuation). That object, named ''return'' in the example, behaves as a function of one argument. When that function is invoked, control immediately returns to the end of the block, and the value passed to the escaping continuation is the result of the ''escape()'' call. If the function object is not called, the closure returns normally and its return value is the result of the ''escape()'' call.
 +
 +As can be seen, an escaping continuation is more general than a return statement: the continuation is a first-class function object and can hence be passed on to other objects. Hence, it is possible to return from multiple nested function calls at once. 
 +
 +There is an important limitation to the use of escaping continuations. An escaping continuation is not a full-fledged continuation (such as the one provided by Scheme's ''call/cc''): the escaping continuation is valid **only** in the execution of the block closure. Once control has returned from the block (either because it terminated normally with a value, or by means of the escaping continuation), the escaping continuation can no longer be used. If the continuation function object is e.g. stored in a variable and applied later, when the block already terminated, this will result in an exception being raised.
at/tutorial/modular.1179250425.txt.gz · Last modified: 2007/06/19 10:52 (external edit)