Статьи

Scala Journey: идиомы, параллелизм и другие аспекты

Я следил за Скалой время от времени около двух лет. Главным образом в рывках. Мне понравился язык, но из-за загруженности и других приоритетов у меня никогда не было времени взять его на полную прогулку. Ну, за последние 2 недели я решил сделать полный шаг. Я использую высокопараллельное производственное приложение, где энергопотребление является очень важным компонентом нашего приложения, и переписываю его в Scala. Я делаю это не просто для удовольствия.

Это приложение выросло из очень хорошо спроектированного, до того, которое все еще довольно красиво спроектировано, но накопило много технических долгов. Со всем, что я узнал о Scala, я думаю, что смогу изменить его, чтобы он стал чище, более кратким и, вероятно, более масштабируемым. Другая важная причина, по которой я собираюсь дать Scala шанс, связана с параллелизмом на основе актеров. Я много лет работал с потоковыми примитивами Java и накопил отношения любовь / ненависть. Пакет параллелизма JSE 5 принес в мой мир несколько замечательных жемчужин, но он не устранил тот факт, что вы все еще программируете императивную модель синхронизации общего состояния. Актеры Scala скрывают некоторые уродства синхронизации потоков, но не устраняют проблему полностью. Из-за природы Скала,Будучи сочетанием императивного и функционального языка и того факта, что акторы реализованы в виде библиотеки, ничто не мешает столкнуться с теми же проблемами, что и в более простых операциях совместного использования состояний потоков (т. е. состояния гонки, состязания блокировок, взаимоблокировки / живые блокировки).

По сути, если вы используете актеров как просто слой абстракции по сравнению со старыми практиками, вы будете в той же лодке, что и в Java. С учетом всего сказанного, в отличие от Java, Scala предоставляет вам средства для создания более чистых и более поточно-ориентированных систем благодаря своим функциональным возможностям программирования. Изменяемое общее состояние является основной причиной недетерминированности в параллельных приложениях Java, поэтому неизменность и передача сообщений — это путь в более детерминированный мир.

Я также рассмотрел другие модели параллельного программирования, такие как STM / MVCC. STM — это основа параллельного программирования в Clojure и парадигма, отличная от Actors. STM — более простая модель, если вы привыкли программировать старые императивные потоки, поскольку они абстрагируют вас от примитивов параллелизма, заставляя изменения состояния происходить в транзакционном контексте. Когда это происходит, система STM заботится о том, чтобы изменения состояния происходили атомарно и изолированно. На мой взгляд, эта система очень хорошо подходит для многоядерной парадигмы и обеспечивает более плавный переход, проблема с ним, по крайней мере, в контексте MVCC, заключается в том, что для каждой изменяемой транзакции и структуры данных делается копия для изоляция (реализация копирования зависит от системы, некоторые могут быть более эффективными, чем другие),но вы уже можете увидеть проблему. Для системы, которая должна обрабатывать многочисленные параллельные транзакции с участием множества объектов, это может стать узким местом, а создание копий может перегружать память и производительность системы. В мире STM ведутся некоторые споры, в основном связанные с поиском места для подобных систем, где стоимость MVCC менее значима, чем стоимость постоянной синхронизации через блокировку.

Модель акторов отличается, она работает с точки зрения изолированных объектов (актеров), все они работают изолированно путем передачи сообщений. Никто не может изменить или запросить состояние другого, за исключением запроса такой операции, отправив сообщение этому конкретному объекту (субъекту). В Scala вы можете разрушить эту модель, поскольку вы можете отправлять изменяемые объекты, но если вы действительно хотите извлечь выгоду из модели Actor, вероятно, следует избегать этого. Актеры лучше подходят для параллельных приложений, которые не только охватывают несколько ядер, но также могут легко масштабироваться на несколько физических узлов. Поскольку передаваемые сообщения являются неизменяемыми структурами данных, которые можно легко синхронизировать и совместно использовать, базовая система Actor может совместно использовать эти сообщения между физически рассредоточенными субъектами так же, как и для субъектов в одном и том же пространстве физической памяти.

