public class Main extends ConsoleApplication {
public override onStart(){
let arguments : Sequence<String> = this.arguments;
let name = arguments.first().orElse("somebody");
console.println("Hello, {{ name }}")
}
}
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).
✓ 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)
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 }}")
}
}
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).
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).
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());
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
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
{
...
}
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
.
Lense is designed with immutability in mind so types are immutable by default. You must opt in for mutability.
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.
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.
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.
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.
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
}
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);
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;
}
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> { ... }
Lense supports functional programming with lambdas:
let first100EvenNumbers = 0..100.map( i => i*2);
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);
}
}
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.