Как стать автором
Обновить

Scala как первый язык

Время на прочтение12 мин
Количество просмотров56K

Эта статья не похожа на то, что обычно публикуют на Хабре.  Здесь не объясняется никаких новых или старых концепций, я не рассказываю, что такое ООП и функциональное программирование, сложного кода нет. Просто хочу пригласить вас к дискуссии стоит ли начинать программировать с языка Scala.

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

Меня иногда просят научить программированию или помочь с поиском подходящего курса. Я хорошо понимаю недостатки и сложности со скалой, но думаю, что Scala 3 — подходящий язык, для того чтобы начать обучение с него, если правильно составить курс. Вся языковая мощь и сложность, конечно, не нужны начинающему программисту, но ему не нужно начинать с самого сложного. На скале можно писать вполне простой и понятный код, постепенно наращивая инструментарий. К тому же сложностей становится все меньше благодаря усилим создателей языка.

Лестница в логотипе отсылает к институту EPFL в Лозанне. Там придумали этот язык
Лестница в логотипе отсылает к институту EPFL в Лозанне. Там придумали этот язык

Обновление: В 2024 меня по-прежнему просят посоветовать, с какого языка начать программировать. И я ни разу не предложил скалу, а предлагал наоборот Python. Ну вы понимаете. Казалось бы, зачем тогда читать эту статью? Затем, что если начать со скалы, то действительно можно хорошо научиться программировать. Например, в универе. Или в детском/подростковом возрасте. Когда человек прежде всего хочет хорошо разобраться и выстроить стройную систему, а не бежать зарабатывать деньги прям сегодня. А разобравшись в скале программист вряд ли об этом пожалеет. И зарабатывать тоже будет очень неплохо. Обо всем этом — дальше.

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

Сначала покажи мне свой код!

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

Это любимая мной со школьных времен игра "Жизнь" (клеточный автомат) Джона Конуэя. Если вы не слышали, то это очень интересная штуковина.

Это полностью рабочий код всего приложения вместе с пользовательским интерфейсом, запускается в браузере. Посмотреть, как этот код работает, можно тут.

import org.scalajs.dom
import org.scalajs.dom.CanvasRenderingContext2D
import scala.scalajs.js.Date

