×Закрыть

Scala 3: як зміниться синтаксис, система типів і застосування мови

Привіт, я — Руслан Шевченко, підприємець, один із засновників групи користувачів Scala в .UA.

Я починав працювати зі Scala з версії 2.7 понад 10 років тому і з того часу беру участь у житті Scala-спільноти. З одного боку, у спільноті немає дефіциту інформації на цю тему, на ScalaUA майже половина доповідей про Scala 3, а з іншого — ми добре знаємо, що відбувається у нашій «бульбашці», але чи видно це зовні?

Наступний реліз [Dotty] буде відрізнятись від нинішнього релізу Scala, мабуть, більше, ніж нинішній відрізняється від Scala 2.7.

У цій статті спробую розповісти про найближче майбутнє Scala тим, хто не є резидентом «бульбашки».

Чого чекати від Scala

Сьогодні мова Scala найбільш поширена в інфраструктурі проєктів обробки потоків даних. Головні killer applications — Spark, що дає змогу обробляти великі об’єми даних, і Kafka, що організує інфраструктуру брокера обміну повідомлень. До речі, 21 квітня компанія Confluent, що стоїть за Kafka, підняла 250 мільйонів доларів у раунді інвестицій. Також Scala часто використовується для організації софт-реалтайм процеcингу, прикладом є рекламні аукціони або обробка платежів.

Як для мови «загального призначення», у Scala зависокий вхідний бар’єр: якщо треба взяти щось із бази даних і показати на фронтенді, то починати з пошуку вільних Scala-розробників буде не найбільш оптимальним шляхом. Можливо, зусилля EPFL змінять це співвідношення.

Нижче — карта доповіді Мартина Одерського Scala 3 Update з конференції ScalaLove, що відбулась 18 квітня.

Реліз наступної версії Scala заплановано на кінець 2020 року. Вона міститиме багато змін. Розповісти про всі в одній статті складно, тому окреслю лише найважливіші.

Cинтаксис

Почнемо з найпростішої та водночас найбільш обговорюваної частини — змін у синтаксисі. Scala належить до сім’ї так званих C-подібних мов, де конструкції відділяються одна від одної за допомогою фігурних дужок. Наприклад:

trait Handler  {

   def  apply(request:Request): M[Reply] = {
        if (authorized(request)) {
             val context = newContext()
             process(request, context)
        } else {
            process(request, PublicContext)
        }
   } 

}

У Scala 3 можна писати по-старому, а можна довірити розставляти дужки компілятору. Тоді цей код матиме такий вигляд:

trait Handler:

   def  apply(request:Request): M[Reply] = 
        if authorized(request) then
             val context = newContext()
             process(request, context)
        else 
            process(request, PublicContext)

Компілятор сам розставить дужки, якщо побачить набір рядків, вирівняних з однаковим відступом після конструкцій управління або кінцевої двокрапки в рядку. (Деталі дивіться тут).

Я користувався новим синтаксисом і можу підтвердити, що таким чином код справді здається «чистішим». Після тижневого користування пишеш його автоматично з дужками, а потім так само автоматично ці дужки видаляєш.

Система типів

Тепер поговорімо про малопомітну, але складну частину — теоретичні основи. Традиційно в об’єктно-орієнтованих мовах програмування систему типів створювали не на основі формальної теорії, а прагматично, на основі наявних практик програмування. Формалізація йшла як доповнення уже згодом. У результаті в нинішніх об’єктно-орієнтованих мовах є різні властивості, що здаються дивними з математичної точки зору. Наприклад, така програма:

object unsoundMini {
  trait A { type L >: Any}
  def upcast(a: A, x: Any): a.L = x
  val p: A { type L <: Nothing } = null
  def coerce(x: Any): Nothing = upcast(p, x)
  coerce("Uh oh!")
}

В Scala 2 програма компілюється і проходить перевірку типів, але видає ClassCastException при запуску. Тобто система типів не є обґрунтованою: існує можливість побудувати такий тип об’єкта, для якого неможлива реалізація. (Докладніше про необґрунтовані системи типів у Java та Scala можна прочитати тут).

Scala 3 ґрунтується на DOT-численні (Dependend Object Types), для якого доведено властивість обґрунтованості: тобто якщо програма пройшла тайпчекінг, ClassCastException під час запуску не буде. Система типів стала розгалуженішою: з’явилися операції перетину та об’єднання типів; за допомогою типів зіставлення та лямбда-типів вирази над типами можна виконати прямо. Система стала і більш регулярною, оскільки там, де раніше треба було вибудовувати ланцюжки імпліцитів, тепер можна написати типові обчислення на зразок:

type MyCollection[T] = hasOrd[T] match 
                                       case  Nothing => HashMap[T]
                                       case  other => TreeMap[T]

Ще одна важлива зміна — Null перестав бути підтипом будь-якого типу посилання. Тобто якщо в нас є клас Person, ми не можемо використовувати значення Null як його екземпляр. А об’єкти, що приходять з Java, мають тип Person | Null. Це дає змогу статистично гарантувати відсутність Null Pointer Exception.

Ергономіка навчання

Ще одна галузь, яка стала напрямом змін. У EPFL (інституті, де розробляється мова Scala) проаналізували, які труднощі виникають у студентів під час вивчення Scala, і змінили мову так, щоб їх стало менше — ввели всі необхідні конструкції більш зрозуміло.

Зокрема, у Scala 2 одним із фундаментальних механізмів є implicit-значення, яке використовують практично всюди. Наприклад, для передачі та синтезу контексту:

def sort[T](list: List[T])(implicit ord: Ord[T])

implicit object PersonOrd extends Ord[Person] {
     def compare(x:Person, y:Person) = 
        implicitly[Ord[String]].compare(x.lastName, y.lastName)
}

