Как я проект с JavaScript на Scala переписывал

в 6:02, , рубрики: javascript, mongodb, nodejs, play framework, scala, боль, хипстеры

Как я проект с JavaScript на Scala переписывал
Я никогда не смогу ходить! Потому что я ползаю.
—Цитаты великих

    Меня всегда учили прежде всего здороваться, так что — здравствуйте. Сегодня я расскажу про творческие (и не очень) муки, страдания и боль, которые я испытывал на протяжении определенного периода своей жизни, который я обозначу как ПРОЕКТ. Сначала он был на JavaScript (node.js), а теперь он на Scala (Play). Сразу скажу, что я — один из самых субъективных негодяев в обозримой Вселенной, поэтому некоторые обороты, высказывания и иже с ними могут быть восприняты уважаемыми читателями весьма неоднозначно. Короче, я предупредил. И у меня еще одна небольшая просьба — если уж взялись прочитать статью, то не кидайтесь сразу строчить разоблачающие комментарии. Дочитайте. Я не Пастернак, правду говорю. И вообще, почти все спорные моменты так или иначе освещаю, объясняю.

Пролог

    Но для начала я позволю себе небольшое отступление и расскажу, что же я делал, и как давно это началось.
Примерно года полтора назад я как раз стоял перед выбором темы для дипломной работы в моем техническом ВУЗе. Конечно, я мог бы отмазаться каким-то банальным сайтом для автомойки, очередной концепцией нового дизайна для сайта Кофехауза или еще чем-то похожим (кстати, это реальные дипломные проекты, и меня от этого коробит, но опустим). Но мы ведь упертые, да еще и профессионалы в своем деле! Легких путей не ищем и бла бла бла.
    Стоит сказать, что к тому времени у меня за плечами было уже порядка 3 лет именно рабочего опыта и ~6-7 лет просто угорания по программированию, а конкретно — по вебу. Поэтому вопросов о реализации передо мной не стояло. Осталась тема, то есть тот самый ПРОЕКТ. Местные ребята должны знать забавную статью про разработку через страдания.
    Так вот на тот момент я действительно испытывал страдания при совместной работе или изучении очередного ЯП с товарищами. Мне нужен был инструмент на подобии pastebin, т.е. банальный как квадрат (отправил — копипастнул ссылку — поделился), но все же с фишкой Google Docs, а именно — одновременным редактированием кода. Согласитесь — это круто, когда ты просто видишь, как кто-то поставил курсор на косяк и исправил его в два нажатия по клавишам. Ну всяко быстрее, чем один и тот же код дублировать каждый раз, меняться ссылкам. Боль.
     И вот пошарился я по этим вашим интернетам в поисках такого сервиса, и… не нашел. Конечно, я в курсе про плагины к тому же Eclipse, Sublime и т.п., и даже знаю про целые standalone-решения. Но вот что-то простое как pastebin я не нашел. Отсюда и начал свой отсчет мой ПРОЕКТ.

Глава 1. JavaScript

    Из пролога у вас могло уже сформироваться краткое ТЗ, что же из себя представляет нужный сервис, и как его следует исполнить.
Имеем, грубо говоря, некие пользовательские чатики, где вместо чата — код, над которым все и корпеют. Как? Если кратко — не люблю флэш, хочу вебсокеты.
    На тот момент я плотно сидел на PHP, но на нем писать WebSocket сервис, где полнодуплексные соединения могут висеть примерно бесконечность — прямая дорога в Ад. Поэтому я обратил свое внимание на node.js как WS-сервис, а статику генерить и отдавать пыхой. И знаете, что? Это было круто. Прототип я накидал буквально за пару дней. Все работало, и это было непередаваемое ощущение. Будто ты только что до конца осознал теорию струн. Или догадался, куда пропадает информация в черной дыре. Ну вы меня поняли, да? А тогда как раз еще вышел релиз nginx'a, который умел в проксирование WebSocket.

Ляпота

