Experimentation Stage

Lense is a strong typed, pure object oriented programming language that aims to be a familiar simple languages that could compile to any object oriented platform. First focusing on the Java Virtual Machine (JVM) and then on other possible platforms like Javascript and CRL (.NET).

Features

  • ✓ Familiar Syntax - from the C family similar to Java, C# and TypeScript

  • ✓ Object Oriented - every variable holds an object, no nulls. No primitives, every object is an instance of a Type (classes or interfaces)

  • ✓ Modular - the compiler produces modules with dependency and version information on other modules.

  • ✓ Strong Static Typing with Inference - all objects have a type at compile time, and the compiler can infer most types

  • ✓ Single Inheritance for classes , multiple inheritance for interface

  • ✓ Support for External Extension - you can use Enhancements to add methods to types you don’t control.

  • ✓ Immutability first - you have to opt out of immutability explicitly

  • ✓ Type Classes - you can define constracts for types, not only for instances

  • ✓ Support for Sealed Type Hierarchies - you can limit the instances of a type and/or the types that inherit from it.

  • ✓ Operator Overloading with no noise - Operators are related to type interfaces and follow mathematical concepts like Magma, Monoid and Group

  • ✓ Flow Sensitive Typing - casts are not necessary when the compiler knows a variable is already of a given type.

  • ✓ Math and Science friendly - support for Natural, Integer , Rational, Imaginary and Complex numbers. Support for the power operator (^^) and units (2 kg)

  • ✓ Reflection and Reification - make your own generics types, control variance and query then via reflection thank to reification support.

  • ✓ Support for Monads , Lambdas and higher order functions

  • ❏ Native code inter-op - Call you favorite platform native library (under consideration)

Familiar Syntax

The syntax is based in the C family of syntaxes ( methods delimited with { and } ) but moves the type declaration to the end like TypeScript or Swift. This is because types can be inferred when not declared and omitting the type is simpler than using some afterthought keyword.

public class Main extends ConsoleApplication {

	public override onStart(){

		let arguments : Sequence<String> = this.arguments;

		let name = arguments.first().orElse("somebody");

		console.println("Hello, {{ name }}")
	}
}

Object Oriented

All data and all methods exist in an Object. All variables are treated as holding objects. All objects are instances of a type. All methods and properties belong to a type. Some fundamental types receive special handling from the compiler in the form of literals or specific operators (see Fundamental Types).

At compile time, the platform-specific back-end assembler is free to optimize types to platform’s primitives (see erasure).

No Nulls

Because everything is an object, all variables must have been initialized at some point and cannot hold null. The concept of "absent value" is supported by introducing the Maybe monad (see nullability).

No Static scope and Support for singleton objects

All things are objects or exist within an object, including methods and properties.On the other hand, Lense, supports singleton object definition.

// define
public object ServiceRegistry  {

	private services = new Map<String, Any>();

	public register ( name: String, service:  Any){
		services.add(name, service);
	}
}

// then you can write
ServiceRegistry.register("nameService", new NameService());

Strong Static Typing with Inference

You can explicitly declare the type of you variables:

let n : Natural = 4; // defines a Natural constant of value 4
let r : Rational = 5.6; // defines a Rational variable initialized to 5.6

Or you can let the compiler infer them by omitting the types. You can simply write:

let n = 4; // defines a Natural constant of value 4
let r = 5.6; // defines a Rational variable initialized to 5.6

Expressions can be easy inserted within a string using a simple syntax (see string).

let age = 34;
let name = "John";

console.print("{{ name }} is {{ age }} year old");

Prints:

John is 34 years old

Single Inheritance for classes , multiple inheritance for interface

This the is same inheritance model present in other Object Oriented languages like Java, C# or TypeScript.

public class SomeClass
	extends ParentClass
	implements InterfaceA, InterfaceB
{
	...
}

Support for External Extensions with Enhancements

Enhancements allow for extension of types you do not control. Extending types makes it possible to add methods and/or calculated properties, like adding a repeat to String