@main def hello =
  val prefix = if scalajs.LinkingInfo.isWebAssembly then "wasm" else "js"
  val canvas = dom.document
    .getElementById(
      s"${prefix}_canvas"
    )
    .asInstanceOf[dom.HTMLCanvasElement]

  val tickButton = dom.document.getElementById(s"${prefix}_tick")
  val playButton = dom.document.getElementById(s"${prefix}_play")
  val perf = dom.document.getElementById(s"${prefix}_perf")

  val ctx = canvas.getContext("2d").asInstanceOf[CanvasRenderingContext2D]
  val side = canvas.width
  val gridWidth = 1
  val cells = 30
  val cellSidePixel = side / cells

  ctx.fillStyle = "black"
  val actualSide = cells * cellSidePixel
  ctx.fillRect(0, 0, actualSide, actualSide)

  def rectStart(row: Int, col: Int) =
    val y = row * cellSidePixel
    val x = col * cellSidePixel

    (x, y)

  def renderLive(row: Int, col: Int) =
    val (x, y) = rectStart(row, col)

    ctx.fillStyle = "white"
    ctx.fillRect(x, y, cellSidePixel, cellSidePixel)

  def renderDead(row: Int, col: Int) =
    val (x, y) = rectStart(row, col)

    ctx.fillStyle = "black"
    ctx.fillRect(x, y, cellSidePixel, cellSidePixel)

  var which = 0

  val buffers = Array.fill(2)(Array.fill(cells * cells)(false))

  def offset(row: Int, col: Int) = row * cells + col

  def isLive(row: Int, col: Int) = active(offset(row, col))
  def willSurvive(row: Int, col: Int) = inactive(offset(row, col))

  inline def active = buffers(which)
  inline def inactive = buffers((which + 1) % 2)

  def rotateBuffer() =
    which = (which + 1) % 2

  def render() =
    for
      row <- 0 until cells
      col <- 0 until cells
    do
      if isLive(row, col) then renderLive(row, col)
      else renderDead(row, col)

  var generations = 0

  def draw(s: String, row: Int, col: Int) =
    process(s, row, col)
      .map(offset(_, _))
      .foreach: offset =>
        active(offset) = true

  draw(glider, 5, 5)
  draw(blinker, 1, 1)
  draw(square, 10, 10)
  draw(beacon, 15, 10)
  draw(rPentomino, 22, 15)

  render()

  def countNeighbours(row: Int, col: Int) =
    var live = 0
    for
      nRow <- (row - 1) to (row + 1)
      nCol <- (col - 1) to (col + 1)
      if nRow >= 0 && nRow < cells && nCol >= 0 && nCol < cells && (row != nRow || col != nCol)
    do if isLive(nRow, nCol) then live += 1

    live

  def makeDead(row: Int, col: Int) =
    inactive(offset(row, col)) = false

  def makeAlive(row: Int, col: Int) =
    inactive(offset(row, col)) = true

  def make(row: Int, col: Int, value: Boolean) =
    inactive(offset(row, col)) = value

  def tick() =
    val t0 = Performance.now()
    val buffer = inactive
    for
      row <- 0 until cells
      col <- 0 until cells
    do
      val alive = isLive(row, col)
      val aliveNeighbours = countNeighbours(row, col)

      if alive && aliveNeighbours < 2 then makeDead(row, col)
      else if alive && (aliveNeighbours == 2 || aliveNeighbours == 3) then
        makeAlive(row, col)
      else if alive && aliveNeighbours > 3 then makeDead(row, col)
      else if !alive && aliveNeighbours == 3 then makeAlive(row, col)
      else make(row, col, alive)

    generations += 1

    rotateBuffer()
    render()
    val t1 = Performance.now()

    val fmt = f"${(t1-t0).toInt}%4d"

    perf.innerText = s"[${fmt}ms per generation]"
  end tick

  enum GameState:
    case Paused
    case Running(timeout: Int)

  var prevGameState = Option.empty[GameState]
  var gameState = GameState.Paused
  val onClick: scalajs.js.Function1[dom.Event, ?] = _ => tick()
  tickButton.addEventListener("click", onClick)

  def handle(state: GameState) =
    state match
      case GameState.Paused =>
        tickButton.removeAttribute("disabled")
        playButton.innerText = "Play"
        prevGameState.collect:
          case GameState.Running(timeout) =>
            dom.window.clearTimeout(timeout)
      case GameState.Running(timeout) =>
        tickButton.setAttribute("disabled", "true")
        playButton.innerText = "Stop"

  val onPlayClick: scalajs.js.Function1[dom.Event, ?] = _ =>
    gameState match
      case GameState.Paused =>
        prevGameState = Some(gameState)
        gameState = GameState.Running(dom.window.setInterval(() => tick(), 100))
        handle(gameState)

      case GameState.Running(timeout) =>
        prevGameState = Some(gameState)
        gameState = GameState.Paused
        handle(gameState)

  playButton.addEventListener("click", onPlayClick)
  handle(gameState)

val glider =
  """
|  *
|* *
| **
""".trim.stripMargin

val square =
  """
|**
|**
""".trim().stripMargin

val blinker =
  """
|***
""".trim().stripMargin

val beacon =
  """
|**
|**
|  **
|  **
""".trim().stripMargin

val rPentomino =
  """
| **
|**
| *
""".trim().stripMargin

def process(s: String, row: Int, col: Int) =
  val coords = List.newBuilder[(Int, Int)]
  s.linesIterator.zipWithIndex.toList.foreach: (line, idx) =>
    line
      .toCharArray()
      .zipWithIndex
      .foreach: (char, offset) =>
        if char == '*' then coords.addOne((row + idx, col + offset))

  coords.result()

import scalajs.js

@js.native
trait Performance extends js.Object:
  def now(): Double = js.native

@js.native
@js.annotation.JSGlobal("performance")
object Performance extends Performance

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

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

Умный компилятор

У скалы один из самых умных компиляторов. Мы можем отдать компилятору очень много работы по анализу кода, проверке его на безопасность и отсутствие ошибок, что очень важно для программиста. Это очень помогает в разработке и делает ее безопасной.

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

(Сложный компилятор раньше приводил к медленным билдам, и за это скалу хейтили. А зря.)

Большая экосистема

Скала совместима с кодом, написанным на языке java и использует все преимущества виртуальной машины этого языка. На джаве написаны миллионы строк кода, которые вы можете использовать из скалы.

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

