Простые приложения на Scala
Простые приложения на Scala
Scala на JVM сочетает функциональный стиль (map, Option, for-comprehension) с ООП. Для старта достаточно scala-cli или sbt: примеры ниже — консольные утилиты без Akka/Play.
Эта глава полезна после первой программы и основ языка. Если хотите сначала закрепить типы и match, откройте типы и паттерн-матчинг, а затем возвращайтесь к практическим мини-проектам ниже.
Что именно вы соберете
- Генератор паролей для CLI-утилит и внутренних инструментов.
- Мини-сервис работы с файлами (чтение, сортировка, запись).
- Безопасный калькулятор с
Optionвместо аварийного падения. - JSON-хранилище задач с сериализацией.
- Минимальный HTTP-эндпоинт на стандартной библиотеке JVM.
Каждый блок можно запускать отдельно. В результате вы получаете рабочие шаблоны для pet-проектов, техзаданий и внутренних автоматизаций команды.
Как запускать примеры из главы
- scala-cli:
scala-cli run файл.scala(Scala 3). - sbt: проект с
scalaVersion→sbt run. - Нужно: 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-коллекцию для APIFiles.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
- Разделить роутинг и бизнес-логику по функциям.
- Добавить JSON-сериализацию ответа и стандартный формат ошибок.
- Включить базовые метрики и логирование запроса/времени ответа.
- Вынести конфигурацию порта в переменные окружения.
Параллельно полезно повторить базу по 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.
Куда идти дальше
- Для углубления синтаксиса: управляющие конструкции и операторы.
- Для уверенной работы с данными: типы и паттерн-матчинг.
- Для функционального стиля: функции и функциональные конструкции.
- Для сравнения JVM-подходов: Java и Kotlin.
См. также: Java (близкий JVM-стек) · Kotlin
Базовый разбор HTTP и HTTPS находится в отдельной статье — HTTP как основа веб-интеграций.