Перейти к основному содержимому

Простые приложения на Scala

Разработчику Архитектору

Простые приложения на Scala

Scala на JVM сочетает функциональный стиль (map, Option, for-comprehension) с ООП. Для старта достаточно scala-cli или sbt: примеры ниже — консольные утилиты без Akka/Play.

Эта глава полезна после первой программы и основ языка. Если хотите сначала закрепить типы и match, откройте типы и паттерн-матчинг, а затем возвращайтесь к практическим мини-проектам ниже.


Что именно вы соберете

  • Генератор паролей для CLI-утилит и внутренних инструментов.
  • Мини-сервис работы с файлами (чтение, сортировка, запись).
  • Безопасный калькулятор с Option вместо аварийного падения.
  • JSON-хранилище задач с сериализацией.
  • Минимальный HTTP-эндпоинт на стандартной библиотеке JVM.

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


Как запускать примеры из главы

  1. scala-cli: scala-cli run файл.scala (Scala 3).
  2. sbt: проект с scalaVersionsbt run.
  3. Нужно: JDK 17+.

Генератор паролей

Как запустить

  • Файл: password.scala
  • Команда: scala-cli run password.scala
  • Результат: пароль в stdout.

import scala.util.Random

def generatePassword(length: Int = 16): String = {
val chars = ('a' to 'z') ++ ('A' to 'Z') ++ ('0' to '9') ++ "!@#$%^&*"
Random.shuffle(chars).take(length).mkString
}

@main def run(): Unit = println(generatePassword())

Разбор:

  • import scala.util.Random подключает генератор случайных перестановок и чисел.
  • generatePassword(length: Int = 16) задаёт параметр с дефолтом, поэтому функцию можно вызвать без аргументов.
  • chars собирает общий пул символов из диапазонов букв/цифр и строки спецсимволов.
  • Random.shuffle(chars) перемешивает последовательность, take(length) берёт нужную длину.
  • mkString объединяет выбранные символы в итоговую строку-пароль.
  • @main def run() объявляет точку входа Scala 3 и печатает результат в stdout.

Как улучшить генератор до "практического"

В продакшен-утилите стоит добавить:

  • минимальную длину (например, от 12 символов);
  • флаги --length, --no-symbols;
  • проверку, что в результате есть хотя бы один символ каждого требуемого класса;
  • опциональный вывод в буфер обмена.

Для криптографической стойкости используйте java.security.SecureRandom, а не scala.util.Random.


Сортировщик файла

Как запустить

  • Файл: sort.scala, input.txt рядом
  • Команда: scala-cli run sort.scala

import scala.io.Source
import java.nio.file.{Files, Paths}
import scala.jdk.CollectionConverters.*

def sortFile(in: String, out: String): Unit = {
val lines = Source.fromFile(in, "UTF-8").getLines().map(_.trim).filter(_.nonEmpty).toSeq.sorted
Files.write(Paths.get(out), lines.asJava)
}

@main def run(): Unit = sortFile("input.txt", "output.txt")

Разбор:

  • Source.fromFile(...).getLines() читает входной файл построчно как итератор строк.
  • map(_.trim).filter(_.nonEmpty) удаляет пробелы по краям и отбрасывает пустые строки.
  • toSeq.sorted материализует коллекцию и сортирует её в естественном порядке.
  • lines.asJava конвертирует Scala-коллекцию в Java-коллекцию для API Files.write.
  • Files.write(Paths.get(out), ...) сохраняет отсортированный результат в выходной файл.
  • @main def run() делает утилиту запускаемой одной командой scala-cli run sort.scala.

Практические замечания по файлам

  • Для больших файлов лучше обрабатывать потоками, а не загружать все строки в память.
  • В Source.fromFile используйте try/finally или Using для гарантированного закрытия ресурса.
  • При записи в файл удобно сначала писать во временный файл и затем атомарно переименовывать.

Эти приемы полезны для ETL-утилит, миграций и pre-processing перед Spark.


Калькулятор с Option

Как запустить

  • Команда: scala-cli run — вызовите calc из main в том же файле.
def calc(a: Double, b: Double, op: Char): Option[Double] = op match {
case '+' => Some(a + b)
case '-' => Some(a - b)
case '*' => Some(a * b)
case '/' if b != 0 => Some(a / b)
case _ => None
}

@main def demo(): Unit = {
println(calc(10, 2, '+')) // Some(12.0)
println(calc(10, 0, '/')) // None
}