Вот исследование производительности виртуальной машины джава (JVM) в мобильных устройствах. В нем чаще побеждает нативный код на Си, но в одном случае победила JVM: Java vs C app performance.

На Android 6 AArch64 JVM победила Си в математических вычислениях, хотя и проиграла во всех других видах
На Android 6 AArch64 JVM победила Си в математических вычислениях, хотя и проиграла во всех других видах

Кстати, Джеймсу Гослингу скала тоже понравилась еще в 2008 году.

«Если бы я выбирал сегодня какой-то язык кроме Java, то это была бы Scala». 2008 год
«Если бы я выбирал сегодня какой-то язык кроме Java, то это была бы Scala». 2008 год

К сожалению, скала наследуют от джавы не только хорошее. Но плюсы от использования JVM перевешивают такие случаи.

Многопоточность

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

Еще про виртуальную машину

Важная часть виртуальной машины Java — это модель памяти для многопоточных вычислений (Java Memory Model). Она формализована, непротиворечива, и проверенная многими годами парадигма, на которую потрачено много усилий. На ее основе строятся надежные высокоуровневые абстракции

Эволюция или революция?

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

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

К тому же Scala 3 умеет сочетать код на разных версиях языка и эта проблема теперь касается небольшого количества проектов.

Техническое объяснение

Это стало возможно благодаря типизированному абстрактному синтаксическому дерево компилятора TASTY, которое позволит взаимодействовать классам, собранным разными версиями компилятора. То есть в вашем проекте могут быть jar-файлы, от разных версий скалы начиная с 2.14.

Scala 3

Первоначальным намерением при создании скалы было сделать язык, на котором будет приятно программировать. Это действительно был большой шаг вперед.

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

Теория в основе новой версии языка

Dependent Object Types — это теоретические основы языка Scala 3, разработанные его основателем. Мало какой язык может похвастать математически точным исчислением, лежащем в его основе. Согласитесь, приятно учить язык с таким бэкграундом.

Мартин Одерски
Мартин Одерски

Кстати, основателя языка зовут Мартин Одерски.

Выразительность и лаконичность

Вообще-то с этого надо было начинать. То, что я больше всего люблю в языке — это его сжатость и лаконичность. Я ненавижу длиннейшие конструкции джавы, в которых приходится сложно и многословно объяснять компилятору простые вещи. Моя любимая шутка про джаву — что это такой язык, в котором one liner (однострочный код) занимает около 30 строк. Так вот, в скале one liner — это one liner.

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

Конечно, лаконичность не должна наносить ущерб читаемости кода

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

Мой любимый пример для кода. Он немного парадоксален для начинающих
/** VariancesTest
  */
class Stack[+A]:

  def push[B >: A](elem: B): Stack[B] = new Stack[B]:
    override def top: B = elem
    override def pop: Stack[B] = Stack.this
    override def toString = s"$elem ${Stack.this}"

  def top: A = sys.error("no element on stack")
  def pop: Stack[A] = sys.error("no element on stack")
  override def toString = ""
end Stack

println(Stack().push("hello").push(Object()).push(7))

В тертьей версии языка есть возможность не писать фигурные скобки, если нет такого желания.

Статическая типизация

По-моему это самое важное, что должно быть в первом языке программирования. Программисту важно понимать, что не стоит складывать яблоки с апельсинами.

Именно по этой причине на мой взгляд не стоит начинать обучение с Питона, Руби, Джаваскрипта, Си и Си++. Динамическая типизация — это верный способ выстрелить себе в ногу.

Скала позволяет избавиться от трудностей, с которыми могут сталкиваться программисты при изучении языков со строгой статической типизацией.

ООП

Объектно-ориентированное программирование — это самая популярная парадигма, без которой не обходится ни один современный язык программирования.

В скале эта парадигма изначально присуща языку

Удивительно, что в Питоне, Си/Си++ и в каком-то смысле джаваскрипте ООП появилось как надстройка. Разумеется, сейчас у этих языков с ООП полный порядок.

Функциональное программирование

Функциональное программирование — это многообещающий подход. Скале оно присуще в не меньшей степени чем ООП, и они взаимно обогащают друг друга благодаря этому языку. Рискну предположить, что скала — первый язык промышленного уровня с такими свойствами. Сейчас за ним подтягивается Котлин и другие более новые языки.

Этот подход позволяет писать гибкий, надежный и хорошо масштабируемый код. Вот несколько хабропостов о нём, если вам интересно:

