In this article we continue to examine the differences between Concurnas and Java.
This article carries on from the first article (part 1) in the series. There is one more article (part 3) next.
More and more developers are starting to realize the expressive power of utilizing aspects of functional programming. Whilst pure functional programming languages may not always be the best fit for all problems, most problems can benefit from some aspects of functional programming. Concurnas presents some of the most commonly used aspects of functional programming. More recent versions of Java have also started to embrace functional programming.
Both Java and Concurnas have support for method references:
interface Operation{//som boilerplate code to assist
int doOp(int a, int b);
}
public class InstanceMethodReference{
public static int plus(int a, int b){
return a + b;
}
}
public static void main() {
Operation op = InstanceMethodReference::plus;
int result = op.doOp(12, 1);
}
The support for method references exposed within Concurnas is much simpler and requires no boilerplate code:
def plus(a int, b int) => a + b
op = plus& //op is of type (int, int) int
result = op(12, 1)
Java provides support for partial functions via code such as the following:
public static int add(int a, int b) {
return a + b;
}
public static Function<Integer, Function<Integer, Integer>> add() {
return new Function<Integer, Function<Integer, Integer>>() {
@Override
public Function<Integer, Integer> apply(final Integer x) {
return new Function<Integer, Integer>() {
@Override
public Integer apply(Integer y) {
return x + y;
}
};
}
};
}
Function<Integer, Function<Integer, Integer>> l1call = add();
Function<Integer, Integer> l2call = l1call.apply(12);//partial function
Integer result = l2call.apply(1);
Partial functions are a much simpler affair in Concurnas:
def plus(a int, b int) => a + b
op = plus&(12, int)//partial function of type: (int) int
result = op(1)
Currying is also doable through code such as the following:
def partialPlus(a int){//returns type (int) int
def plus(b int) => a + b
plus&
}
op (int) int = partialPlus(12)//curried function
result = op(1)
Concurnas was designed from the ground up to support function, method and constructor references. As such it is not surprising that the support for function references is much better in Concurnas than that achievable with Java.
Both Java and Concurnas provide lambdas. For example in Java:
java.util.ArrayList<Integer> mylist = new java.util.ArrayList<Integer>();
mylist.add(1);mylist.add(2);mylist.add(3);mylist.add(4);
//a -> a+10 is a lambda
java.util.List<Integer> result = mylist.stream().map(a -> a+10).collect(java.util.stream.Collectors.toList());
And the same in Concurnas, using a SAM type:
trait Operator{//This is a SAM type as there is only one method defined which is abstract
def perform(arg int, arg2 int) int
}
class MyNumberHolder(~a int){
def apply(b int, operator Operator) => operator.perform(a, b)
}
mnh = MyNumberHolder(12)
res = mnh.apply(50, a, b => a + b)//here is a lambda!
And here is a Concurnas lambda defined on its own:
//and here is a lambda on its own...
power2 (int) int = a => a*2
Concurnas has good pattern matching support, inspired by languages such as Scala and Haskell. Consider the following in Java:
public static String matcher(Object an) {
if(an instanceof Person) {
Person p = (Person)an;
if(p.yearOfBirth < 1970) {
return "Person. Born: " + p.yearOfBirth;
}else {
return "A Person";
}
}else if(an instanceof Integer) {
int value = (Integer)an;
if(value < 10){
return "small number";
}else {
return "another number";
}
}else {
return "unknown input";
}
}
And here is the same in Concurnas:
class Person(-yearOfBirth int)
def matcher(an Object){
match(an){
Person(yearOfBirth < 1970) => "Person. Born: {an.yearOfBirth}"
Person => "A Person"
int; < 10 => "small number"
int => "another number"
x => "unknown input"
}
}
res = matcher(x) for x in [Person(1829), Person(2010), "oops", 43, 5]
//res == [Person. Born: 1829, A Person, unknown input, another number, small number]
The Concurnas implementation is much easier to understand and maintain.
Concurnas has support for lazy variables. Java does not. Here is an example:
a = 100
lazy myvar = {a=400; 22}//assign, with side effect of re-assigning a
res = [a myvar a] //lets create an array holding the value of a, the evaluated myvar and a again
//res == [100 22 400]
As a function argument:
a = 100
lazy myvar = {a=400; 22}//assign, with side effect of re-assigning a
def makeArray(lazy operate int) => [a operate a]
res = makeArray(myvar:)
//res == [100 22 400]
Besides the concurrency model exposed within Concurnas one of the most noticeable differences compared to Java is its concise model of class definition.
Let's look at a typical data oriented class in Java, we will define a equals
and hashCode
method so that we can use any instance objects in HashMap
objects etc:
public class UniversityMember{
private boolean enrolled;
public UniversityMember(boolean enrolled){
this.enrolled = enrolled;
}
public boolean getEnrolled(){
return this.enrolled;
}
public void setEnrolled(boolean enrolled){
this.enrolled = enrolled;
}
@Override
public int hashCode(){
if(enrolled){
return 1;
}else{
return 0;
}
}
@Override
public boolean equals(Object an){
if(an instanceof UniversityMember){
UniversityMember um = (UniversityMember)an;
return um.enrolled == this.enrolled;
}
return false;
}
}
public class Person extends UniversityMember{
private String firstname;
private String sirname;
private short yob;
public Person(String firstname, String sirname, short yob, boolean enrolled){
super(enrolled);
this.firstname = firstname;
this.sirname = sirname;
this.yob = yob;
}
public Person(String firstname, String sirname, short yob){
this(firstname, sirname, yob, false);
}
public boolean getFirstname(){
return this.firstname;
}
public void setFirstname(String firstname){
this.firstname = firstname;
}
public boolean getSirname(){
return this.sirname;
}
public void setSirname(boolean sirname){
this.sirname = sirname;
}
public short getYob(){
return this.yob;
}
public void setYob(short yob){
this.yob = yob;
}
@Override
public int hashCode(){
int hc = super.hashCode() + this.yob;
if(this.firstname != null){
hc += this.firstname.hashCode()
}
if(this.sirname != null){
hc += this.sirname.hashCode()
}
return hc;
}
@Override
public boolean equals(Object an){
if(an instanceof Person && super.equals(an)){
Person pers = (Person)an;
if(Objects.equal(pers.sirname, this.sirname) && Objects.equal(pers.firstname, this.firstname) && pers.yob == this.yob){
return true;
}
}
return false;
}
}
And the same in Concurnas:
class UniversityMember(~enrolled boolean)//~indicates we wish to generate a setter and a getter
class Person(~firstname String, ~sirname String, ~yob short, enrolled = false) < UniversityMember(enrolled)
The difference is quite dramatic! 100 lines of code to 2, a 50:1 improvement! Not only does Concurnas generate all the necessary constructors (with appropriate wiring to super constructors), setters and getters for us, but it has also automatically generated a reliable cycle free hashCode
and equality
implementation as well as a deep copy implementation (explored later).
We can invoke getters and setters on instance objects in Java as follows:
Person p1 = new Person("Dave", "Smith", 1970);
String name = p1.getFirstname();
p1.setSirname("Brown");
And in Concurnas:
p1 = Person("Dave", "Smith", 1970)//new is optional
name = p1.firstname
p1.sirname = "Brown"
Concurnas will automatically map to getters and setters using the above field access style notation.
Luckily for us, in our Java implementation above we have put the extra work in to manually create hashCode
and equality
implementations, this allowing us to perform equality by value and hashing by value thus enabling the following code:
Person p1 = new Person("Dave", "Smith", 1970);
Person p2 = new Person("Dave", "Smith", 1970);
boolean eq = Objects.equal(p1, p2);//Objects implementation deals with the case where p1 or p2 is null
java.util.HashSet<Person> mySet = new java.util.HashSet<Person>();
mySet.add(p1);
mySet.add(p2);
int sz = mySet.size();//sz == 1
Equality in Concurnas is easier, equality does what one would expect:
p1 = Person("Dave", "Smith", 1970)
p2 = Person("Dave", "Smith", 1970)
eq = p1 == p2//true!
req = p1 &== p2//not true, p1 and p2 are different objects
mySet = set()
mySet.add(p1)
mySet.add(p2)
sz = mySet.size()//sz == 1
Copying data has always been problematic in Java. Here is a typical example carrying on from our previous instance:
Person p1 = new Person("Dave", "Smith", 1970);
Person p2 = new Person(p1.getFirstname(), p1.getSirname(), p1.getYob(), p1.getEnrolled());
Luckily we exposed all state of our Person
class so the above code works. This is never an issue for Concurnas. As previously mentioned, with Concurnas a safe deep copy implementation is provided:
p1 = Person("Dave", "Smith", 1970)
p2 = p1@//copy!
The copy operator has a few other tricks we can make use of as follows:
p1 = Person("Dave", "Smith", 1970)
p2 = p1@(sirname="Taylor")//copy! with specific field overwrite
This works for all instance objects running in a Concurnas program, including instance objects of Java classes.
The following pattern of code, when casting objects, is common in Java:
if(anObject instanceof Something){
Something asSomething = (Something)anObject;
//now we can use anObject as asSomething
}
Concurnas presents a more concise way of solving this problem with smart casts:
if(anObject is Something){
//now we can use anObject as a Something instance - no cast needed!
}
Traits in Concurnas are extremely powerful and on a par with those found in Scala. They enable us to create classes via composition as opposed to exclusively via inheritance. Java has interfaces but interfaces are limited to abstract method declarations or default methods, interfaces may not contain state. This is different with Concurnas where state may be abstract or concrete within a trait:
trait IndAncDec{
-count int//abstract state needing provision within implementing class
-countdown int =0
def inc() => ++count + countdown
def dec() => --countdown + count
}
class HasIncAndDec ~ IndAncDec{
override count = 0
}
with(HasIncAndDec()){ inc(), inc(), dec(), count, countdown }
//returns: (1, 2, 1, 2, -1)
Overall the implementation of traits is far superior to Java interfaces.
Local classes are a feature of Concurnas. We can compose classes using traits at their point of localized declaration as follows:
def foo(){
AnotherCounty = class ~ IndAncDec{
override count = 0
}
new AnotherCounty()
}
Concurnas also solves the diamond pattern by permitting multiple traits to be implemented by a class and requiring disambiguation at the concrete (non abstract) class level where appropriate:
abstract class AbstractFooClass{
def foo() => "version AbstractFooClass"
}
trait A{
def foo() => "version A"
}
trait B{
def foo() => "version B"
}
class FooClass < AbstractFooClass with B, A{
override def foo() => "" + [super[AbstractFooClass].foo(), super[A].foo(), super[B].foo()]
}
FooClass().foo()
In the above example we override the foo
method in FooClass
- as this is required in order to resolve the ambiguity in the inherited versions of foo
. We then choose to call, via the qualified super
keyword, a few different implementations of foo
.
Traits may inherit from one another:
trait SuperTrait{
def foo() String => "superTrait method called"
}
trait ATrait < SuperTrait{
override def foo() => "trait method called " + super.foo()
}//override keyword must be used since we are overriding the definition from the super trait: SuperTrait
class AClass with ATrait
AClass().foo()
//returns: trait method called, superTrait method called
The hierarchy of trait inheritance may be differed until the point of declaration of a class. This has the effect of changing the meaning of what super
refers to for each trait contingent upon their order in said class declaration. Thus stacking is possible:
abstract class Operator{
def operate(a int) int => a
}
open class ID < Operator{
override def operate(a int) => a
}
trait PlusOne < Operator{ override def operate(a int) => super.operate(a)+1 }
trait Square < Operator{ override def operate(a int) => super.operate(a)**2 }
trait MinusOne < Operator{ override def operate(a int) => super.operate(a)-1 }
trait DivTwo < Operator{ override def operate(a int) => super.operate(a)/2 }
x = new ID ~ PlusOne, Square, MinusOne, DivTwo // (((x/2) - 1) ** 2) + 1
y = new ID ~ DivTwo, MinusOne, Square, PlusOne //reverse operator application
z = new ID ~ DivTwo, MinusOne, Square, PlusOne{
override def operate(a int) => super.operate(a)+1000
}//reverse operator application with additional operation
inst.operate(10) for inst in [x y z]
//returns: [60, 17, 1017]
Null has been described as a billion dollar mistake. In Java, since it's valid for any variable reference at any point in a programs execution to be null, to write null safe programs we are obliged to persistently test for nullability. Here is an example:
public static int plusOneLength(String something){
return something.length();
}
plusOneLength(null);//this causes a null pointer exception to be thrown
To be safe we really should write the following, but doing so consistently is time consuming, easy to get wrong and tedious so most developers don't bother:
public static int plusOneLength(String something){
if(null == something){//handle null
return 0;
}
return something.length();
}
plusOneLength(null);//this is ok now, null is handled
Concurnas, like Kotlin and Swift is a null safe language, nullability is built into its type system. Concurnas requires that a variable's type be explicitly declared as being nullable in order for it to be assigned null or a potentially null value. This restriction allows us to largely eliminate the risk of NullPointerException
, empowering us to write more concise, safe code:
aString String = "something"
aString = null //compilation error, aString is not of a nullable type.
Of course there are some instances where using null is of value/unavoidable (say when using libraries written in Java or other JVM languages which do not offer null safety). To this effect Concurnas offers a number of mechanism by which null
can be factored for. We can append a ?
to the type in order to declare a nullable type:
aString String? = "something"
aString = null //this is ok
And we can implement our nullable plusOneLength
function as follows:
def plusOneLength(something String?) => something?.length()
plusOneLength(null)//this is ok now, null is handled
Alternatively Concurnas offers null scope analysis, thus we can write the following:
def plusOneLength(something String?){
if(null == something){
0
}else{//'something' is provably non null at this point...
something.length()//... so this code is null safe
}
}
plusOneLength(null)//this is ok now, null is handled
Concurnas also offers the elvis operator and no null assertion
Often when we are applying the builder pattern, or otherwise calling a lot of methods on an object as a chained method call, it's more convenient for those methods to return a reference to their host instance object to make the following code possible:
class Accumilator{
private int state = 0;
public Accumilator(int state) {
this.state = state;
}
public Accumilator add(int e) {
state += e;
return this;
}
public Accumilator minus(int e) {
state -= e;
return this;
}
public Accumilator mul(int e) {
state *= e;
return this;
}
public int getState() {
return this.state;
}
}
int res = new Accumilator(2).add(23).mul(8).minus(12).getState();//188
Effectively we have had to change the definition of our Accumilator
in order to suit the usage of said class. With Concurnas it is not necessary for any changes like this to be made in order to support chained method calls, as we may use the double dot ..
syntax. The double dot simply returns the object upon which a method was invoked rather than its return type (if there is one):
class Accumilator(-state int){
def add(e int) => state += e;;
def minus(e int) => state -= e;;
def mul(e int) => state *= e;;
}
Accumilator(2)..add(23)..mul(8)..minus(12).state
Lovely!
The with
statement is often used in similar situations as when chained method calls are required. Consider the following (carrying on from our previous example) in Java:
Accumilator acc = new Accumilator(2);
{
acc.add(23);
if(acc.getState() > 16) {
acc.minus(16);
}else {
acc.mul(8);
acc.mul(2);
}
}
int res = acc.getState();
This can be achieved in Concurnas using a with statement as follows:
res = with(Accumilator(2)){
add(23)//maps to accumilator add(23)
if(state > 16){//maps to accumilator getState()... etc
minus(16)
}else{
mul(8)
mul(2)
}
state
}//res == 9
Often when writing code in Java that makes use of generics types we find ourselves writing very long definitions such as the following:
ArrayList<Set<HashMap<ArrayList<String>, Set<Integer>>>> myDataStructureInstance = new ArrayList<Set<HashMap<ArrayList<String>, Set<Integer>>>>(10)
public static ArrayList<Set<HashMap<ArrayList<String>, Set<Integer>>>> processStruct(ArrayList<Set<HashMap<ArrayList<String>, Set<Integer>>>> myDataStructureInstance){
//operation here
return null
}
//a slight variant of the above structure...
ArrayList<Set<HashMap<ArrayList<String>, Set<String>>>> strucutreVariant = new ArrayList<Set<HashMap<ArrayList<String>, Set<String>>>>(10)
Not only is this hard to read but hard to maintain as simple changes in the data structure require changes in a lot of places. We also end up with cut-and-paste code where, as for our strucutreVariant
type, there are small changes in the type definition required.
With Concurnas we need only define the following typedef
in order to express the type of our myDataStructureInstance
variable, we can then use this as a macro when working with this type elsewhere in our code:
typedef DataStructure = ArrayList<Set<HashMap<ArrayList<String>, Set<Integer>>>>
myDataStructureInstance = new DataStructure(10)
In fact, we can even take this one step further and define a generic typedef as follows (and use it to create our DataStructure
type):
typedef DataStructure<X, Y> = ArrayList<Set<HashMap<ArrayList<X>, Set<Y>>>>
typedef DataStructure = DataStructure<String, Integer>
We can use this within the main body of our code as follows:
myDataStructureInstance = new DataStructure(10)
def processStruct(myDataStructureInstance DataStructure) DataStructure?{//operation here
null
}
//a slight variant of the above structure...
strucutreVariant = DataStructure<String, String>(10)
Typedefs offer a much better solution than what we see in the Java case.
With Java we can create Strings as follows:
String myString = "hello \"world\"";//String with escape character
String formatString = String.format("xyz: %s %s", 12, 13)// => formatString = xyz: 12 13
String concat = "hey: " + 12;
Concurnas offers similar capabilities for Strings with some additional convenience:
myString = 'hello "world"';//escape character's not needed
myString2 = "hello 'world'";//escape character's not needed
formatString = String.format("xyz: %s %s", 12, 13)// => formatString = xyz: 12 13
concat = "hey: {12}"//code may be embedded within a String
Concurnas supports (via implicit operator overloading) a number of operations on Strings:
myString = "abcdefg"
cont = "de" in myString
aChar = myString[2] //==c
substr = myString[2 ... 4] //==cd
substr = myString[4 ... ] //efg
substr = myString[ ... 4] //abcd
elms = x for x in myString //[a, b, c, d, e, f, g]
This is nice and easy to read and work with. In contrast this is the same code in Java:
String myString = "abcdefg";
char aChar = myString.charAt(2);
String substr = myString.substring(2, 4);
String substr = myString.substring(4);
String substr = myString.substring(0, 4);
java.util.ArrayList<Character> elms = new java.util.ArrayList<Character>();
{
for(int n = 0; n < myString.length(); n++){
elms.add(myString.charAt(n));
}
}
Concurnas has first class citizen support for regex. Compare the following in Java:
Pattern p = Pattern.compile("a*b");
vs Concurnas:
pat = r"a*b"
A small but handy feature.
Checkout the third and final article in the series for more differences between Concurnas and Java.