іmplicit def listOrd(v:List[T])(implicit elemOrd:Ord[T]):  Ord[List[T]]

Проте засвоєння концепції універсального неявного значення є досить складним для студентів. У Scala 3 цю концепцію змінили: є given-значення, яке може синтезуватись автоматично та передаватися в using clauses для використання. Основна відмінність від implicit — таку концепцію краще пояснювати:

def sort[T](x:List[T])(using Ord[T])   

given Ord[Person]:
     def compare(x:Person, y:Person) = 
        summon(Ord[String]).compare(x.lastName, y.lastName)

given Ord[List[T]](using Ord[T]):
     def compare(x:List[T],y:List[T]) = ….

Для розширення класів ввели спеціальний синтаксис:

extension on x:T (using ord:Ord[T]):

    def  < (y:T) = ord.compare(x,y) 
   …

Замість:

implicit class OrdOps[T](x: T) {

    def  < (y: T)(implicit ord: Ord[T]) = ord.compare(x,y) < 0
    …

} 

Такі зміни сприяють зменшенню кількості boirterplate-коду.

Також студентам заважає варіативність — коли одну й ту ж річ можна зробити по-різному за відсутності будь-яких правил вибору. Тому в Scala 3 цю варіативність намагаються зменшити й рекомендувати для кожного патерну, де це можливо, варіант за замовчуванням.

Метапрограмування

У Scala 2 макроси фактично давали доступ програмісту до нутрощів компілятора: програмісти отримували ті самі типи внутрішнього представлення дерев, що використовувались у компіляторі. Це означало, що макроси залежали від деталей реалізації компілятора, котрі могли змінюватись від версії до версії. І щоб нормально орієнтуватись в Macro API, потрібно було прочитати частину компілятора.

В Scala 3 вибудували рівень ізоляції: дерево програми представлено за допомогою Tasty API, що не міняється під час зміни версії компілятора. І сама робота з макросами стала простішою. Далі таке представлення коду на рівні дерев насправді потрібно не для всіх макросів, тому в API виділили набір ще простіших інтерфейсів. Як-от quotes, де можна писати та аналізувати Scala-вирази у Scala-синтаксисі, навіть не знаючи, як вони транслюються в дерева. Також з’явилось API стейджингу, що дає змогу легко вбудувати компілятор у свій проєкт і генерувати код на Scala, який можна одразу переводити в байт-код і запускати.

Є багато цікавих застосувань Scala-метапрограмування, що відкривають нові можливості для екосистеми. Наприклад, наразі я займаюся побудовою інтерфейсів асинхронного програмування (проєкт на Github), що дасть змогу використовувати Scala як мову програмування загального призначення навіть у відносно простих задачах, де раніше застосування Scala було схоже на стрільбу з гармати по горобцях.

Differentiable programming

Хочеься розказати про ще одне потенційне застосування метапрограмування — диференційоване програмування (Differentiable programming), де програміст пише параметризовану функцію, а набори макросів автоматично генерують похідну цієї функції та все необхідне для градієнтної оптимізації. Класичний приклад — з простого перемноження кількох матриць автоматично генерується алгоритм оберненої пропагації нейронної сітки. Разом із системою ретаргетингу виконання коду на чомусь типу TensorFlow це відкриває новий вимір можливостей, де експериментування з різними архітектурами систем машинного навчання стає набагато зручнішим.

Популярність і використання

Що можна сказати про подальшу популярність Scala? Давати прогнози — невдячна справа. Однозначно у сфері інфраструктури обробки даних Scala буде однією з найважливіших мов дуже довго. Оскільки ця галузь зростає, то й застосування Scala загалом буде збільшуватись. Проте інфраструктурні проєкти рідше віддають на сторонню розробку, тому я не впевнений, що це буде видно з позиції нашої аутсорсингової індустрії.

Чи будуть нові сфери застосування — тут багато залежить від того, як нововведення до Scala 3 демократизують криву навчання. Загалом настрій у спільності оптимістичний: у підсумку люди вибирають технології, а опанувавши Scala, важко не стати її палким прихильником.

У підсумку

Як бачимо, кількість змін у Scala 3 робить її ледь не іншою мовою. Перехід екосистеми буде відбуватися поступово, в Dotty є можливість використовувати версії бібліотек для Scala 2, а також опція підтримки старого синтаксису.

Експериментувати з цим можна вже зараз: на сайті Dotty є все необхідне.

Відомо, що після Scala 3 буде дослідження в царині систем ефектів. Також варто звернути увагу на розвиток бекенду для не-jvm платформи (scala-js та scala-native).

Загалом я думаю, Scala буде залишатись одним з основних каналів зв’язку, що поєднують світи академії та індустрії, темпи її еволюції вражають і найближчі роки будуть цікавими.

LinkedIn

5 комментариев

Подписаться на комментарииОтписаться от комментариев Комментарии могут оставлять только пользователи с подтвержденными аккаунтами.

Про поточну ситуацію з розвитком мови Scala є також непогана стаття від одного з її контриб’юторів Li Haoyi (на хабрі десь навіть був переклад).

Вот единственное чего жаль из того что сделали за бортом — это невозможность писать компиляторные плагины до работы тайпера. А некоторые популярные плагины были как раз из таких.

(вобще в формулировке: «хотите написать плагин что-то меняющий в типах — будьте добры сами распространять свою версию компилятора», это не так уж и непонятно)
С другой стороны — так и видится, что кто-то напишет sbt плагин, загружающий патч к компилятору, отменяющий запрет вызова research plugin-ов в релизных версиях

Дякую за статтю! З нетерпінням чекаю Scala 3 у продакшені)

Подписаться на комментарии