Иммутабельность

Вообще-то это свойство функционального программирования. Мне кажется очень важным прививать умение пользоваться неизменяемыми структурами данных с самого начала. Это позволит в будущем создавать безопасный многопоточный код. И не только многопоточный.

Расширяемость

Название языка изначально преподносилось как аббревиатура Scalable Language. Особенности языка позволяют писать гибкие и красивые DSL, удобные даже для не-программистов, вкладывать абстракции друг в друга, создавать очень удобные библиотеки и простые скрипты.

В умелых руках такая гибкость обращается в большое благо.

А иногда она обращается против пользователей библиотек, повышая порог входа в их API.  Опытные разработчики могут создавать на Scala простое и удобное API, тем самым заметно облегчая жизнь.

Мощь

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

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

Источник: https://prwatech.in/blog/apache-spark/introduction-to-scala-programming-language/
Источник: https://prwatech.in/blog/apache-spark/introduction-to-scala-programming-language/

Рынок вакансий

Конечно, вакансий для скала-программистов значительно меньше, чем для джава-программистов. Но зато Scala выше оплачивается. Картинка обновлена в 2024. Скала на седьмом месте. И выше нее находятся совсем не те языки, о которых вы могли подумать.

Источник: https://survey.stackoverflow.co/2024/technology#top-paying-technologies-programming-scripting-and-markup-languages

Вот вам картинка про области применения скалы:

Источник: https://prwatech.in/blog/apache-spark/introduction-to-scala-programming-language/
Источник: https://prwatech.in/blog/apache-spark/introduction-to-scala-programming-language/

Мои первые книжки

Меня очень вдохновил курс Functional Programming in Scala, я проходил его в самой первой версии, с тех пор он, конечно, успел измениться. Если вы хотите поиграть скалой, не устанавливая IDE — добро пожаловать в Scastie.

Пример кода

Много примеров короткого кода на скале можно найти вот в этом обсуждении: Samples of Scala and Java code where Scala code looks simpler/has fewer lines? Это тред 2014 года, но очень забавный — рекомендую. Вот один из примеров:

Scala

val keywords = List("Apple", "Ananas", "Mango", "Banana", "Beer")
val result = keywords.sorted.groupBy(_.head)
println(result)

Java 8

import java.util.*;

class Main {
  public static void main(String[] args) {
    List<String> keywords = Arrays.asList("Apple", "Ananas", "Mango", "Banana", "Beer"); 
    Map<Character, List<String>> result = keywords.stream().sorted().collect(Collectors.groupingBy(it -> it.charAt(0)));
    System.out.println(result);
  }
}

Java 7

import java.util.*;

class Main {
  public static void main(String[] args) {
    List<String> keywords = Arrays.asList("Apple", "Ananas", "Mango", "Banana", "Beer"); 
    Map<Character, List<String>> result = new HashMap<Character, List<String>>(); 
    for(String k : keywords) {   
      char firstChar = k.charAt(0);     
      if(!result.containsKey(firstChar)) {     
        result.put(firstChar, new  ArrayList<String>());   
      }     
      result.get(firstChar).add(k); 
    } 
    for(List<String> list : result.values()) {   
      Collections.sort(list); 
    }
    System.out.println(result); 
  }
}

Поступь прогресса

На прощанье — шутка Кея Хорстмана «The March of Progress»

1980: C 

printf("%10.2f", x);

1988: C++

cout << setw(10) << setprecision(2) << fixed << x;

1996: Java

java.text.NumberFormat formatter = java.text.NumberFormat.getNumberInstance(); 
formatter.setMinimumFractionDigits(2); 
formatter.setMaximumFractionDigits(2); 
String s = formatter.format(x); 
for (int i = s.length(); i < 10; i++) System.out.print(' '); 
System.out.print(s);

2004: Java

System.out.printf("%10.2f", x);

2008: Scala and Groovy

printf("%10.2f", x)

2012: Scala 2.10

println(f"$x%10.2f")

На этом я рискну опубликовать этот пост, но по-прежнему планирую его дописывать по результатам обсуждения с вами. Кто бы мог подумать, что спустя 4 года я все еще буду продолжать его дописывать.

Теги:
Хабы:
Всего голосов 14: ↑12 и ↓2+16
Комментарии89

Публикации

Работа

Ближайшие события