// define
public enhancement StringRepetition extends String {

	public String repeat(n : Natural){
		mutable let repetition = this;

		for ( i in 0..n ){
			repetition = repetition ++ repetition;
		}

		return repetition;
	}
}

// use like

let santaHello = "ho".repeat(3);
assert ( "hohoho" == santaHello);

After defining the enhancement the repeat method is available to call on String even though it is not defined in the String class. This functionality is closely related to Extension Methods in C#, Gosu or Kotlin but without recurring to the concept of static.

Immutability first

Lense is designed with immutability in mind so types are immutable by default. You must opt in for mutability.

In Local Variables

The declaration of constants is preferred and is the default

let n  = 4; // defines an imutable variable of value 4

n = 3; // compilation error. constants are immutable

To produce a mutable variable you must declare it explicitly

mutable let r  = 5.6; // defines a mutable variable initialized with value 5.6

r = 4.2; // ok, variables can change value.

In Types

By default a classe is immutable.

public class Fraction {

	public constructor(
		private numerator : Integer;
		private denominator : Integer;
	){}

	public multiply (other : Fraction) : Fraction {
		return new Fraction(
			this.numerator * other.denominator,
			other.numerator * this.denominator,
		);
	}

	public invert(){
		// try to invert values in place
		let numerator = this.numerator;
		this.numerator = this.denominator; // compilation error
		this.denominator = this.numerator; // compilation error
	}
}

A better design is to return a new object that is the result of the operation.

public class Fraction {

	public constructor(
		private numerator : Integer;
		private denominator : Integer;
	){}

	public multiply (other : Fraction) : Fraction {
		return new Fraction(
			this.numerator * other.denominator,
			other.numerator * this.denominator,
		);
	}

	public invert(){
		return new Fraction(this.denominator,this.numerator);
	}
}

But some time we need simple property bags

public class Client {

	public constructor(
		public name : String;
		public country : String;
	){}

}

let john = new Client("John", "France");

// john moved to Italy

john.country = "Italy"; // compilation error

Since the default is immutability it is not possible to modify the country. We need to opt in for mutability:

public mutable class Client {

	public constructor(
		public name : String;
		public mutable country : String;
	);

}

let john = new Client("John", "France");

// john moved to Italy

john.country = "Italy"; // ok

Note that making a property mutable also required to mark the class as mutable.

Support for Sealed Algebraic Type Hierarchies

With Sealed Algebraic Type Hierarchies you can define a type Hierarchy that cannot be extended out side of you code. First you define your types using the is - case syntax. Use is to enumerate the hierarchy types and case to mark a type belongs to the hierarchy.

public abstract class Node is Branch , Leaf {

}

public case class Branch extends Node {
	...
}

public case class Leaf extends Node {
	...
}

Then, is some on else tries to create another type in the hierarchy, an error is raised:

public case class Other extends Node { // compilation error
	...
}

Algebraic types can be used together with switch , like so :

public void gatherElements (node : Node, list : List<Object>){
	switch (node){
		case is Branch {
			// recursive call
			gatherElements (node.left , list);
			gatherElements (node.right , list);
		}
		case is Leaf {
			list.add(node.element);
		}
	}
}

Due to sealed algebraic types hierarchies the compiler knows no other options exist, so it does not complains about a missing default clause. Cast is not necessary due to flow sensitive typing.

Flow Sensitive Typing

Casting is reduced with the introduction of flow sensitive typing:

public class SomeClass {

	private innerValue: Natural;

	public equalsTo(other: Any) : Boolean {
		return other is SomeClass && other.innerValue == this.innerValue;
	}

	// other methods omitted
}

Notice how the cast when accessing other.innerValue is not needed. The program just checked other is of the correct type, so the compiler includes the cast implicitly.

Operator Overloading with no noise

