Як я проект з 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© {
    //устанавливаем таймаут на начало обмена
    sender.setValidTimeout©;
    //если пришли данные
    c.on('data', function(message) {
        sender.process(message,c);
    });
    c.on('close',function() {
        connection.removeConnection©;
    })
});

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 не простий.
Але для себе я вирішив — не страждати. Навіщо?
    
Джерело: Хабрахабр

0 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.