понедельник, 13 мая 2013 г.

Примитивный DSL на языке Scala

Привет всем начинающим (и продолжающим) программистам Scala.

Интро:

Одной из наиболее привлекательных сторон Scala (а ее название, как известно, произошло от scalable language) (и наиболее располагающей к творчеству) является возможность создания своего языка на основе Scala. Так называемого DSL (domain-specific language).

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

instance.method(parameter1, parameter2, ...)
instance.method2(parameter1, parameter2, ...)

и т.п.

Но описывая тот или иной аспект системы проще выражать его в форме чем-то напонимающей предложения английского языка, вроде:

circuit voltage 220 V current 100 mA

Как видите достаточно компактно, нет лишних скобок, фигурных скобок.

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

Далее приведена попытка разработать свой небольшой DSL, задача которого описывать шаги некоего build скрипта, аналог команд записанных в bat-файле.

Естественно, что этому мини-проекту расти и расти до мало-мальского реального применения в реальной жизни, но моя задача показать что можно сделать такого на Скале out-of-the-box чего нельзя на Java (это к слову сказать весьма частый вопрос людей не знакомых с языком).

Код ниже:

package spb.ext

import sys.process._

/**
 * Builds our project
 * User: oabakumov
 * Date: 29.04.13
 * Time: 12:44
 */
object Spb extends App {

  /**
   * Build script
   *
   * @param options Map of pairs String -> String that represents build settings (path, build variables, options etc.)
   * @param body script's body to run
   * @return
   */
  def script(options: Map[String, String])(body: (Map[String, String] => Any)) {
    body(options)
  }

  /**
   * Represents a command (atomic part of the script)
   * @param label label
   * @param cmd shell command
   * @param silent verbosity of the command
   */
  case class Command(label: Option[String], cmd: String, silent: Boolean) {
    def loudly(m: String) = Command(Some(m), this.cmd, false)

    def doing(c: String) = Command(this.label, c, this.silent)
  }

  def command = new Command(None, "", true)

  def run(c: Command) {
    c match {
      case Command(_, cmd, true) => {
        // silent
        cmd.!!<
      }

      case Command(label, cmd, false) => {
        // verbose
        println(label.get)
        val r = cmd.!!<
        println(r)
      }
    }
  }

  def bunch(label: String)(commands: Command*) {
    println(label)

    commands.foreach(c => {
      run(c)
    })
  }

  val MYSCRIPT1 = script(Map("codebase" -> "c:\\my-code-is-here\\")) _
  MYSCRIPT1(
    options => {
      val cb = options("codebase")

      bunch("STAGE 1") (
        command doing "cmd /C cd " + cb + "proj\\sub-proj",
        command loudly "publish locally" doing "cmd /C sbt clean publish-local"
      )
    }
  )
}

То что вышло в результате (и то что можно назвать DSL) начинается вот тут:

val MYSCRIPT1 = script(Map("codebase" -> "c:\\my-code-is-here\\")) _
  MYSCRIPT1(
    options => {
      val cb = options("codebase")

      bunch("STAGE 1") (
        command doing "cmd /C cd " + cb + "proj\\sub-proj",
        command loudly "publish locally" doing "cmd /C sbt clean publish-local"
      )
    }
  )
Я создаю переменную (вернее -- значение - val) MYSCRIPT1 для дальнейшего использования.
Функция script - это функция которая возвращает другую функцию "зафиксировав" параметр
options: Map[String, String].

Это называется словом "карринг". Полезная в ряде случаев техника.

Т.е. сначала я "набиваю" функцию необходимыми настройками, которые мне будут нужны в моем билд-скрипте, но тем не менее мешающими читать код, путающимися под ногами.

Затем я вызываю ее тут --

MYSCRIPT1(
    options => {

    ...   
    }
)

options - доступен мне как параметр - могу его использовать в блоке кода.


Вот это и есть шаги скрипта (т.е. что ему делать на каждом этапе):

bunch("STAGE 1") (
    command doing "cmd /C cd " + cb + "proj\\sub-proj",
    command loudly "publish locally" doing "cmd /C sbt clean publish-local"
      )

Т.е. написанное выше это тоже что и:

bunch(
new Command().doing("cmd /C cd " + cb + "proj\\sub-proj"),
new Command().loudly("publish locally").doing("cmd /C sbt clean publish-local")
)
Можно и "одиночными" run {command doing "cmd /C sbt clean publish-local"}

Как это работает:

Все эти удобства благодаря тому что в Скале можно вызывать метод экземпляра класса опуская скобки для единственного параметра метода:

old-style: util.print(somethingToPrint)
new one:  util print somethingToPrint

И точка тоже не нужна для таких случаев.

Итог - запись короче и выглядит натуральнее.

Развивая этот tool дальше не обойтись без очень полезной возможности языка -- implicit conversions.

А для этого поста хватит на сегодня )

Комментариев нет:

Отправить комментарий