//Кусок кода, который отвечал за прием сообщений и контроль соединения. Довольно кратко и доступно.
var sockjs_im = sockjs.createServer(sockjs_opts);
sockjs_im.on('connection', function(c) {
    //устанавливаем таймаут на начало обмена
    sender.setValidTimeout(c);
    //если пришли данные
    c.on('data', function(message) {
        sender.process(message,c);
    });
    c.on('close',function() {
        connection.removeConnection(c);
    })
});

var server = http.createServer();

server.addListener('upgrade', function(req,res){
    res.end();
});

sockjs_im.installHandlers(server, {
    prefix: config.get("path")
});

exports.startWSServer = function() {
    server.listen(2410, '0.0.0.0',function() {
        console.log(' [*] Listening WS on 0.0.0.0:2410' );
    });
}

    Но вернемся к делу. Я схватил этот молоток и начал колотить по всему вокруг, а там уже пусть сам разбираются — кто гвоздь, а кто верблюд. Взял самые хипстерские технологии: SockJS как клиент-сервер под WS-соединения, MongoDB как, вы не поверите, базу данных, Ace Editor как редактор на клиенте, слепил их и начал писать обвязку, логику.

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

    Месяц спустя после очередной банки энергетика я, оторвав красные глаза от экрана, понял, что на свет родился монстр, тварь, нечто, что лучше бы исдохло сразу. Не, оно работало, без сбоев, функционал был почти весь готов. Но то, как оно работало — внушало священный ужас. Я совершил фатальную ошибку и не переписал прототип. Я его нарастил. Код стал слиииишком сложен и избыточен.
    С этого момента работа над проектом превратилась в пытку. Добавление новой функции или фишки требовало немалого усилия. Я чувствовал себя Сизифом, только камень был еще и квадратным. Страдания.
    Логичный вопрос — что ж ты такого там наговнокодил? Вот небольшой список:

  • Callback hell
  • Попытка писать ООП на JS с его prototype
  • … отсюда попытка сделать уберабстракцию над абстракцией
  • … а пока ты абстрагируешься — уже приходит седьмой, мать его, круг callback-ада и ты здороваешься за руку с богохульниками и содомитами (как же был прав Данте!)

    И вот подходит ко мне Минотавр, который охраняет пояса седьмого круга и говорит:
    -Эй, парень. Ну ты же сам дурак. Написал чертовщину, ногу сломишь, сам же сознался. В чем твоя проблема?
И знаете, что я отвечу? Это все JavaScript. То есть не поймите неправильно, я не имею ввиду, что я такой милый и пушистый с iq >>> бесконечности. Я имею ввиду то, что язык сам подталкивает тебя писать именно так, а не иначе. Эдакий змий, который шепчет:
    -Да ладно, браток, воткни-ссс здесь быстренько третий колбэк в аргументы, ничего не убудет-ccc....
И такой длинный красный язык перед глазами.

Выглядело все это примерно так


/**
* Загрузка сессии. а точнее - передача колбэка в загрузчик сессии...
*/
var loadSession = function(sessid, callback) {
    if(typeof sessid != 'string') return;

    var sess = sessions.getSession(sessid);

    sess.setLoadCallback(callback);
}

...

/**
* Загрузим сессиию, получим пользователя, попробуем получить комнату, потом попробуем туда пихнуть юзера.
* Все в колбэки-колбэки, а там внутри еще колбэки.
*/
loadSession(msgObj["sessid"], function(sess) {
        var r = rooms.getRoom(msgObj["room"]);

        r.setLoadCallback(function() {
            r.addUser(connection, sess, function(newUser) {
                if(newUser!==false) {
                    var outMsg = {
                        action: "newUser",
                        data: newUser
                    }
                    r.broadcast(connection, outMsg);

                    r.write(connection,{
                        action: "join"
                    });
                }else{
                    error(connection,{
                        error:true,
                        errsmg: "something went wrong"
                    })
                }
            });
        });
    });
