Java.next: General Principles of New Generation Languages
This is the first part of a series of articles on Java.next. In the first part, I'm going to look at general principles that are shared by the Java.next languages.
I chose four languages ​​that I called “Java.next” together: Clojure, Groovy, JRuby, and Scala. At first glance, these languages ​​are completely different. Clojure is Lisp. Groovy is "almost Java." JRuby has the beauty of Ruby, and leverages the power of Rails. Scala, unlike other languages, insists that we need static typing.
As you can imagine, which of these languages ​​is better around for some purpose, or even better, there is constant debate. In this debate, the fact that these languages ​​have much in common is somehow forgotten. They all evolved against the background of the same language: Java. Their design is constantly influenced by the study of what works well in Java and what does not.
In this article I will demonstrate two important principles that are common to these languages:
- Over the past decade, when we coded in object-oriented, virtual machine-based languages, we learned a lot about writing expressive, easily supported applications. Java.next languages ​​embody this knowledge, confessing the principle of “essence above ceremonies” (essence over ceremony)
- Decisions regarding the design of languages, which were made under the influence of the principle “essence above ceremonies”, led to very serious changes in programming. The mental shift in the transition from Java to Java.next is much larger than the previous shift between C / C ++ and Java.
I have summarized the general advantages of Java.next to eight points, which I will discuss in more detail below:
- Everything is an object
- Easy property declarations
- Powerful collections
- Functional programming
- Operator Overriding
- Easy to support exceptions
- Adding custom methods to existing objects
- Creating your own language constructs
Everything is an object
In Java, we are faced daily with the difference between objects and primitives. This raises three practical problems:
- It is necessary to duplicate API: one method for objects and one more for primitives. Or, even worse, one method for objects and the method for several primitive types.
- The default numeric types have a limited range. Go beyond it and the program will break down in a magical way.
- You cannot use intuitive mathematical operations (+, -, etc.) with special exact numeric types.
In Java.next languages, everything is an object. You can call methods for any type using the same syntax.
')
; clojure
(. 1 floatValue)
1.0
// groovy
1.floatValue()
===> 1.0
# ruby
1.to_f
=> 1.0
// scala
1.floatValue
res1: Float = 1.0
Easy property declarations
In Java, in order to create a property, you have to define a field, getter, setter, and (quite often) a constructor, and write the correct access modifiers everywhere. In java.next, you can define all this in one step.
; clojure
(defstruct person :first-name :last-name)
// groovy
class Person {
def firstName
def lastName
}
# ruby
Person = Struct.new(:first_name, :last_name)
// scala
case class Person(firstName: String, lastName: String) {}
If you need to redefine a specific getter, setter, or constructor for a class, you can easily do it without having to tread on the code for all the other standard cases.
And that's not it! All these languages ​​adhere to the principle “There Is More than One Way to Do It” (There's More Than One Way To Do It, TMTOWTDI), so there are several options for implementing the techniques shown above.
Powerful collections
Java.next languages ​​offer convenient syntax for the most important types of collections: arrays and associative lists (maps). In addition, you can interconnect several operations on collections by passing functions as arguments, which allows you to stop using iterators and loops. For example, to find all squared values ​​from one to ten that are odd, you can do the following:
; clojure
(filter (fn [x] (= 1 (rem x 2))) (map (fn [x] (* xx)) (range 10)))
(1 9 25 49 81)
// groovy
(1..10).collect{ it*it }.findAll { it%2 == 1}
===> [1, 9, 25, 49, 81]
# ruby
(1..10).collect{ |x| x*x }.select{ |x| x%2 == 1}
=> [1, 9, 25, 49, 81]
// scala
(1 to 10).map(x => x*x).filter(x => x%2 == 1)
res20: Seq.Projection[Int] = RangeMF(1, 9, 25, 49, 81)
Similar facilities are provided for working with key-value pairs or, in other words, hash tables (dictionaries) or dictionaries.
Functional programming.
The convenience of the collections we have just described is a special case of a more general idea: functional programming. Java.next supports functions as first-class objects, allowing you to pass them as arguments, define functions that create new functions, and use closures. As a simple example, consider the creation of a function that adds some value defined at runtime:
; clojure
(defn adder [x] (fn [y] (+ xy)))
// groovy
adder = { add -> { val -> val + add } }
# ruby
def adder(add)
lambda { |x| x + add }
end
// scala
def sum(a: Int)(b: Int) = a + b
Operator Overriding
In Java, you cannot override statements. For types like BigDecimal, mathematical operations look like this:
// Java math
balance.add(balance.multiply(interest));
Java.next languages ​​allow you to redefine operators. This makes it possible to create new types that are perceived as well as built-in ones. Those. You can create ComplexNumber or RationalNumber types that support the +, -, *, and / operations.
; Clojure
(+ balance (* balance interest))
// Groovy
balance + (balance * interest)
# JRuby
balance + (balance * interest)
// Scala (See [1])
balance + (balance * interest)
Easy to support exceptions
Checked exceptions were an unsuccessful experiment. The Java code, because of them, only swells, in no way contributing to the improvement of error handling. Worse, checked exceptions are a real headache for supporting at the boundary between layers of abstractions. The introduction of new types of exceptions should not lead to recompilation!
Java.next languages ​​do not require you to declare checked exceptions or explicitly handle checked exceptions that come from other code. Although the fact that other languages ​​are able to ignore the ugly checked exceptions of the Java language indicates the unexpected flexibility of Java as a platform.
Adding custom methods to existing objects
In Java, you cannot add methods to existing types. This leads to problems in modeling objects, when developers have to create utility classes that violate the principles of OOP:
// Java (from the Jakarta Commons)
public class StringUtils {
public static boolean isBlank(String str) {
int strLen;
if (str == null || (strLen = str.length()) == 0) {
return true;
}
for (int i = 0; i < strLen; i++) {
if ((Character.isWhitespace(str.charAt(i)) == false)) {
return false;
}
}
}
In Java.next languages, you can add methods to existing types:
; Clojure
(defmulti blank? class)
(defmethod blank? String [s] (every? #{\space} s))
(defmethod blank? nil [_] true)
// Groovy
String.metaClass.isBlank = {
length() == 0 || every { Character.isWhitespace(it.charAt(0)) }
}
# Ruby (from Rails)
class String
def blank?
empty? || strip.empty?
end
end
// Scala
class CharWrapper(ch: Char) {
def isWhitespace = Character.isWhitespace(ch)
}
implicit def charWrapper(ch: Character) = new CharWrapper(ch)
class BlankWrapper(s: String) {
def isBlank = s.isEmpty || s.forall(ch => ch.isWhitespace)
}
implicit def stringWrapper(s: String) = new BlankWrapper(s)
Creating your own language constructs
In Java, you have a language and there are libraries. They are fundamentally different: you can create new libraries, but you cannot create new language constructs.
In Java.next languages, the line between language and libraries is more blurred. You can create new expressions that work as if they were built into the language. For example, Clojure contains the function
and :
; clojure
(and 1 2) => 2
But maybe your problem is not so simple. You need the
most function, which returns true if most arguments are true. There is no such function in Clojure, but you can create it yourself:
; clojure
(most 1 2) => true
(most 1 2 nil) => true
(most 1 nil nil) => false
The main thing here is not whether my language needs the
most conditional operator. Perhaps not needed. The main thing is that different areas of use need different possibilities. In Java.next languages, the boundary between language and libraries is minimal. You can tailor the language to your needs instead of looking for workarounds.
Or here's another example. Consider attribute syntax in Ruby:
# Ruby
class Account
attr_accessor :name
dsl_attribute :type
end
attr_accessor is built into the language.
dsl_attribute is the library method I wrote. It allows you to omit the "=" character to assign values. Those.
# normal attributes
account.name = "foo"
# equals-free attributes
account.type checking
Conclusion
Java.next languages ​​have a lot in common. Although I used small examples to demonstrate, real power comes from using all these features together. When they are combined in Java.next languages, this leads to a completely different coding style.
- You no longer need to encode in defensively (defensively), clutching at factories, patterns and dependency injection in order to keep your code testable and easily modified. Instead, you create a minimal solution, and then develop it.
- Instead of direct coding in Java.next, you can create your own Domain-Specific Language that best suits your problem area.
In my experience, this coding style leads to a decrease in the source size by an order of magnitude without reducing readability.
Many people are waiting for the “Next Cool Language” (next big language). This language is already here. Only this is not one language, but a collection of ideas that I have listed (plus, perhaps, those that I missed), expressed in Java.next languages.
Does Java.next deserve the word "cool"? Of course. My experience shows that the transition from Java to Java.next in every aspect is no less steep than previous tectonic shifts in the industry, both in terms of the learning curve and in terms of performance gains.
Working in the development industry, we need to raise our bar and start exploring Java.next. Then it will be possible to discuss the difference between these languages. I will try to consider the unique features of Java.next languages ​​in future articles in this series.
From the translator: I decided to cite English terms in brackets more often, because I prefer them. Sometimes I don’t translate a term at all if I don’t know a successful variant. Let me know if this suits you or not. And be indulgent to my mistakes. ;)