Таким образом, мир параллелизма становится все более захватывающим с этими удивительными парадигмами. Следует помнить, что нет единого размера, подходящего для всей модели параллелизма, и я не вижу, чтобы что-либо из вышеперечисленного стало стандартом де-факто в ближайшее время. Для каждой из них есть приятное место, поэтому нужно изучить все тонкости каждой модели.

Теперь, когда я избавился от параллелизма, давайте вернемся к фактическому синтаксису Scala. Scala очень мощная (по крайней мере, по сравнению с Java). Эта сила приходит с ответственностью. Вы можете использовать Scala для написания красивых / лаконичных программ или использовать его для написания непонятных / неразборчивых программ, которые никто, включая первоначального автора, не сможет понять. Лично я предпочитаю и могу ответственно справиться с этой ответственностью. Я долгое время программист на Perl (еще до того, как я начал программировать на Java), и я видел (и даже писал время от времени) программы, которые сам Ларри Уолл не смог бы понять.

Scala поставляется с перегрузкой оператора, но, если не использовать ее разумно, одна эта сила может быть ответственна за неприемлемость любой системы. Это одна из основных причин, по которой такие языки, как Java, решили ее не включать. Лично я считаю, что перегрузка операторов может быть прекрасным дополнением к любому API. Это может облегчить написание DSL и сделать их более естественными. Опять же, эта сила велика в использовании опытных и ответственных программистов.

Получив большой опыт (Perl) и большую сдержанность (Java), я больше склоняюсь к власти (кто бы не стал :-). С одной стороны, приятно иметь возможность читать и понимать чьи-либо Java-программы, даже если они написаны неправильно, с другой стороны, это трудная попытка написать программу и перепрыгнуть через все обручи и ограничения из-за различных ограничений. , В идеальном мире ИИ компилятор выводит возможности программиста и ограничивает его возможности, основываясь на них, в некотором роде, чтобы никого не обидеть ? Так что, если новичок выводится, ах, происходит перегрузка оператора и неявный преобразования, и т. д. Но сейчас я предпочел бы использовать мощный инструмент, когда я пишу программное обеспечение, и Scala, кажется, нажимает нужные мне кнопки на данный момент.

Я собираюсь начать список постов, начиная с этого, о моем опыте работы со Scala.

Вот кое-что, что я придумал несколько часов назад. Наше программное обеспечение имеет ограниченную совместимость с базой данных SQL и требует легкой абстракции. Мы решили не использовать какие-либо сторонние абстракции ORM или SQL, в основном из-за того, что зависимость от этих абстракций действительно не дает никаких преимуществ для нашего ограниченного использования SQL. Поэтому я разработал простой уровень абстракции варианта SQL, который позволяет нам выполнять запросы SQL, которые определены в реализации SQLVariant. Переход от одной базы данных к другой, просто требует реализации интерфейса SQLVariant для обеспечения надлежащей абстракции. Первоначально я написал это на языке Java, и хотя он был приличным, он требовал немного больше кода и не выглядел так кратко, как мне хотелось. Одной из проблем было PreparedStatement и его интерфейс для привязок заполнителей.Как связать примитивы и типы-оболочки java как заполнители и как SQLVariant узнает, какой метод PreparedStatement.bind * вызвать? Я прибег к использованию перечисления, которое определяет эти операции и отражение с целью вызова этих операций. Я в основном избегаю статической типизации в месте, которое я не уверен, что я действительно хочу или должен. Вот реализация Java.

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

 

  import java.lang.reflect.Method;
import java.sql.*;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

