Java is the first language that I learned and used professionally. It has been my bread and butter for about a decade and half years. And yet, it’s not the only language I’ve learned nor used in all my years: for example, a long time ago, I’ve had to develop JavaScript code to implement a dynamic user interface. At that time, it was called DHTML… A couple of years ago, I also taught myself Kotlin, and never stopped using it. Last year, working for a new company, I tried Clojure - that didn’t stick though.
In all cases, Java still represents the baseline by which I learn and judge other languages. Here are some interesting characteristics of languages, which I find quite thought-challenging coming from a Java background.
JavaScript: prototypes
JavaScript was the first language I had to use alongside Java. While JavaScript has evolved along all those years, there’s one common feature that is implemented very strangely: the instantiation of new objects.
In Java, one first creates a class:
public class Person {
private final String name;
private final LocalDate birthdate;
public Person(String name, LocalDate birthdate) {
this.name = name;
this.birthdate = birthdate;
}
public String getName() {
return name;
}
public LocalDate getBirthdate() {
return birthdate;
}
}
Then, one can proceed to create instances of that class:
var person1 = new Person("John Doe", LocalDate.now());
var person2 = new Person("Jane Doe", LocalDate.now());
JavaScript is quite similar to Java syntax-wise:
class Person {
constructor(name, birthdate) {
this.name = name;
this.birthdate = birthdate;
}
}
let person1 = new Person("John Doe", Date.now());
let person2 = new Person("Jane Doe", Date.now());
The parallel stops there. Thanks to JavaScript dynamic’s nature, it’s possible do add properties and functions to an existing instance.
person1.debug = function() {
console.debug(this);
}
person1.debug();
However, those are only added to the instance. Other instances lack those additions:
person2.debug(); // Throws TypeError: person2.debug is not a function
To add functions (or properties) to all instances, present and future, one needs to leverage the concept of prototypes:
Person.prototype.debug = function() {
console.debug(this);
}
person1.debug();
person2.debug();
let person3 = new Person("Nicolas", Date.now());
person3.debug();
Kotlin: extension functions/properties
Some years ago, I tried to teach myself Android. I found the experience not very developer-friendly: of course, I understand one of the goals is to have the smallest footprint possible, but this is at the cost of a very terse API.
I remember having to call methods with a lot of parameters, most of them being null. I tried to find a way to cope with that… and found Kotlin’s extension properties - with default parameters. I stopped my path to learn Android, but I continued using Kotlin.
I love Kotlin. A lot of people praise Kotlin for it’s null-safety approach. I like it, but for me, the highest value lies elsewhere.
Imagine we regularly need to capitalize strings. The way to achieve that in Java is to create a class, with a static method:
public class StringUtils {
public static String capitalize(String string) {
var character = string.substring(0, 1).toUpperCase();
var rest = string.substring(1, string.length() - 1).toLowerCase();
return character + rest;
}
}
In earlier days, there was not a single project without a StringUtils
and a DateUtils
class.
Fortunately, now, existing libraries provide the most needed capabilities e.g. Apache Commons Lang and Guava.
And yet, they follow the same design principle based on static methods.
This is sad, because Java is supposed to be an OOP language.
Unfortunately, static methods are not Object-Oriented.
Kotlin allows to add behavior, respectively state, to existing classes, with the help of extension functions, and properties. The syntax is pretty straightforward, and is fully compatible with the Object-Oriented approach:
fun String.capitalize(): String {
val character = substring(0, 1).toUpperCase()
val rest = substring(1, length - 1).toLowerCase()
return character + rest
}
I use this a lot when writing Kotlin code.
Under the hood, the Kotlin compiler generates bytecode that is similar to the one from the Java code. It’s "only" syntactic sugar, but from a design’s point-of-view, it’s a huge improvement compared to the Java code!
Go: implicit interface implementation
In most OOP languages (Java, Scala, Kotlin, etc.), classes can adhere to a contract also known as an interface. This way, client code can reference the interface and not care about any specific implementation.
public interface Shape {
float area();
float perimeter();
default void display() {
System.out.println(this);
System.out.println(perimeter());
System.out.println(area());
}
}
public class Rectangle implements Shape {
public final float width;
public final float height;
public Rectangle(float width, float height) {
this.width = width;
this.height = height;
}
@Override
public float area() {
return width * height; (1)
}
@Override
public float perimeter() {
return 2 * width + 2 * height; (1)
}
public static void main(String... args) {
var rect = new Rectangle(2.0f, 3.0f);
rect.display();
}
}
1 | One should use BigDecimal for precision purpose - but that’s not the point here |
The important point is:
because Rectangle
implements Shape
, one can call the display()
method defined on Shape
on any instance of Rectangle
.
Go is not an OOP language: it has no concept of classes. It provides structures, and functions can be associated with such a structure. It also provides interfaces that structures can implement.
However, Java’s approach to the implementation-interface is explicit:
the Rectangle
class declares it implements Shape
.
On the opposite, Go’s approach is implicit.
A structure that implements all functions of an interface implicitly implements this interface.
This translates into the following code:
package main
import (
"fmt"
)
type shape interface { (1)
area() float32
perimeter() float32
}
type rectangle struct { (2)
width float32
height float32
}
func (rect rectangle) area() float32 { (3)
return rect.width * rect.height
}
func (rect rectangle) perimeter() float32 { (3)
return 2 * rect.width + 2 * rect.height
}
func display(shape shape) { (4)
fmt.Println(shape)
fmt.Println(shape.perimeter())
fmt.Println(shape.area())
}
func main() {
rect := rectangle{width: 2, height: 3}
display(rect) (5)
}
1 | Define the shape interface |
2 | Define the rectangle structure |
3 | Add both the shape functions to rectangle |
4 | The display() function only accepts a shape |
5 | Because rectangle implements all functions of shape and because of implicit implementation, rect is also a shape .
Hence, it’s perfectly legal to call the display() function and pass rect as a parameter |
Clojure: "dependent types"
My previous company was very invested in Clojure. Because of that, I tried to learn the language, and even wrote a couple of posts to sum up my understanding of it.
Clojure is heavily inspired by LISP. Hence, expressions are surrounded by parentheses, and the function to execute is positioned first inside of them. Also, Clojure is a dynamically-typed language: while there are types, they are not declared.
On the side, the language provides contract-based programming. One can specify pre-conditions and post-conditions: those are evaluated at runtime. Such conditions can do type-checking e.g. is a parameter a string, a boolean, etc.? - and can even go further, something akin to _dependent types:
In computer science and logic, a dependent type is a type whose definition depends on a value. A "pair of integers" is a type. A "pair of integers where the second is greater than the first" is a dependent type because of the dependence on the value.
https://en.wikipedia.org/wiki/Dependent_type
It’s enforced at runtime, so it cannot be truly called dependent types. However, it’s the closest I’ve seen in the languages I’ve dabbled in.
I previously wrote a post dependent types and contract-based programming in detail.
Elixir: pattern matching
Some languages tout themselves as providing pattern matching features. In general, pattern-matching is used in evaluating variables e.g. in Kotlin:
var statusCode: Int
val errorMessage = when(statusCode) {
401 -> "Unauthorized"
403 -> "Forbidden"
500 -> "Internal Server Error"
else -> "Unrecognized Status Code"
}
This usage is a switch statement on steroids. However, in general, pattern matching applies much more widely. In the following snippet, one checks first regular HTTP status error codes, and if none is found, defaults to a more generic error message:
val errorMessage = when {
statusCode == 401 -> "Unauthorized"
statusCode == 403 -> "Forbidden"
statusCode - 400 < 100 -> "Client Error"
statusCode == 500 -> "Internal Server Error"
statusCode - 500 < 100 -> "Server Error"
else -> "Unrecognized Status Code"
}
Still, it’s limited.
Elixir is a dynamically-typed language running on the Erlang OTP that brings pattern matching to a whole new level. Elixir’s pattern-matching can be used for simple variable destructuring:
{a, b, c} = {:hello, "world", 42}
a
will be assigned :hello
, b
"world" and c
42.
It also can be used for more advanced destructuring, on collections:
[head | tail] = [1, 2, 3]
head
will be assigned 1, and tail
[2, 3]
.
Yet, it goes even beyong that, for function overloading.
Being a functional language, Elixir doesn’t have keywords for loops (for
or while
):
loops need to be implemented using recursion.
As an example, let’s use recursion to compute the size of a List
.
In Java, it’s easy because there’s a size()
method, but Elixir API doesn’t provide such a feature.
Let’s implement it in pseudo-code, the Elixir way - using recursion.
public int lengthOf(List<?> item) {
return lengthOf(0, items);
}
private int lengthOf(int size, List<?> items) {
if (items.isEmpty()) {
return size;
} else {
return lengthOf(size + 1, items.remove(0));
}
}
This can be translated into Elixir nearly line by line:
def length_of(list), do: length_of(0, list)
defp length_of(size, list) do
if [] == list do
size
else
[_ | tail] = list (1)
length_of(size + 1, tail)
end
end
1 | Pattern matching with variable destructuring.
The head value is assigned to the _ variable, which means there’s no way to reference it later - because it’s not useful. |
However, as mentioned previously, Elixir pattern matching also applies to function overloading. Hence, the nominal way to write Elixir would be:
def list_len(list), do: list_len(0, list)
defp list_len(size, []), do: size (1)
defp list_len(size, list) do (2)
[_ | tail] = list
list_len(size + 1, tail)
end
1 | Call this function if the list is empty |
2 | Call this function otherwise |
Note that patterns are evaluated in the order of declaration: in the above snippet, Elixir first evaluates the function with the empty list, and only evaluates the second function if this doesn’t match i.e. if the list is not empty. If the functions were to be declared in the opposite order, matching would occur on the non-empty list every time.
Python: for-comprehensions
Python is a dynamically-typed language on a trend.
Just as in Java, Python offers loops, via the for
keyword.
The following snippets loops through all items in the collection, and print them one by one.
for n in [1, 2, 3, 4, 5]:
print(n)
To collect all items in a new collection, one can create an empty collection, and then add each item in the loop:
numbers = []
for n in [1, 2, 3, 4, 5]:
numbers.append(n)
print(numbers)
Yet, it’s possible to use a nifty Python’s feature: for comprehensions.
Though it uses the same for
keyword as a standard loop, a for comprehension is a functional construct to achieve the same result.
numbers = [n for n in [1, 2, 3, 4, 5]]
print(numbers)
The output of the previous snippet is [1, 2, 3, 4, 5]
.
It’s also possible to transform each item. For example, the following snippet will compute the square of each item:
numbers = [n ** 2 for n in [1, 2, 3, 4, 5]]
print(numbers)
This outputs [1, 4, 9, 16, 25]
.
A benefit of for comprehensions is the ability to use conditionals. For example, the following snippet will filter only items that are even, and then square them:
numbers = [n ** 2 for n in [1, 2, 3, 4, 5] if n % 2 == 0]
print(numbers)
The output is [4, 16]
.
Finally, for comprehensions allow cartesian products.
numbers = [a:n for n in [1, 2, 3] for a in ['a', 'b']]
print(numbers)
The will output [('a', 1), ('b', 1), ('a', 2), ('b', 2), ('a', 3), ('b', 3)]
.
The above for comprehensions are also called list comprehensions because they are designed to create new lists. Map comprehension is very similar and aims to create… maps.
Conclusion
While Java is an ever-evolving language, and it’s a good thing. And yet, approaches found in other languages are worth being studied. Remember that a language structures the way one thinks about a problem, and how one designs the solution. Learning or at least getting familiar with other languages is a great way to consider other perspectives.