Operators symbols are predefined and associated with specific interfaces so classes like numbers and strings can use operators.However defining you own operator symbol is not allowed in order to maintain the code simple to read and avoid symbolic noise. The use of interfaces to define operations follows an algebraic structure paradigm so the compiler can reason about the operations (example : altering the order of operations to enhance performance if the operation is commutative)

Support to Rational, Imaginary and Complex numbers. It is important for Lense that all these numeric types are supported even if the performance is not optimal. Peformance is a problem for the runtime , not the language. In Lense expression of intention is more important that performance.

let n : Natural = 3; // numbers are naturals by default. naturals are non negative
let d : Integer = -2; // integer holds negative whole numbers

let r : Rational = -1.5; // decimal literals are rational numbers by default.

if (r == n/d ){
	// this will be true, because whole division always produces a Rational
	// and numbers are compared by value independently of type.
}

let img : Imaginary = 4i;
let complex = n + img;

if (complex == 3 + 4i){
	// this will be true because n==3 and img ==4i
	// and numbers are compared by value independently of type.
}

// you can use the power operator even to take roots
let distance = (x^^2 + y^^2) ^^ 1/2;

Lense also supports Intervals and Ranges.

for ( x in 3..7 ){ // iterate a range
	// iterate from 3 to 7
}

if ( x in |[ 3 , 7)| ){
	// test if x is >= 3 and < 7
}

Math and Science friendly

Lense enables handling all major algebraic structures like Naturals, Integers, Rational , Imaginary and Complex numbers while also supporting IEEE Float numbers. Operations are defined using concepts like Magma, Groups and Ring.

Lense also enables juxtaposition that enables writing things like:

let a : Complex = 3 - 4i;
let b : Complex = 3 + 4i;

assert ( 5 == (a * b) ^^ 0.5 );

assert ( -1 == i ^^ 2);

let q : Quaternion = 2k + 3j + 5i + 8;

assert ( new Quaternion(8,5,3,2) == q);

assert ( -1 == j ^^ 2);
assert ( -1 == k ^^ 2);

let time = 2s; // you can write with no spaces
let distance = 2 km; // or with spaces

let velocity = distance / time;

assert(  1 km/s  == velocity );
assert( velocity is Measure);

Modular

Lense is Modular. The compiler merges code and meta information into a "module bundle" (think .jar or .dll) with information about their respective dependencies. This allows for the runtime to determine the modules that are needed for a given module to run.

module my.application 1.2.0 {

	require other.some.library 1.0.0;
	require other.some.other.library 1.3.5;

	export my.application.api;
	export my.application.api.data;
}

Reflection and Reification

Generics are reified and the type information of the generic type parameters can be inspected at runtime. This is really works well with constructors that can control the correct instance to return:

public class Bag<T> {

	constructor (){
		if (T is Boolean){
			return new BooleanBag(); // optimized bag for booleans
		}
		return new ObjectBag<T>();
	}
}

class BooleanBag extends Bag<Boolean>{
	...
}

class ObjectBag<T> extends Bag<T>{
	...
}

Types can have generic parameters and these parameters can declare their intended variance on site.

public interface Sequence<out T> { ... }

public interface Validator<in T> { ... }

Support for lambdas and higher order functions

Lense supports functional programming with lambdas:

let first100EvenNumbers = 0..100.map( i => i*2);

Native code inter-op (under consideration)

Lense can interact with the platform native language like Java or Javascript.

import native(java) java.lang.System;
import native(java) java.time.Duration;

public class StopWatch {

	private mutable mark : Int64 = 0;

	public start(){
		// invoke java and automatically convert long to Int64
		mark = System.currentTimeMillis​();
	}

	public stop() : Duration{
		// invoke java
		return Duration.ofMillis(System.currentTimeMillis​() - mark);
	}
}

Innovative

Constructors act like factory methods. A class is a factory and constructors really construct the object (not only initialize it). All calls to create new objects are calls to factory methods present in an object thus enforcing the static factory method design pattern out-of the box.

Meta classes allow to program methods that apply to classes instead of instances, like operators.

Enhancements allow you to add methods in classes that originally did not support them.