public abstract class SqlVariant {

public abstract SqlSelectStatement getResultsNotYetNotifiedForStatement(NotificationType... types);

public abstract SqlSelectStatement getResultsNotYetNotifiedForStatement(int limit, NotificationType... types);

public abstract SqlUpdateStatement getUpdateWithNotificationsForStatement(Result result);

private abstract static class SqlStatement {

protected String sql;
protected List bindParams = new ArrayList();
protected PreparedStatement stmt;

public SqlStatement(String sql) {
this.sql = sql;
}

public SqlStatement addBindParam(BindParameter param) {
bindParams.add(param);
return this;
}

public String getSql() {
return sql;
}

public List getBindParams() {
return Collections.unmodifiableList(bindParams);
}

protected PreparedStatement prepareStatement(Connection conn) throws SQLException {
PreparedStatement stmt = conn.prepareStatement(sql);
for (int bindIdx = 0; bindIdx < bindParams.size(); bindIdx++) {
BindParameter p = bindParams.get(bindIdx);
try {
Method m = stmt.getClass().getMethod(p.type.method, Integer.TYPE, p.type.clazz);
m.invoke(stmt, bindIdx + 1, p.value);
}
catch (Exception e) {
throw new RuntimeException("Couldn't execute method: " + p.type.method + " on " + stmt.getClass(), e);
}
}
return stmt;
}

public abstract T execute(Connection conn) throws SQLException;
}

public static final class SqlSelectStatement extends SqlStatement<ResultSet> {

public SqlSelectStatement(String sql) {
super(sql);
}

@Override
public ResultSet execute(Connection conn) throws SQLException {
return prepareStatement(conn).executeQuery();
}
}

public static final class SqlUpdateStatement extends SqlStatement<Boolean> {
public SqlUpdateStatement(String sql) {
super(sql);
}

@Override
public Boolean execute(Connection conn) throws SQLException {
stmt = prepareStatement(conn);
return stmt.execute();
}
}


public static final class BindParameter<T> {
private final BindParameterType type;
private final T value;

public BindParameter(Class<T> type, T value) {
this.type = BindParameterType.getTypeFor(type);
this.value = value;
}

public BindParameter(BindParameterType type, T value) {
this.type = type;
this.value = value;
}
}

private static enum BindParameterType {
STRING(String.class, "setString"),
INT(Integer.TYPE, "setInt"),
LONG(Long.TYPE, "setLong");

private Class clazz;
private String method;

private BindParameterType(Class clazz, String method) {
this.clazz = clazz;
this.method = method;
}

private static BindParameterType getTypeFor(Class clazz) {
for (BindParameterType t : BindParameterType.values()) {
if (t.clazz.equals(clazz)) {
return t;
}
}
throw new IllegalArgumentException("Type: " + clazz.getClass() + " is not defined as a BindParameterType enum.");
}
}
}

Теперь вот как можно реализовать интерфейс SQLVariant. Ниже приведена реализация в отличной. Я выбираю groovy, когда мне нужно выполнить много строковую интерполяцию, которую ява и scala отказываются поддерживать. Код был сокращен, чтобы просто продемонстрировать минимум.

  class MySqlVariant extends SqlVariant {
@Override
public SqlVariant.SqlSelectStatement getResultsNotYetNotifiedForStatement(int limit, NotificationType[] types) {
SqlVariant.SqlSelectStatement stmt = new SqlVariant.SqlSelectStatement("SELECT ...")
for (NotificationType t : types)
stmt.addBindParam(new SqlVariant.BindParameter(String.class, t.name().toUpperCase()))
return stmt;
}

@Override
public SqlVariant.SqlUpdateStatement getUpdateWithNotificationsForStatement(Result result) {
SqlVariant.SqlUpdateStatement stmt = new SqlVariant.SqlUpdateStatement("INSERT INTO ....")
result.notifications?.each { Notification n ->
stmt.addBindParam(new SqlVariant.BindParameter(SqlVariant.BindParameterType.LONG, n.id))
stmt.addBindParam(new SqlVariant.BindParameter(SqlVariant.BindParameterType.LONG, result.intervalId))
}
return stmt
}
......
}

Я начал переопределять вышесказанное в Scala и наткнулся на очень мощную и красивую функцию неявного преобразования Scala . Это позволило мне по-настоящему абстрагировать реализации SQLVariant от каких-либо специфических знаний привязок с помощью неявного средства приведения, которое обычно предоставляют только динамически типизированные языки. Scala дает нам эту возможность, но также обеспечивает статическую безопасность типов неявных преобразований во время компиляции.

Еще одна замечательная особенность — это отложенные значения, которые позволяют нам аккуратно реализовать отложенные вычисления, которые мы (программисты Java) так привыкли делать, создавая поле члена как нулевое, а затем проверяя его перед инициализацией при начальном вызове метода доступа. Если вы уже видели код, похожий на приведенный ниже, вы будете рады узнать, что вам больше не нужно делать это в Scala.