...

    Возможно, это все энергетики, и никакой минотавр со змием ко мне и не приходили (приползали), но получилось то, что получилось. Диплом был сдан, но тварь жить осталась, если это можно было назвать жизнью.
     После нескольких попыток отрефакторить это чудовище или вообще переписать все на coffeescript, я забросил всю эту чертовщину до лучших времен и уехал кататься по Европе. Дааа, так глубоко я пал и так далеко бежал от кошмаров прошлого!

Глава 2. Scala

    Прошло полгода. Чудище все существовало, а у меня не было никакого желания его добить и выпустить уже в продакшн. Открывая код на JavaScript мне приходилось срочно бежать за металлическим тазиком, который со звоном использовался под рвотные массы.
    И тут, заблудившись в интернете, я оказался на сайте Play Framework. Уже не помню, что же меня привлекло и задержало на сайте, да и важно ли? В итоге, через день я уже копался с фреймворком, писал первое приложение и записался на курс Scala на coursera.org.
    Не могу сказать, что было просто, особенно по началу. Конечно, питон или пыха попроще, но имея бэкграунд на Qt/C++ и Java я разобрался в скалке довольно быстро, по крайней мере в основных моментах. Чтобы вкурить в неявные преобразования и параметры, ко/контр/инвариантность и иже с ними понадобилось поднапрячь свой гугл-скилл в поисках различных примеров и документации, дабы составить общую картину происходящего где-то там, под капотом. И все же какое-то время я чувствовал себя тупым валенком, хотя есть мнение, что это нормально.
    И вот, немного набив руку, я решил посмотреть, как Play умеет в вебсокеты. И вот тут меня словно ушатом с ледяной водой окатили. Первая реакция была простой — WTF??? Куда делись милые и доступные решения, что это за функционалохардкор с околонизкоуровневыми Iteratee/Enumeratee? Да, ничего общего с той ляпотой на JavaScript. Верните мне мои push и onMessage!

Хрен тебе, а не каналы

def index = WebSocket.using[String] { request => 
  
  // Just consume and ignore the input
  val in = Iteratee.consume[String]()
  
  // Send a single 'Hello!' message and close
  val out = Enumerator("Hello!").andThen(Enumerator.eof)
  
  (in, out)
}

    Однако, будучи наученным горьким опытом слишком простых решений, я решил не сдаваться и снова взял в руки… Нет, не молоток. Гугл. В итоге нашелся приятный подход через

Concurrent.unicast
val promiseIn = Promise[Iteratee[String, Unit]]()
        val out = Concurrent.unicast[String](
          onStart = onStart(promiseIn, r, userSession),
          onError = onError
        )
(Iteratee flatten (promiseIn.future), out)

...

private def onStart(promiseIn: Promise[Iteratee[String, Unit]], ...): (Channel[String] => Unit) = {
...
    (ch: Channel[String]) => {
      val channel = new ChannelContainer(ch)
      for (optUserConnection <- isConnectedF(r, channel)) yield {
        optUserConnection match {
          case Some(userConnection) => {
            val in = Iteratee.foreach[String] {
              MessageController onMessage (r, userConnection, channel) //bind handler for room and this connection
            } map {
              MessageController onDisconnect (r, userConnection, channel) //handler for disconnect
            }
            promiseIn success in //success promise and fill it with iteratee
          }
          case None => channel eofAndEnd //in case of some troubles with new user creation - close connection
        }
      }
    }
}

    Да, согласен. Смотрится все же это не так мило и весьма громоздко, но позволяет использовать каноничные каналы, создающиеся для обмена сообщениями, запихивать эти каналы в обертки, передавать их сообщениями в акторы… Акторы!
    Акторы в Scala. Рахат-лукум моего сердца. BEST PARADIGM EVAH! Ну или уж точно то, что надо для моих целей. Комната — актор, менеджер комнат — актор. И даже клиенты — акторы. Логично вливается в идеологию об обмене сообщениями между пользователями. Кстати, разработчики Play тоже прочухали эту тему, и начиная с недавно вышедей версии 2.3