Разбор:

  • op match выбирает ветку вычисления по символу операции.
  • Some(...) возвращается для валидных операций с успешным числовым результатом.
  • Guard case '/' if b != 0 защищает от деления на ноль.
  • case _ => None обрабатывает все невалидные сценарии: неизвестная операция или b == 0.
  • Использование Option[Double] явно сигнализирует, что результат может отсутствовать.
  • demo показывает оба сценария в консоли без падения программы.

Когда брать Option, а когда Either

  • Option подходит, когда важно только "есть результат / нет результата".
  • Either[String, A] подходит, когда нужна причина ошибки.

Пример улучшения:

def calc2(a: Double, b: Double, op: Char): Either[String, Double] = op match {
case '+' => Right(a + b)
case '-' => Right(a - b)
case '*' => Right(a * b)
case '/' if b != 0 => Right(a / b)
case '/' => Left("division by zero")
case x => Left(s"unknown operation: $x")
}

Разбор:

  • Either[String, Double] добавляет диагностическую информацию вместо "просто нет результата".
  • Right(...) обозначает успешное вычисление.
  • Отдельная ветка case '/' => Left("division by zero") сообщает конкретную причину ошибки.
  • case x => Left(...) перехватывает все неизвестные операторы и возвращает человекочитаемое сообщение.
  • Такой контракт удобен для CLI/API, где пользователю нужна точная причина отказа.

JSON-трекер (circe / uPickle — или java.nio + ручной JSON)

На JVM для учёба часто используют uPickle:

Код ITЗагрузка примера кода…

Разбор:

  • case class Task(...) задаёт компактную модель задачи с дефолтом done = false.
  • read[Seq[Task]](...) десериализует JSON в типизированную коллекцию задач.
  • В load проверка os.exists(path) предотвращает исключение при первом запуске без файла.
  • write(tasks, indent = 2) сериализует задачи в JSON с форматированием для читаемости.
  • os.write сохраняет результат на диск, превращая структуру памяти в постоянное хранилище.
  • runTracker демонстрирует полный цикл: загрузка → добавление → сохранение → чтение JSON.

Минимальный HTTP (com.sun.net.httpserver)


import com.sun.net.httpserver.{HttpServer, HttpHandler, HttpExchange}
import java.net.InetSocketAddress

val server = HttpServer.create(new InetSocketAddress(8080), 0)
server.createContext("/", (ex: HttpExchange) => {
val body = """{"ok":true}"""
ex.getResponseHeaders.add("Content-Type", "application/json")
ex.sendResponseHeaders(200, body.length)
ex.getResponseBody.write(body.getBytes("UTF-8"))
ex.close()
})
server.start()

Разбор:

  • HttpServer.create(new InetSocketAddress(8080), 0) поднимает встроенный HTTP-сервер на порту 8080.
  • createContext("/", ...) регистрирует обработчик для корневого маршрута.
  • Внутри обработчика задаётся JSON-тело ответа и заголовок Content-Type.
  • sendResponseHeaders(200, body.length) отправляет статус и длину тела.
  • getResponseBody.write(...) пишет байты ответа, ex.close() закрывает обмен и освобождает ресурсы.
  • server.start() запускает сервер и начинает принимать запросы.

Что добавить после первого запуска HTTP

  1. Разделить роутинг и бизнес-логику по функциям.
  2. Добавить JSON-сериализацию ответа и стандартный формат ошибок.
  3. Включить базовые метрики и логирование запроса/времени ответа.
  4. Вынести конфигурацию порта в переменные окружения.

Параллельно полезно повторить базу по HTTP и HTTPS и архитектуре JVM-приложений на Scala.


Характерный пример — for-comprehension

def safeDivide(a: Double, b: Double): Option[Double] =
for {
_ <- Option.when(b != 0)(())
r <- Some(a / b)
} yield r

println(safeDivide(10, 2))
println(safeDivide(10, 0))

Разбор:

  • Option.when(b != 0)(()) создаёт Some(()) только если делитель не ноль, иначе None.
  • В for первый шаг служит guard-проверкой без полезного значения (_).
  • r <- Some(a / b) выполняется только при успешном первом шаге.
  • yield r возвращает Some(result) или автоматически None при коротком замыкании.
  • println демонстрирует обе ветки: успешное деление и безопасный отказ без исключения.

Цепочка Option читается как последовательность шагов без вложенных if.


Куда идти дальше

См. также: Java (близкий JVM-стек) · Kotlin

Основа по протоколу

Базовый разбор HTTP и HTTPS находится в отдельной статье — HTTP как основа веб-интеграций.