public class SomeClass {
private SomeType type;

public SomeType getSomeType() {
if (type == null) type = new SomeType(); // Often more complex than that
return type;
}
}

Вышеупомянутое, помимо того, что оно не идеально, также подвержено ошибкам, если, скажем, тип используется где-то еще в SomeClass, и вы не используете метод доступа для его получения. Вы должны обеспечить использование аксессора по соглашению или иметь дело с тем фактом, что он может быть не создан. Это больше не относится к Scala, поскольку его среда выполнения обрабатывает ленивые экземпляры для вас. Смотрите ниже код.

Примечание. Я по-прежнему разрешаю клиентским абстракциям доступа к данным работать с необработанным ResultSet jdbc, возвращаемым из SQLVariant. Я не рассматриваю это как проблему в данный момент, во-первых, поскольку эти абстракции специфичны для SQL, а также потому, что ResultSet является стандартным интерфейсом для любого взаимодействия JDBC SQL. Вот моя краткая реализация Scala. Я все еще учусь, так что это может измениться, так как я лучше знакомлюсь с идиомами Scala и начинаю писать более идиоматический код Scala.

import javax.sql.DataSource
import java.sql.{ResultSet, Connection, PreparedStatement}
import com.bazusports.chipreader.sql.SqlVariant.{SqlSelectStatement, BindingValue}

abstract class SqlVariant(private val ds: DataSource) {

def retrieveConfigurationStatementFor(eventTag: String): SqlSelectStatement;

protected final def connection: Connection = ds.getConnection
}

object SqlVariant {

trait BindingValue {def >>(stmt: PreparedStatement, idx: Int): Unit}

// This is how implicit bindings happen. This is beauty, we can now
// bind standard types and have the compiler perform implicit conversions
implicit final def bindingIntWrapper(v: Int) = new BindingValue {
def >>(stmt: PreparedStatement, idx: Int) = {stmt.setInt(idx, v)}
}

implicit final def bindingLongWrapper(v: Long) = new BindingValue {
def >>(stmt: PreparedStatement, idx: Int) {stmt.setLong(idx, v)}
}

implicit final def bindingStringWrapper(v: String) = new BindingValue {
def >>(stmt: PreparedStatement, idx: Int) {stmt.setString(idx, v)}
}

abstract class SqlStatement[T](conn: Connection, sql: String, params: BindingValue*) {

// Ah, another beautiful feature, lazy vals. Basically, it's
// evaluated on initial call. This is great for the
// so common lazy memoization technique, of checking for null.
protected lazy val statement: PreparedStatement = {
val stmt:PreparedStatement = conn.prepareStatement(sql)
params.foreach((v) => v >> (stmt, 1))
stmt
}

def execute(): T
}

class SqlUpdateStatement(conn: Connection, sql: String, params: BindingValue*)
extends SqlStatement[Boolean](conn, sql, params: _*) {
def execute() = statement.execute()
}

class SqlSelectStatement(conn: Connection, sql: String, params: BindingValue*)
extends SqlStatement[ResultSet](conn, sql, params: _*) {
def execute() = statement.executeQuery()
}
}

/* Implementation of the SQLVariant */

class MySqlVariant(private val dataSource:DataSource) extends SqlVariant(dataSource) {

def retrieveConfigurationStatementFor(eventTag: String) =
new SqlSelectStatement(connection, "SELECT reader_config FROM event WHERE tag = ?", eventTag)
}

И обязательный модульный тест с использованием ошеломляющей среды Scala Specs.

object MySqlVariantSpec extends Specification {
val ds = getDataSource();

"Requesting a configuration statement for a specific event" should {
"return a SqlSelectStatement with properly bound parameters" in {
val sqlVariant:SqlVariant = new MySqlVariant(ds)
val stmt:SqlSelectStatement = sqlVariant.retrieveConfigurationStatementFor("abc")
stmt must notBeNull
// .... Other assertions go here
}
}
}

 

Несмотря на то, что я едва поцарапал верхушку айсберга, я надеюсь, что это поможет вам увидеть кое-что из того, что может предложить Scala. Больше впереди, как я прогрессирую.

Вы можете увидеть больше сообщений в блоге / информацию в блоге Ильи