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 належить до сім’ї так званих
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 буде залишатись одним з основних каналів зв’язку, що поєднують світи академії та індустрії, темпи її еволюції вражають і найближчі роки будуть цікавими.