Language

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:

  1. the function is placed directly in (any) namespace,
  2. the modifier is extension,
  3. 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.

Advertisement
%d bloggers like this: