Pull to refresh

Java.next: Общие принципы языков нового поколения

Reading time 7 min
Views 9.6K
Original author: Stuart Halloway

Java.next: Общие принципы языков нового поколения


Это первая часть серии статей насчёт Java.next. В первой части я собираюсь рассмотреть общие принципы, которые разделяют языки Java.next.

Я выбрал четыре языка, которые вместе и назвал «Java.next»: Clojure, Groovy, JRuby, and Scala. На первый взгляд, эти языки совершенно разные. Clojure — это Lisp. Groovy — это «почти Java». JRuby обладает красотой Ruby, и использует мощь Rails. Scala, в отличие от других языков, настаивает на том, что нам нужна статическая типизация.

Как вы можете представить, вокруг того какой из этих языков лучше для каких-то целей или лучше вообще ведутся постоянные дебаты. В этих дебатах как-то забывается тот факт, что у этих языков много общего. Они все развивались на фоне одного и того же языка: Java. На их дизайн постоянно влияло изучение того, что работает хорошо в Java, а что нет.

В этой статье я продемонстрирую два важных принципа, которые являются общими для этих языков:
  • За последнее десятилетие, когда мы кодировали в объектно-ориентированных, основанных на виртуальных машинах языках, мы научились очень многому насчёт написания выразительных, легко поддерживаемых приложений. Языки Java.next воплощают в себе это знание, исповедуя принцип «суть превыше церемоний» (essence over ceremony)
  • Решения относительно дизайна языков, которые принимались под влиянием принципа «суть превыше церемоний» привели к очень серьёзным изменениям в программировании. Ментальный сдвиг при переходе с Java на Java.next гораздо больше, чем предыдущий сдвиг между C/C++ и Java.

Я свёл общие преимущества языков Java.next к восьми пунктам, которые более детально рассмотрю ниже:
  • Всё является объектом
  • Простота объявления свойств (properties)
  • Мощные коллекции
  • Функциональное программирование
  • Переопределение операторов
  • Легкость в поддержке исключений
  • Добавление собственных методов к существующим объектам
  • Создание собственных конструкций языка

Всё является объектом

В Java нам ежедневно приходится сталкиваться с разницей между объектами и примитивами. Это порождает три практические проблемы:
  1. Приходится дублировать API: один метод для объектов и ещё один для примитивов. Или, что ещё хуже, один метод для объектов и по методу для нескольких типов-примитивов.
  2. Числовые типы, используемые по умолчанию, имею ограниченный диапазон. Выйдите за его рамки — и программа сломается волшебным образом.
  3. Вы не можете использовать интуитивные математические операции (+,- и т.п.) со специальными точными числовыми типами.

В языках Java.next всё является объектом. Вы можете вызывать методы для любых типов, используя один и тот же синтаксис.

; clojure
(. 1 floatValue)
1.0

// groovy
1.floatValue()
===> 1.0

# ruby
1.to_f
=> 1.0

// scala
1.floatValue
res1: Float = 1.0

Простота объявления свойств (properties)

В Java для того, чтобы создать свойство вы должны определить поле, getter, setter, и (довольно часто) конструктор, причём везде прописать правильные модификаторы доступа. В Java.next, вы можете определить всё это одним шагом.

; 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) {}

Если вам нужно переопределить конкретный getter, setter или конструктор для класса, вы вполне можете сделать это без необходимости нудно вбивать код для всех остальных, стандартных случаев.

