– Typee Documentation –
3 – Typee Language Description
3.13 – Exceptions
Typee manages exceptions. Exceptions are faulty execution of code, such as division by 0 or out-of-bounds indexing, for instance. Typee uses a Python-like naming. C++ and Java programmers should nevertheless get no difficulty to understand and to use this concept.
Exceptions can be raised by code, and catched in caller functions for the processing of the detected errors.
Every exception inherit from base class Exception
. There are built-in exception in Typee and programmers can define their own exceptions as long as they all inherit from Exception
.
We first group here Typee syntax related to exceptions that has been already described in previous sections. We then list all Typee built-in exceptions. We finally provide some examples of catching exceptions, raising exceptions and creating exceptions.
3.13.1 Exceptions – related Typee syntax
3.13.1.1 try statement
The formal EBNF specification in Typee is this:
<try statement> ::= 'try' <statements block> <try except> <statements block> (<try except> <statements block>)* [<try otherwise> <statements block>] [<try finally> <statements block>] <try except> ::= 'except' '(' [ (<try except expr> | 'all' ) ] ')' <try except expr> ::= <expression> ['as' <identifier>] (',' <expression> ['as' <identifier>])* <try otherwise> ::= 'otherwise' <try finally> ::= 'finally'
The keyword try
tags a block of statements in which any raised exception will be caught. As soon as an exception is raised by some statement in this block, the execution of the statements in this block stops and the chain of except
clauses is checked against the type of the raised exception.
Each except
clause specify one or many types of exceptions for which their associated block of statements will be processed. The keyword as
names the raised exception which can then be addressed in the statements block after it.
If the type of the raised exception matches the type expressed in an except
clause, the statements block associated with this except
clause is immediately processed.
If none of the exceptions types expressed in the chain of except
clauses matches the raised exception, then the otherwise
clause is used and its associated block of statements is processed, if this clause is present.
If a finally
clause is present, the raised exception having been caught by a previous except
clause or not, the statements block associated with clause finally
is processed. This means that when a finally
clause is present, it is always processed.
Example:
try { for( float n in [5.0:-1.0:-1.0] ) print( 1.0 / n ); } except ZeroDivisionException { print( "Infinity" ); } otherwise { print( "otherwise clause" ); } finally{ print( "finally clause is always processed." ); } // prints: // 0.2 - 1.0 / 5.0 // 0.25 - 1.0 / 4.0 // 0.333333 - 1.0 / 3.0 // 0.5 - 1.0 / 2.0 // 1.0 - 1.0 / 1.0 // Infinity - 1.0 / 0.0 // finally clause is always processed.
3.13.1.2 raise statement
The formal EBNF specification of statement raise
in Typeeis:
<raise statement> ::= 'raise' <expression> ['from' <expression>]
Statement raise
is associated with exceptions. This statement raises exceptions at the discretion of programmers. If this statement is processed in an environment where exceptions are caught, the associated raised exception will be caught and processed accordingly. The expression next to keyword raise
should be the constructor of an Exception or an instance of an exception class. It may be followed by keyword from
and another exception expression. This secondary information may help producing more precise errors messages when raising exceptions. In Python, this is used to chain exceptions. This is its use also in Typee.
See example later in this section of Typee documentation.
3.13.2 Exceptions are classes
Class Exception
is the base class for all exceptions in Typee.
class Exception { Exception( const str text, const ... args ){ me.text = text; me.args = [arg for arg in args]; } str cast operator str(){ str my_txt = me.txt; for (arg in me.args){ try{ my_txt += ' ' + str( arg ); } except{ nop; } } return my_txt; } :protected: const str text; const list args; }
The above code is an illustration of base class Exception
with a quite naive programming. The cast operator str
casts the exception to a string that contains its associated text and as much optional arguments as possible, separated with spaces. This operator is automatically called by Typee anywhere a string would be expected while passing an instance of this class. This is the case, for instance, when printing an instance of an exception.
Example:
print( Exception("Exception raised", "line", 125, "- unknown cause.") ); // prints: Exception raised line 125 - unknown cause.
3.13.3 Built-in Exceptions
Typee built-in exceptions are many. We list all of them in their alphabetical order here. For the most of their use, you will intercept them in try-except clauses.
exception class | inheritance | comments |
---|---|---|
ArithmeticException |
Exception |
Base class for all other arithmetic errors |
AssertException |
Exception |
raised by assert |
DimensionException |
Exception |
trying to index a non existing dimension of a container |
EnsureException |
AssertException |
raised by ensure |
FileAccessException |
FileException |
can’t grant access to a specified file |
FileClosedException |
FileException |
trying to operate a closed file |
FileEOFException |
FileException |
end of file reached while reading data from file |
FileException |
Exception |
Generic file exceptions, base for all other file exceptions |
FileExistsException |
FileException |
trying to create an already existing file |
FileNotFoundException |
FileException |
trying to access a non existing file (most often: erroneous file path) |
FileNotOpenedException |
FileException |
trying to operate a not yet opened file |
FileOpenedException |
FileException |
trying to open an already opened file |
FilePermissionException |
FileException |
trying to operate a file with wrong permissions |
FilePosException |
FileException |
Erroneous cursor or index positionning in a file |
FileRWModeException |
FileException |
trying to write in a read-only file or trying to read in a write-only file |
FloatingPointException |
ArithmeticException |
erroneous final float value |
KeyNotFoundException |
Exception |
the specified key is not present in a map |
ModuleException |
Exception |
any import error: trouble when importing a module or not found identifier in imported module |
ModuleNotFoundException |
ModuleException |
imported module cannot be located |
NotFoundException |
Exception |
a searched item has not been found in a container |
NotMutableException |
Exception |
trying to modify an entity of constant value |
OpSysException |
Exception |
any Operating System error |
OutOfBoundsException |
Exception |
indexing a container (array, list or file) with an out of bounds index value |
OverflowException |
ArithmeticException |
too big value |
RequireException |
AssertException |
raised by require |
StringFormatException |
Exception |
erroneous format string |
SystemError |
Exception |
any other kind of exception (run-time error for instance) |
TypeException |
Exception |
wrong type or type misuse |
UnderflowException |
ArithmeticException |
too small value |
ValueException |
Exception |
a wrong or forbidden value has been used |
ZeroDivisionException |
ArithmeticException |
division by zero |
The watchful reader will eventually ask how could most of these exceptions be raised at run-time while we will just be translating a Typee program into any other programming language, and since those targeted languages may lack of such an exception mechanism or of such exceptions values?
Well, indeed this is a very good question.
We will completely address it when talking about Typee programs, but we can say right now that any Typee program, once translated into a targeted language, will get all the needed stuff around the main()
function (the one that is run as the entry point of the program for the final Operating System). This stuff will be something like an embedding try
statement which will intercept as much as possible of the above exceptions from the final OS and the targeted programming language and which will raise back the related Typee exception. This way, your Typee code will just have to intercept Typee Exception, whatever the targeted programming language and the final Operating System it will run on.
Of course, should some target platform (i.e. OS plus programming language) lack the exception mechanism or lack some Typee exceptions, those exceptions will not be intercepted on this targeted platform.
3.13.4 Creating your own Exceptions
This is just inheritance! Let’s illustrate it with an example. Caution, this example is a little big long but it proposes many Typee features and many OOP features that the reader should enjoy. As an exercice, you could try to translate it in your favorite programming language.
So, we want to define a dedicated class for a specific type of container: sparse matrices.
Sparse matrices are huge matrices with most of their entries set to null, or zero, or no-meaning value and a very few of their entries that contain value. Programming them as a 2D-array is a huge waste of space since most of their content is of no value. Moreover, it may be that their dimensions will be too huge for them to be placed as a whole in volatile memory.
An example of such matrices? Netflix notation of hundred of thousands video content by hundred of million of users. This is about 10,000 Giga-entries. No way to keep it in a 10 TBytes volatile memory. Meanwhile, this hundred of million of users may have noted a mean of 10 or 20 movies, which is about a few GBytes of notations.
A way to code a sparse matrix is to associate its two coordinates [raw][code]
as a key with the associated value. Let’s make such matrices inherit from this Typee built-in container. Let’s say that they will contain a single type for their content, as a template type ItemT
.
class SparseMatrix<ItemT> { SparseMatrix( const uint32 max_raws, const uint32 max_cols ){ me.raws, me.cols = max_raws, max_cols; } ItemT operator [] ( const uint32 raw, const uint32 col ){ return me.data [ [raw, map] ]; } :private: uint32 raws, cols; map<ItemT> data; }
This is a very good canvas for our programming, but it won’t work as is. Why? Let’s use it as is.
SparseMatrix my_mat = SparseMatrix( 100_000_000, 100_000 ); my_mat[0,0] = 5; // should raise an KeyNotFoundException!
Here, in operator[]
, me.data
is an empty map just after creation time. Trying to access item with key [0,0], to assign it a value, is an error since this this indexed item does not yet exist. So, we have to take care of this in operator []
.
class SparseMatrix<Itemt> { SparseMatrix( const uint32 max_raws, const uint32 max_cols ){ me.raws, me.cols = max_raws, max_cols; } ItemT operator [] ( const uint32 raw, const uint32 col ){ try { return me.data [ [raw, map] ]; } except KeyNotFoundException{ me.data [ [raw, map] ] = ItemT( 0 ); return me.data[ [raw, map] ]; } } :private: uint32 raws, cols; map<ItemT> data; }
We’ve added a try
statement into the definition of operator[]
. Now, when accessing a not yet created item in the sparse matrix, the raised KeyNotFoundException
is intercepted and processed this way:
- we first create the related entry with list
[raw, col]
as its key and we assign it with the null value of typeItemT
; - we then return a reference to this newly created entry.
This supposes that the template type ItemT
gets a way to provide null values either because it is an integer or floating point scalar type, or because it is a class with a “null” constructor.
Now, we can address any item in the sparse matrix even if it is absolutely empty:
SparseMatrix my_mat = SparseMatrix( 100_000_000, 100_000 ); print( my_mat[0,0], my_mat[99_999_999, 99_999] ); // prints: 0 0
Furthermore, we can now assign values to not yet existing items as well as modify values of already existing items.
SparseMatrix my_mat = SparseMatrix( 100_000_000, 100_000 ); my_mat[0,0] = 5; print( my_mat[0,0], my_mat[99_999_999, 99_999] ); // prints: 5 0 my_mat[0,0] = 3; print( my_mat[0,0], my_mat[99_999_999, 99_999] ); // prints: 3 0
Well, we are not fully done with this code. Now, we can get the “empty” or “null” value for items when asking for the value of not-initialized items. But we have programmed this in a way where the missing item is created into our internal structure, the map
with identifier data
. This was mandatory to be able to assign some value to the indexed item. We do need to create the item in the internal storage structure. But when just dealing with the value of an item, we do not want to create it in the sparse matrix if this item is null.
To obtain this effect, we have to define a const
operator. While returning a constant item, this operator will not be called when assigning a value to this item. Meanwhile, when only dealing with the value of the indexed item, in an expression for instance, or as a constant argument of a function, Typee will first check for the constant version of operator []
if defined.
Here it is:
class SparseMatrix<ItemT> { SparseMatrix( const uint32 max_raws, const uint32 max_cols ){ me.raws, me.cols = max_raws, max_cols; } itemT operator [] ( const uint32 raw, const uint32 col ){ try { return me.data [ [raw, map] ]; } except KeyNotFoundException{ me.data [ [raw, map] ] = ItemT( 0 ); return me.data[ [raw, map] ]; } } const ItemT operator [] ( const uint32 raw, const uint32 col ){ try { return me.data [ [raw, map] ]; } except KeyNotFoundException{ return ItemT( 0 ) ; } } :private: uint32 raws, cols; map<ItemT> data; }
Finally, just to validate the sparsing nature of SparseMatrix
, let’s add a new casting operation on it: operator cast str ()
. This way, we will be able to print the content of a sparse matrix with no effort.
class SparseMatrix<ItemT> { SparseMatrix( const uint32 max_raws, const uint32 max_cols ){ me.raws, me.cols = max_raws, max_cols; } ItemT operator [] ( const uint32 raw, const uint32 col ){ try { return me.data [ [raw, map] ]; } except KeyNotFoundException { me.data [ [raw, map] ] = ItemT( 0 ); return me.data[ [raw, map] ]; } } const ItemT operator [] ( const uint32 raw, const uint32 col ){ try { return me.data [ [raw, map] ]; } except KeyNotFoundException{ return ItemT( 0 ) ; } } str operator cast str() { str my_content = ""; for( key, value in me.data ) my_content += " " + str(key) + ":" + str(value); return my_content; } :private: uint32 raws, cols; map<ItemT> data; }
We can now easily print the matrix content.
SparseMatrix my_mat = SparseMatrix( 100_000_000, 100_000 ); my_mat[0,0] = 5; my_mat[99_999_999,99_999] = 3; print( my_mat ); // prints: [0, 0]:5 [99999999, 99999]:3
But wait. We were describing here how to create our own exceptions… So, what all the above stuff is useful for?
Here we are, where we can provide dedicated exceptions with sparse matrix when some error has been found. The simpler one is an out-of-bounds indexing. But there could be many other ones. We will group all of these under the same label SparseMatrixException
class OutOfBoundsSparseMatrixException : OutOfBoundsException { OutOfBoundsSparseMatrixException( const str raw_col_txt, const uint32 index, const uint32 max_index ){ OutOfBoundsException( "Out of bounds exception in sparse matrix,", raw_col_txt, index, "is not in bounds [ 0,", max_index, ")" ); } } class OutOfBoundsRawException : OutOfBoundsSparseMatrixException { OutOfBoundsRawException( const uint32 raw, const uint32 max_raw ){ OutOfBoundsSparseMatrixException( "raw", raw, max_raw ); } } class OutOfBoundsColException : OutOfBoundsSparseMatrixException { OutOfBoundsColException( const uint32 col, const uint32 max_col ){ OutOfBoundsSparseMatrixException( "column", col, col_raw ); } } class SparseMatrixException : Exception { SparseMatrixException(){ Exception( "SparseMatrix exception - unknown cause" ); } }
Now, we can intercept the related exceptions in our code and raise back our newly defined exceptions. Quite simple to do.
class SparseMatrix<ItemT> { SparseMatrix( const uint32 max_raws, const uint32 max_cols ){ me.raws, me.cols = max_raws, max_cols; } itemT operator [] ( const uint32 raw, const uint32 col ){ try { if( raw < 0 or raw >= me.raws ) raise OutOfBoundsRawException( raw, me.raws ); if( col < 0 or col >= me.cols ) raise OutOfBoundsColException( col, me.cols ); return me.data [ [raw, map] ]; } except KeyNotFoundException { me.data [ [raw, map] ] = ItemT( 0 ); return me.data[ [raw, map] ]; } otherwise { raise SparseMatrixException(); } } str operator cast str() { str my_content = ""; for( key, value in me.data ) my_content += " " + str(key) + ":" + str(value); return my_content; } :private: uint32 raws, cols; map<ItemT> data; }
Let’s now try it.
SparseMatrix<uint16> my_mat = SparseMatrix<uint16>( 100, 10 ); my_mat[ 10, 9 ] = 10009; print( my_mat ); // prints: [10, 9]: 10009 try{ print( my_mat[1, 1] ); // "const ItemT" signature called here } except Exception as e { print( e ); // not interecepted here but in operator code } // prints: 0 try{ print( my_mat[ -1, 10 ] ); // raw index is out of bounds } except Exception as e { print( e ); } // prints: // Out of bounds exception in sparse matrix, raw -1 is not in bounds [0, 100) try{ print( my_mat[ 10, 100 ] ); // col index is out of bounds } except Exception as e { print( e ); } // prints: // Out of bounds exception in sparse matrix, col 100 is not in bounds [0, 10) try{ print( my_mat[ 0, 0 ] = -1; // value -1 is not of type uint16, intercepted by 'otherwise' } except Exception as e { print( e ); } // prints: SparseMatrix exception - unknown cause
3.13.5 Conclusion
Next section formerly explains statement embed
which is so specific to Typee.