The following description applies to Skila-1 which development is stopped. Skila-3 is under active development now, with slightly different goals, when it is completed to be on par with Skila-1 this entire document will be updated.
The time for more elaborate introduction will come, but since the compiler is still in its early days, I hope comparison to other languages (mainly C#) will suffice.
The look
No braces
Ruby fans should feel like at home since Skila instead of braces encloses code in class-end
pair, namespace-end
, def-end
(functions and properties), if-then-end
, and so on.
while true do if elem.parent is null then return elem; end elem = elem.parent; end
Comments
In current incarnation one-liners are denoted with double %
symbol:
%% one line comment
This comment has the priority in Skila syntax. The other one is block comment (which can be nested):
%%rem %%rem %%end priority matters, this does not close the comment block %% %%end still within comment block %%end
Names
You cannot indent the nested code less than its parent and you have to keep the order in names — first upper case letter for namespaces and types, the lower case letter for functions and properties. Rules for variables, fields and parameters are a bit more relaxed — the leading characters can be underscores, after that there has to be either digit or lower case letter.
Namespaces
Skila like C# is case sensitive, but there are special rules for namespaces — for duplication namespaces are compared in case insensitive mode, meaning you cannot refer to namespace System
as SYSTEM
, but on the other hand you cannot define SYSTEM
as well, because it conflicts with System
. Moreover the name of the namespace has to be in sync with underlying directory, so you can put SYSTEM
in directory system
(comparison is case insensitive again) but not in testing
. Those ideas are taken from D.
Order of the elements
While IDE can help you finding given entity, keeping elements in order helps even more — you know for example that you will find all the fields in top of the type definition because compiler enforces this:
class Foo let field Int; def property Int; static def staticMethod() ••• def method() ••• let nope Int; %% error, incorrect order end
Names resolution
Skila uses first-found, bottom-up scheme, however entities appearing in namespaces (like variables, types, functions, and so on) reserve their name for current and descendant namespaces:
namespace DirtyPlayground let x Int = 5; def func(x Int) %% at this point "x" is reserved ••• end end
In other words, Skila does not allow namespace shadowing with the only exception of exposing nested entity via alias:
%% global namespace using void = Void.void; %% from now on "void" is reserved class Void %% ok, because the global entity points right here static public let void = Void(); ••• end
Object
Everything is an object, Skila does not have value types (struct
in C#), only reference ones. Similarly to C#, there is single root type Object
. It is possible to create instances of Object
but besides that it is pretty numb, because it does not have any methods:
let obj = Object(); obj.equals(obj); %% error, no such method obj.getHashCode(); %% error as well obj.toString(); %% nope
Wildcard
In similar fashion as in Scala, Skila supports wildcard _
. It is used for various purposes:
def ignore1() _Int ••• def ignore2() _ Int ••• def notUsed1(_ Int) ••• def notUsed2(_ x Int) ••• _ = dontCare(); let guess = MyClass<_>(3);
Expressions
Identity, equality
Unlike C#, Skila uses operator ==
for equality test, and is
operator for identity check.
let x = 5; let y = 5; let z = 6; assert(x==y); assert(x is not z);
Logical operators
Skila prefers English-like keywords instead of symbolic operators — thus we have not
, and
, or
, exor
. Special value null
is treated as false
in predicates and logical operators, for example true and null
gives false
, not null
as it would in three valued logic.
Reading expressions
All expressions in Skila have to be read, unless a function (which stands for such expression) allows to be ignored.
%% wildcard before the result typename def canBeIgnored() _Int ••• def readMe() Int ••• canBeIgnored(); %% OK readMe(); %% error
void
value
Like F# unit
value (of Unit
type), Skila has void
value (of Void
type). It means, it exists, it can be used, compared, wrapped, copied, passed, stored, and so on. It is no magical something that does not exists like in C-like languages.
Statements
Everything (or close to that) is expression in Skila — you can read actual value from a statement. Here as terminating expression:
var (yyyy,mm,dd) = * do let buffer = ![Int](); buffer!append(2016)!append(4)!append(25); end %% no semicolon at the end
The value of the entire block is taken from its last expression. When you put the do-end
block in parentheses it becomes regular expression like in this absurd example:
let z = (do 5; end) + 10;
In case of loops the outcome is a sequence of the values:
let squares = for elem in coll do elem*elem; end
Operators
Comma operator
Syntax is completely ad-hoc, however the operator works as regular comma operator known in C-family:
let take5 = 1 ;; 3 ;; 5;
Pre/post dec-/increment
At first glance everything is like in C:
++x y--
But the expansion shows the difference:
x = x.succ() t = y ;; y = y.pred() ;; t
It might sound surprising but those operators do not alter the original objects (as the above code shows).
Method cascading
If you don’t remember what is the difference between method chaining and method cascading — take a look at the Wikipedia. Example:
class !Container<T> %% please note "Void" is returned def !add(elem T) Void ••• end let coll = !Container(); coll |> !add(x) |> !add(y);
You could meet pipe operator in such functional languages like Elm (I took the symbols of the operator from it), F# or Elixir. As for the meaning — it is different from those languages — I reinvented the wheel here, only later I found out it was present in Smalltalk already (see: Smalltalk-80: The Language by Adele Goldberg, David Robson).
Assignments
Expressions
If you want to use assignment as an expression you have to use expression-assignment operator:
if x := y •••
Compositions
C# composition:
a += " world"; b = "hello " + b;
Such code is valid in Skila (the operator differs though):
%% double + for sequences a ++= " world"; b = "hello " ++ b;
The last line can be expressed in terms of the left hand side of the assignment:
b = "hello " ++ `lhs;
This is also stolen idea but I don’t remember where did I read about it.
Parallel assignments
Parallel assignments can be thought as transaction-like operation, meaning either everything will be assigned or nothing at all. Consider such example with optional parallel assignment:
if (year,month) ?= (y_str to ?Int , m_str to ?Int) then ••• end
The variables year
and month
will be updated only if both conversions succeed.
Static variables
Similarly to C++ Skila supports static variables:
def stateInside() static let cache [Int] = []; ••• end
Data alteration
Variables
In C# you have readonly
keyword, but for fields only:
readonly int x = 0; void foo(int y) { y = 1; readonly int z = 2; // error }
In Skila you have var
to indicate a variable or a field which you can reassign, and let
which is counterpart of readonly
. You can use it in parameters as well and then the latter is the default.
let x Int = 0; def foo(y Int) y = 1; %% error, cannot reassign let z Int = 2; end
Types
Skila borrows the concept of read-only and read-write data from C++ but instead of just decorating the given entity (type or method) with keyword const
, Skila makes such decoration a part of the name. And borrows exclamation mark from Ruby for that:
def usage() let reader = Reader(); reader.justRead(); let writer = !Writer(); writer!alter(); let ro_writer = Writer(); ro_writer!alter(); %% error end class Reader def justRead() ••• def !alter() ••• %% error end class !Writer def justRead() ••• def !alter() ••• end
Pretty much all the rules from C++ apply, you cannot alter read-only (understood as read-only view) data, you cannot alter the object in method marked with !
. And you cannot derive read-only type from read-write one, but of course the reverse is possible. The only exception is root type Object
, but there is no harm, it holds no data.
Rich loops
You have classic while-end
and loop-while
loops to choose from — with nice twist:
loop var still_visible = 0; ••• while still_visible>0;
The third loop is counterpart of C# foreach
, just with shorter name. All those loops can be labelled — not only this allows to command break
or continue
from the nested loop, but also allows to query loop in a limited way:
ctrl: for elem in [10,20,30] do if ctrl.isFirst then System.Terminal.stdOut.writeLine(elem," ",ctrl.index); end end
It is possible to pass an iterator — not a sequence — to the loop, as well as advancing the iterator on the fly. Those features allow to nest continuation loops in an easy way:
let coll = [1,2,3]; top: for x in coll do for y in top.iterator!next() do System.Terminal.stdOut.writeLine(x,",",y); end end
You will get (1,2), (1,3), (2,3). Dropping the initial iterator advancement (!next()
) will give you additionally pairs with equal elements — (1,1), (2,2) and (3,3).
Parameters
Positions
In C# once you pass a name with an argument you have to continue with names:
void foo(int x,int y) ••• foo(x:0,1); // error
In Skila — it depends. If the name matches the position it is OK to drop the name in the following arguments:
def foo(x Int,y Int) ••• foo(x:0,1); %% OK
Names
Skila fixes the problem of passing the cryptic true/false
values and many other issues with keyword parameters:
%% colon means the name is required def setFlag(value : Bool) ••• setFlag(true); %% error setFlag(value: true); %% OK
Position is no longer relevant, only the name is (think of it as a Dictionary
of parameters in C#). Thanks to keyword parameters Skila is able to handle such crazy signatures as:
def defaultFirst(x Int = 0, y : Int) •••
Since you have to give the name for y
it is safe to place that parameter wherever it fits best the logic of this part of the code.
Optional parameters
The usage of the optional parameter is the same as in C#, the implementation detail that might make you happy is that while in C# the default value is moved outside the function, in Skila it is sucked inside. This is legal in Skila:
def getter() Int ••• def optional(value Int = getter()) •••
Variadic parameters
Skila supports variadic parameters, but please note variadic does not mean optional, in other words it does not have implicit default value. Let’s take a look at C# first:
void taker(params int[] args) ••• taker(); taker(a,b,c); taker(int_array);
And Skila approach:
def taker(args Int ...) ••• taker(); %% error taker(a,b,c); taker(*int_array); %% unpack the array
If you would like to call the function without any argument, set the default value for it:
def taker(args Int ... = []) ••• taker(); %% OK now
You can set the desired limits as well:
def taker(args Int 2...10) ••• taker(3); %% error in compile time taker(*[7]); %% error in run time
Inside this function the difference is minor — C# treats variadic parameter as an array, Skila as a sequence.
Recurrence
Recursive call has to be written explicitly:
def recursive() Int let x = recursive(); %% error let y = func(); %% OK return x+y; end
func
stands for “call the current function again”, but in case of derivation chain can be also used to express the notion of “call the current function in base type”.
class Foo refines Bar refines def cont() base.func(); ••• end end
Option
Skila has null
values but only for the types that are marked as Option
(i.e. the ones accepting null
). The closest language is probably Swift here:
let a String = null; %% error let b ?String = null;
The rest was inspired by Icon:
if c ?= b then %% optional assignment ... end %% optional execution _ = opt_call(b); _ = b.callOrFail();
Unlike C# calling a method on null
(or passing null
as an argument) does not lead to NullException
, the code simply fails to execute. Yet unlike Icon the failure does not go silently — you either have to read the outcome or say you ignore it (as in example above).
This part of Skila — optional processing — is experimental feature, I would like to see if this is usable and how far I can go with it.
Aliases
A variable is too far to reach? Use an alias:
using short = Somewhere.Type.long; if short •••
You can create anonymous alias:
using X = A.B.X; %% named alias using A.B.X; %% anonymous equivalent
Please note the latter form is similar to C# but has different meaning — in C# it would allow you to grab anything inside A.B.X
without writing prefix A.B.X
. In Skila it allows you to use just X
without prefix A.B
. To get the same effect as in C# you write:
using A.B.X._; %% wildcard alias
All aliases are put as other entities inside some scope — namespace, type, or a function. If there is no visibility specified, alias is visibile within given file at maximum. Except for wildcard alias you can change it by applying appropriate visibility modifier.
Modifiers
There are abstract
, base
, extension
, final
, implicit
, inline
, partial
, pinned
, private
, protected
, public
, refines
, static
, test
.
base
as modifier is opposite of final
(sealed
in C#). partial
is used not only for types, but for constructors as well to indicate that given constructor is used only as initializer, so it cannot be called to create new object. refines
is used to mark derivation (counterpart of Java both implements
and extends
) but also in place of override
in C++-like languages.
pinned
is similar to abstract
on steroids — use this modifier once, and you will have to reimplement given method in every derived type.
test
allows given function to reach any data, no matter how private it is, however test functions can be called only by other test functions. You can use assert
in any function, but in test functions it is automatically translated into power-assert (idea taken from Groovy).
inline
forces calls to given function to be replaced with function body. Only standalone function can be inlined — such function has to be in form of single expression, its parameters can be used once at most, return
is forbidden as well as declaring static data.
this
Object reference
When given function has any parameters or local variables it is required to use this
reference for all members:
class !Foo var field Int; def !foo(x Int) field = x; %% error this.field = x; %% OK end end
Type reference
You can use this
in static methods — the meaning changes to “this view of type”. Why “view”? Because if we are in read-only method we have read-only view as well, so we cannot alter the type members:
class !Foo static def !changer() ••• static def justReading() this!changer(); %% error end end
Current type
Compile time — It
There is an alias for current type (in compile time sense) — It
.
Run time — Self
For “current” in run time sense there is (experimental) Self
type. It is to type, what this
is to object. Classic example is ICloneable
in C# — once you implement it, and then derive from that implementation you are stuck with given type. In Java it is less of the problem because the outcome of the method can be covariant (true as well in Skila). But why bother with constant updating the names:
base class Foo pinned def clone() `Self ••• end
Self
means read-only view of current type, !Self
means current type (only for mutable types), and `Self
means current type (valid for all types).
Pinned methods
When you put Self
as an outcome of the method it has to be pinned explicitly and cannot be unpinned later (because doing so would violate the contract of the original method).
Self
as parameter makes the method implicitly pinned and can be unpinned later by altering the contract to even more demanding — all it takes is replacing given parameter type by current or less derived type name — Object
at extreme.
Type inference
When you declare the variable and the type of the init expression is clear you can drop the type from the declaration:
let x Int = 1; let y = 2;
In case of fields you can drop type only if the constructor is explicitly used:
class Foo let x Int = 1; let y = Int(2); let z = 3; %% error end
enum
You can think of enum
as a container of immutable singletons:
enum DayOfWeek case sunday; case monday; case tuesday; case wednesday, %% syntax shortcut thursday, friday, saturday; end
Each enum
object has ordinal
property, there are copy
and replica
constructors automatically added, as well as ==
and !=
operators. If you insist on working with numeric equivalent of the enum
entries, you can call appropriate constructor to get required value:
let wrong_day = DayOfWeek(ord:7);
Also entire enum
type has meta values
property which returns a sequence of enum
entries, so you can write:
var day = DayOfWeek`values.first(); %% the outcome is "sunday"
You can add System.Enumerable
as a parent and Skila will add System.Comparable
as parent as well, with implementation of pred
, succ
and compare
methods. With those we can call:
_ = DayOfWeek.monday < DayOfWeek.friday; %% true ++day; %% the outcome will be "monday"
As for the last line if we instead wrote:
--day; %% the outcome will be… crash
That is because sunday
does not have predecessor (so it gives us null
value) and we try to fit it back into enum
.
Common types
Collections
You can use proper typename to indicate desired type, like:
let my_array = Array<Int>();
But Skila has some handy shortcuts:
~Int %% Sequence of Ints, equivalent of IEnumerable<Int> [ String ] %% Array of Strings ( Int, String ) %% Tuple ( x:Int, y:Int ) %% named Tuple { Int : String } %% Dictionary
And you can use the same shortcuts for objects creation (except for Sequence, because it is an interface):
[ "hello", "world" ] ( 2, "hi" ) let t = ( x: 2, y: 5 ) assert(t._0==2); %% regular access assert(t.x==2); %% via given name { 4 : "entry" }
Tuples
Tuple
is a true member of Collections
family:
let triple = ( 3, 7, 8 ); %% reading elements via indexer let first = triple[0]; %% iterating over tuple for elem in triple •••
Text
The !String
type in Skila is mutable. By default you use double quote character to build interpolated string literal:
"hello" %% immutable string !"hello" %% mutable string "\x0a" %% as code "\u2621" %% wide code, ☡ "x is ${x}" %% embedded expression
Character literal is written like in C#:
'e' %% same as above, but just single characters '\x0a' '\u2621'
Syntax for regex literals is borrowed from Perl:
/^hi$/
Quotations
This is taken from Ruby (the symbols are changed though) — instead of hardcoded quotation character in advance you define it on fly. Please note parenthesis-like symbols are matched against their closing counterparts, and also like in Ruby they are counted. First go the quotation operator (%
), then one of the symbols crRsS
(they cannot be any more mnemonic than that so the only thing to remember is upper case goes for verbatim) and finally the quotation character of your choice:
%c(a) %% interpolated character %r|^world| %% interpolated string %R/^hello$/ %% verbatim regex %s{{program}} %% interpolated string %S<<html>> %% verbatim string
Interpolated character does not allow to embed expressions via ${expression}
syntax. Unlike C# you still use backslashed quote within verbatim quotation:
@"C# verbatim "" quote and \ backslash" %S"Skila verbatim \" quote and \\ backslash"
Operators
Skila uses a bit different operators for !String
(inspiration came from Haskell):
"hello" ++ " " ++ "world" "hi" ** 3
Binary operator ++
is C# Concat
and works for all Sequences
, unlike C# it can concatenate elements of different types computing the common one on-fly.
Explicit inheritance
Skila here is in opposition to C# — by default almost everything is sealed
(final
in Java), so you have to mark anything you would like to “connect” in inheritance chain with base
modifier. If the entity by its nature is open for derivation (like interface
) you just leave it:
interface ImplementMe def write(); end class SealedClass end base NotLast refines ImplementMe refines def write() ••• base def notSealed() ••• end
Creating objects
Constructors
The counterpart of instance constructor in C# is init
constructor — the name is always the same, no matter what is the name of the type:
class Point let x Int; let y Int; def init() x = 0; y = 0; end end
We have also type initializer just as in C#:
class Foo %% this will be moved as part of type initializer static let myField Int = 3; static let other Int; %% it has to be "private" static private def init() this.other = 2; end end
Whenever there is a call for object creation — Foo()
— there is new
constructor called. Its generated code looks always the same:
static def () `Self let __this = `Self.alloc(); %% allocating memory __this.init(); return __this; end
new
constructor has to be static, it has to return Self/!Self
type, it cannot have name, it cannot be generic. Its body is completely repetitive and the compiler synthesizes it for every non-partial init
constructor there is.
We can of course provide our version of new
constructor — for example for all truly immutable types like Int
we can write such copy constructor:
static def (copy Self) Self return copy; end
Instead of init
constructor we defined new
constructor which returns the original object. Note that compiler can synthesize new
constructor for init
, but not vice-versa.
Skila also supports companion constructor (anonymous factory), similar to apply
method in companion objects in Scala:
interface Tuple new static def <T0,T1,out C>(_0 T0,_1 T1) Tuple<T0,T1,C> where C base T0; C base T1; return (_0,_1); end end %% usage: Tuple(3,7)
You can place companion constructor in any type, the result type does not have to be Self
, the method can be generic. Anonymous factory method has to be marked with new
modifier to be treated as new
constructor, but without its restrictions.
Object initialization
Consider a constructor:
class !Point def x Int get set; %% properties def y Int get set; def init(x: Int,y: Int) this.x = x; this.y = y; end end
Writing such code is a mundane task, C# provides additional mechanism for object initialization. Skila on the other hand simply automatize this task, there is no extra mechanism:
class !Point in: def x Int get set; in: def y Int get set; end _ = !Point(x:0,y:1); _ = !Point(0,1); %% error, missing names
Both properties are marked with keyword in
indicating they should be initialized via constructor, so compiler builds exactly the same code as before according to your instructions. If you prefer regular parameters, drop the colon:
class !Point %% no colon after "in" in def x Int get set; in def y Int get set; end %% both calls are valid _ = !Point(x:0,y:1); _ = !Point(0,1);
Properties
class !Any def a Int; %% getter and init-setter def ai Int = Int(0); %% as above, with initialization def b Int get; %% getter-only def bi Int = Int(1) get; %% as above, with initialization def c Int get => a; %% proxy-getter def d Int get set; %% getter and setter def di Int = Int(2) get set; %% as above, with initialization def e Int %% manual property get: return a; set: this.a = value; end def ei Int = Int(3) %% as above, with initialization get: return a; set: this.a = value; end end
With setter you can set the property anywhere (it is a counterpart of var
), with init-setter you can set it in constructors (it is counterpart of let
), without any setter the property is just for reading.
A property creates its own scope, so it can keep its own, private, data:
class !Any def keeper Int let x Int; get: ••• end end
The field x
is internal data of keeper
, nothing else can reach it.
Refining the methods
In Skila a property can refine the base method:
interface Sequence<out T> base def count() Int ••• end class !Array<T> refines Sequence<T> refines def count Int ••• end
The method counterpart of the indexer getter is at
.
Static fields
Static fields are accessed 100% directly (without any wrappers), they are initialized eagerly and they allow only basic types and basic init values. If you need complex type or complex init value use static property instead — properties are initialized lazily (on demand) and initialization is guarded, i.e. in case of circular initialization the program will crash.
Template constraints
Probably the most powerful enhancement is the ability to specify that given type should be an ancestor of another:
def common<T1,T2,C>(t1 T1,t2 T2) C where C base T1; C base T2; ••• end
Skila is capable not only of checking such constraint, but computing the correct type as well:
let c = common<Int,Int,_>(0,1);
The third template type parameter would be an Int
here.
Rich interfaces
As in Java, an interface can have method definitions, not only declarations, which is very handy:
interface Equatable def ==(cmp Self) Bool; def !=(cmp Self) Bool => not (this==cmp); end
You have to implement the former, but you cannot even refine the latter (“not equal” cannot change its meaning to something else than “the opposition to being equal”).
Methods
Twin methods
Each time you write a method meaning “modify itself” (for example: !sort
, !reverse
, !remove
, and so on) you don’t have to write its immutable counterpart manually. Skila will recognize it and add it for you, like here:
%% your code def !reverse() ••• end %% end of your code %% synthesized code added by Skila, do not write it def reverse() `Self let clone = this.copy(); clone!reverse(); return clone; end %% end of added code
On what conditions the synthesis occurs — your type has to be descendant of Copyable
or Replicable
interface (in order to perform cloning), the method you write cannot return anything, your method cannot be marked as static
or partial
. And if you write the latter method by hand, Skila won’t get into your way — in such case the synthesis will also be skipped.
Refinements
Virtual dispatch
Unlike C# refining method does not mean automatically the method becomes virtual. In Skila refining method (creating for example base-refines
chain) indicates the logic, not underlying dispatch mechanism. One obvious case is constructor chain — you can refine base constructor in derived types, yet constructors are not called virtually (and while we are at it — it is forbidden to use any method in constructor that leaks “virtual”).
As for virtual dispatch — all non-static, non-constructor methods when refined behave like C# virtual ones.
Signatures
The output type in Skila is covariant (you can use descendant type, as in Java), the input types are contravariant (you can use ancestor type) and you can add new optional parameters (as in PHP):
base class Start base def some(a String) Object ••• end class Next refines Start refines def some(a Object,x Int = 0) Int ••• end
Extensions
The idea of the external extensions comes from C#, the syntax (extension
keyword and grouping) from Swift.
If you place any extension at the same file the type is defined you have full access to all private members of it.
External extensions
Those are pure syntactic sugar, there is nothing you couldn’t write by yourself using non-extension part of the language.
Micro extensions
Consider scenario when you have collection of collection of some elements, and you would like to write method flatten
. You can add such method as extension of Sequence
type:
extension def flatten<E>(this ~~E) ~E ••• end
Three things to notice:
- the function is placed directly in (any) namespace,
- the modifier is
extension
, - there is explicit
this
parameter.
Such extension can be used as a function or as a method.
Extension types
Here we group the methods and properties into extension type:
extension class Real def square Real get => this*this; def cube Real get => square*this; end
We didn’t change the source of the type. We just added new properties on the side:
_ = 4.5.square; _ = 1.7.cube;
Please note in case of extension types, we don’t add explicitly this
parameter and we cannot call methods as standalone functions.
Internal extensions
You can think of them as specializations but unlike C++ template specializations you cannot provide customized version of a given method present in a template. You can only add new methods (or properties) which do not exist in a template being extended.
As an example consider extending Sequence
to be Equatable
:
extension interface Sequence<T> refines Equatable where T refines Equatable; refines def ==(cmp ~T) Bool ••• end end
What does it mean? A general Sequence
type does not inherit from Equatable
interface (because it cannot), but once we drop something that is Equatable
inside Sequence
type, we get type specialization which does inherit from Equatable
.
There is more — a type which inherits from Sequence
(like !Array
) also behaves in the same way, its general version is not Equatable
, but if you have let’s say array of numbers (which are Equatable
) entire array becomes Equatable
— think of it as one specialization inherits from another specialization.
Specializations can be combined — imaginary example, if we wrote specialization for Sequence
to inherit from X
when elements inherit from X
, and in same manner we did with type Y
what happens if we have Sequence
of elements which derive from X
and Y
at the same time? We will get Sequence
type inheriting from X
and Y
.
You have to place internal extensions in the same file as type being extended. Also you cannot extend non-template type (because there would be no way to discriminate such extension).
Non-Virtual Interface pattern
As D, Skila supports NVI pattern in full:
interface Transmogrifier private def transmogrify(); private def untransmogrify(); def thereAndBack() transmogrify(); untransmogrify(); end end class CardboardBox refines Transmogrifier refines private def transmogrify() ••• refines private def untransmogrify() ••• def foo() transmogrify(); %% error end end
We have to implement those two methods, but we cannot access them from CardboardBox
, because originally they were declared as private
.
The code (with modifications) comes from “The D Programming Language” by Andrei Alexandrescu.
Errors
throw/try/catch
Currently Skila has only rudimentary tooling:
try throw !Exception("Something bad."); catch ex do do_something = ex.message; end
Handling errors
After implementing the features described below I found out Swift uses already very similar approach of handling the errors.
Current Skila approach is derived from C# while focusing on removing Do/TryDo
pattern (like Parse
and TryParse
methods in C# Int32
).
Errors as exceptions
You access an array with index out of its boundary. You divide by zero. In cases like those you throw an exception to indicate the error in computation.
Errors as values
Your common sense, practice, intuition, tells you that for some computations throwing an exception would be too harsh, so instead you return magic value instead to indicate “not a value” value. Probably the best example is String.indexOf
— for such purpose Skila has type Option<T>
and special value null
(it is not null
you know from C# — it is like Scala none
or null
in Swift or Kotlin).
Errors conversion
In Skila you can convert one mode (error as exception) to the other one (error as value). Consider the code:
let dict = {String:Int}(); let val ?Int = dict["hi"] $;
Let’s focus on the second line — when we access the dictionary (it is empty) the exception is thrown from the indexer getter. That exception in Skila is special — it is nearby exception (because it is thrown in nearby, or next if you like, function we just called). Then there is dollar — $
— character which works as an exception trap. Only nearby exceptions can be trapped (the others go through) — and if this happens, they are converted to null
value.
You don’t have to write duplicated code, you simply convert errors as you like — go from null
to exception, or from exception to null
. Both conversions are expressions.
Making nearby exceptions special has a purpose — to distinguish between three outcomes in total:
- we looked and we found the key,
- we looked and we didn’t find the key (i.e. we are sure the key is not there),
- we crashed while looking (because, say, our comparison function is buggy), so we cannot tell if the key exists or not.
Exception chaining
In the same manner as you trap nearby exception you can chain it as well while providing more information — compare:
dictionary!add(key,value); dictionary!add(key,value) && "File ${filename}, line ${line}.";
When the key already exists we will get exception with a message and stack trace (the first line). Still true for the second line, but this time a new exception will be created on fly with added information about the context.
Passing exception
Probably nobody will ever need it, but for the record — there has to be a way to close the distance of the exception, otherwise assert
and fail
would be useless:
def [key K] V set: ••• fail("Key not found."); end
Your code calls dictionary indexer, dictionary indexer calls fail
, fail
calls assert
, assert
throws an exception. So for sure such far placed throw
will not be considered as “nearby” from your code perspective. Unless we tamper with the exception distance counter:
def assert(•••) Void ••• throw Exception(•••) && throw; end def fail(•••) Void assert(•••) && throw; end
This part of code && throw
tells compiler to pass an exception further. Effectively when you write assert
or fail
the exception which is thrown thinks it originated from where those functions were called.