И это не всё! Все эти языки придерживаются принципа «Существует Более Одного Cпособа Cделать Это» (There's More Than One Way To Do It, TMTOWTDI), поэтому существуют несколько вариантов воплощения показанных выше приёмов.

Мощные коллекции

Языки Java.next предлагают удобный синтаксис для самых важных типов коллекций: массивов (arrays) и ассоциативных списков (maps). Вдобавок вы можете связывать между собой несколько операций над коллекциями передавая в качестве аргументов функции, что позволяет отказаться от использования итераторов и циклов. Например, чтобы найти все возведённые в квадрат значения от единицы до десяти, которые нечётны можно сделать следующее:

; clojure
(filter (fn [x] (= 1 (rem x 2))) (map (fn [x] (* x x)) (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)

Аналогичные удобства предоставляются и для работы с парами ключ-значение или, другими словами, хэш-таблицами (hashes) или словарями (dictionaries).

Функциональное программирование.

Удобства коллекций, о которых мы только что рассказали, являются частным случаем более общей идеи: функционального программирования. Java.next поддерживает функции как первоклассные объекты (first-class objects), позволяя передавать их в качестве аргументов, определять функции, создающие новые функции, а так же использовать замыкания (closures). В качестве простого примера рассмотрим создание функции, которая добавляет некоторое значение, определённое во время исполнения (runtime):

; clojure
(defn adder [x] (fn [y] (+ x y)))

// groovy
adder = { add -> { val -> val + add } }

# ruby
def adder(add)
lambda { |x| x + add }
end

// scala
def sum(a: Int)(b: Int) = a + b


Переопределение операторов

В Java вы не можете переопределять операторы. Для типов вроде BigDecimal математические операции выглядят так:

// Java math
balance.add(balance.multiply(interest));

Языки Java.next позволяют вам переопределять операторы. Это даёт возможность создавать новые типы, которые воспринимаются так же как и встроенные. Т.е. вы можете создать типы ComplexNumber или RationalNumber, которые поддерживают операции +, -, *, и /.

; Clojure
(+ balance (* balance interest))

// Groovy
balance + (balance * interest)

# JRuby
balance + (balance * interest)

// Scala (See [1])
balance + (balance * interest)

Легкость в поддержке исключений

Checked exceptions оказались неудачным экспериментом. Код Java из-за них лишь разбухает, ничем особо не способствуя улучшению обработки ошибок. Хуже того, checked exceptions являются настоящей головной болью для поддержке на границе между слоями абстракций. Введение новых типов исключений не должно приводить к перекомпиляции!

Языки Java.next не требуют от вас объявлять checked exceptions или явно обрабатывать checked exceptions, приходящие из другого кода. Хотя то, что другие языки способны игнорировать уродливые checked exceptions языка Java говорит о неожиданной гибкости Java как платформы.

Добавление собственных методов к существующим объектам

В Java вы не можете доблять методы к существующим типам. Это ведёт к проблемам в моделировании объектов, когда разработчикам приходится создавать вспомогательные (utility) классы, которые нарушают принципы ООП:

// 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;
}
}
}


В языках Java.next, вы можете добавлять методы к существующим типам:

; 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)

Создание собственных конструкций языка

В Java у вас есть язык и есть библиотеки. Они фундаментально отличаются: вы можете создавать новые библиотеки, но не можете создавать новые языковые конструкции.

В языках Java.next линия между языком и библиотеками более размыта. Вы можете создавать новые выражения, которые работают как будто они встроены в язык. Например Clojure содержит функцию and:

; clojure
(and 1 2) => 2

Но может быть ваша проблема не настолько проста. Вам нужна функция most, которая возвращает true, если большинство аргументов вычисляются в true. Такой функции в Clojure нет, но вы можете её создать сами:

; clojure
(most 1 2) => true
(most 1 2 nil) => true
(most 1 nil nil) => false

Главная фишка здесь не в том, нужен ли моему языку условный оператор most. Возможно и не нужен. Главное заключается в том, что разные области использования нуждаются в разных возможностях. В языках Java.next граница между языком и библиотеками минимальна. Вы можете подогнать язык под свои потребности вместо того, чтобы искать обходные пути.

Или вот вам другой пример. Рассмотрим синтаксис атрибутов в Ruby:

# Ruby
class Account
attr_accessor :name
dsl_attribute :type
end

attr_accessor встроен в язык. dsl_attribute — это библиотечный метод, который я написал. Он позволяет опускать символ "=" для присвоения значений. Т.е.

# normal attributes
account.name = "foo"

# equals-free attributes
account.type checking

Заключение

Языки Java.next имеют много общего. Хотя я использовал для демонстрации небольшие отдельные примеры, настоящая мощь возникает при использовании всех этих возможностей вместе. Когда они объединяются в языках Java.next, это ведёт к совершенно другому стилю кодирования.
  • Вам не нужно больше кодировать в защитном стиле (defensively), хватаясь за фабрики, паттерны и dependency injection для того, чтобы сохранять ваш код тестируемым и легкоизменяемым. Вместо этого вы создаёте минимальное решение, а потом его развиваете.
  • Вместо прямого кодирования на языке Java.next вы можете создать свой Domain-Specific Language, который лучше всего подходит для вашей проблемной области.

По моему опыту, этот стиль кодирования приводит к уменьшению размера исходников на порядок без снижения читабельности.

Многие люди ждут «Следующий Крутой Язык» (next big language). Этот язык уже здесь. Только это не один язык, а коллекция идей, которые я перечислил (плюс, возможно, те, которые я пропустил), выраженные в языках Java.next.

Заслуживает ли Java.next слова «крутой»? Безусловно. Мой опыт показывает, что переход от Java к Java.next в каждом своём аспекте не менее крут, чем предыдущие тектонические сдвиги в индустрии как с точки зрения кривой обучения, так и с точки зрения выигрыша в производительности.

Работая в индустрии разработки, нам нужно поднять нашу планку и начать изучать Java.next. Потом можно будет обсудить разницу между этими языками. Я постараюсь рассмотреть уникальные особенности языков Java.next в будущих статьях этой серии.

От переводчика: Решил почаще приводить английские термины в скобках, ибо их и предпочитаю. Иногда вообще не перевожу термин, если не знаю удачного варианта. Дайте знать, подходит ли это вам или нет. И будьте снисходительны к моим ошибкам. ;)
Tags:
Hubs:
+47
Comments 133
Comments Comments 133

Articles