WebSocket-соединения теперь тоже акторы

import play.api.mvc._
import play.api.Play.current

def socket = WebSocket.acceptWithActor[String, String] { request => out =>
  MyWebSocketActor.props(out)
}

import akka.actor._

object MyWebSocketActor {
  def props(out: ActorRef) = Props(new MyWebSocketActor(out))
}

class MyWebSocketActor(out: ActorRef) extends Actor {
  def receive = {
    case msg: String =>
      out ! ("I received your message: " + msg)
  }

    И это прекрасно, когда Вселенная тебя слышит. Мои сигналы таки отразились от какой-то поверхности в световых годах отсюда, на планете бабочек, какающих радугами, и прилетели обратно. Кванты таки запутались, Алиса и Боб нашли друг друга. А я обрел спокойствие.
    А почему? Потому что Scala в большинстве случаев просто смотрит на тебя как на говно, если ты делаешь что-то не по правилам. Она как бы говорит тебе:
    -Парень, я даю тебе нативную поддержку Future в виде монад, JSON сообщения кастую в нужные тебе инстансы case class'ов, предоставляю возможность наследования множества трейтов, я проверяю тебя, слежу за каждым чихом при компиляции, не играй со мной в игры. Делай нормально или возвращайся в свой Ад к Минотаврам.
    Строгий, но справедливый компаньон. Бородатый Хайзенберг программирования. Yea, Scala, biaaatch! А в спину тебя буравит глазами сам Мартин Одерски.

Эпилог

    Итак, что можно сказать после столь сумбурного потока водысознания, каков вывод?
    Первое. Я не сравниваю языки. Упаси макарон. Сравнивать Scala и JavaScript — это как сравнивать льва и бабушкины пирожки. Да, вы можете съесть бабушкины пирожки и будете сыты. А вот льва — вряд ли. Зато лев может съесть вас. Это разные языки для разных задач. Кто будет писать на Scala фронтенд?
    Второе. Я сравниваю возможности, даже вернее — подходы, которые предоставляют языки и платформы, используемые в решении конкретных задач. Господа, но колбэки не компонуются! Это гребаный приговор.
    Я могу взять и сделать в одну строку из десятка Future один единственный и ждать его исполнения. Просто потому, что Future — это монада, все выглядит просто и естественно, в духе языка. Для колбэков мне придется писать обертку, которая либо будет считать количество окончившихся функций, либо станет полноценным Deferred/Promise. Да, да, я знаю про существование подобных библиотек для JavaScript. Но это же просто раздутые обертки к тем самым колбэкам. Замкнутый адкруг.
    Третье. Написал бы я сейчас на JavaScript лучше, чем тогда? Несомненно. Это, возможно, даже выглядело и работало бы попроще. Опыт в решении задач одного типа — это опыт, его не пропьешь. Вышло бы лучше, чем на Scala? Не факт. Да, на Scala местами приходится писать гораздо больше кода, но я, черт возьми, уверен в нем! Мне не нужно писать сотни тестов на один метод, который принимает объект из JSON'a, чтоб убедиться, что все идет так, как я задумал. Что какой-то скрипт-кидди не подставил строку вместо массива, массив вместо числа и т.п. За меня это сделает язык, платформа, компилятор.
    Четвертое. Scala сложна, Scala сложна, Scala сложна. Тысячи их. Бла бла бла. Знаете, что сложно? Держать в голове контекст this и еще сотни аспектов, которые постоянно мытарствуют по нервным узлам труъ JavaScript-нинзя. Серьезно, что бы писать большой проект с кучей логики на JSe, нужно быть гораздо большем спецом, чем на Scala. Я это ощутил на своей заднице. Уважения тем, кто каждый день идет в этот неравный бой. Вы круты, без шуток. JavaScript не прост.
    Но для себя я решил — не страдать. Зачем?

Автор: ImLiar

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js