Блог товарища Nihirash

16.12.2018

Создание лисп-подбного скриптового языка. Часть 5.

Проблема написания статьи постфактум - это ужасная проблема, с которой столнулся и я - ELLS уже даже доступен в виде Maven-артефакта, а на GitHub'е уже есть небольшой плохой пример того, как проинтегрить ELLS со своим проектом. Плюс травма(я сломал ногу) выбила меня из рабочего состояния и процесс совсем затянулся. Но я обещал - и я сделал.

Ремарки

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

Вам достаточно знать, что если вы испортируете объект com.nihirash.ells.Implicits, то числа, строки и булевы значения будут автоматически приводиться к типам ELLS.

Все же вернемся к расширяемости

Для этого все формы покинут наш класс Eval.

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

Мы не будем описывать какие именно формы будут описаны в каком классе, а просто точка входа будет возвращать Option от результата, и если он будет None - будем считать, что переданная конструкция не относится к текущему описанию форм и стоит поискать определения в других подключенных файлах.

Как результат мы получаем следующий участок кода:

trait SpecialForm {
  val eval: (EllsType, Env) => EllsType

  def call(id: EllsIdentifier, args: List[EllsType], env: Env): Option[EllsType]
}

Я не буду описывать перенос каждой из форм в отдельные файлы, но для примера реализуем форму form-1, при вызове которой мы всегда будем получать результат в виде числа 1.

Для этого нам необходимо создать класс с единственным методом.

class CustomForm(val eval: (EllsType, Env) => EllsType) extends SpecialForm {
    override def call(id: EllsIdentifier, args: List[EllsType], env: Env): Option[EllsType] = id.v match {
      case "form-1" => Some(1)
      case _ => None
    }
}

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

Для этого нам нужно описать все доступные формы - введем для этого переменную forms содержащую последовательность спец.форм и добавим в нее нашу форму:

val forms: Seq[SpecialForm] = Seq(new CustomForm(eval))

Все дополнительные формы можно добавлять, просто расширив коллекцию элементами.

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

private def evalCall(id: EllsIdentifier, args: List[EllsType], env: Env): EllsType = {

    def tryEval(forms: Seq[SpecialForm], id: EllsIdentifier, args: List[EllsType], env: Env): Option[EllsType] =
      forms match {
        case Nil => None
        case form :: _ =>
          form.call(id, args, env) match {
            case None                   => tryEval(forms.tail, id, args, env)
            case result: Some[EllsType] => result
          }
      }

    tryEval(forms, id, args, env).getOrElse(env.get(id) match {
      case EllsNil()       => EllsNil()
      case f: EllsFunction => evalFunction(f, args.map(eval(_, env)), env)
      case _               => throw EllsEvalException(s"Can't eval form '$id' with args '$args'")
    })
}

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

Попробуем исполнить новую форму:

  class MyEval extends Eval {
    override val forms: Seq[SpecialForm] = baseForms :+ new CustomForm(eval)
  }

  val eval = new MyEval

Parser("(list (form-1) 2 3)").map(eval.eval(_, Env.preDef)) 

Результат будет списком из чисел 1, 2 и 3.

Для удобства работы с интерпретатором был введен класс Interpreter:

case class EllsResult(result: EllsType, env: Env)

class Interpreter(evalator: Eval, initialEnv: Env) {
  def run(script: String, env: Env = initialEnv.copy()): Either[String, EllsResult] = {
    Parser(script).flatMap { ast =>
      try {
        val result = evalator.eval(ast, env)
        Right(ells.EllsResult(result, env))
      } catch {
        case x: Throwable =>
          Left(x.getLocalizedMessage)
      }
    }
  }
}

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

Как же встроить все это дело в наш проект?

В качестве простого(и не очень то и качественного, но рабочего) примера я интегрировал ELLS в проект, где подключил Processing к Scala. На мой взгляд далеко не самое стандартное использование Scala, но пример быстрый и легко создаваемый, а главное наглядный.

Посмотреть на проект можно По ссылке. Опишу лишь самый минимум.

Новые формы языка размещены в этом файле - там определен самый минимальный набор новых форм, а так же, там пожно подсмотреть передачу внешних объектов в описание форм.

Класс исполнителя в себе не содержит практически ничего нового(не считая проброс pApplet'а и подключения наших новых форм к списку доступных.

Инициализировав окружение в конструкторе нашего апплета, мы будем его использовать в каждом из вызовов нашего жизненного цикла.

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

Этот пример может и представляет не функциональных подход к Scala(а скорее не очень хороший Java-образный подход), однако показывает на примере, как подключить ELLS к Вашему приложению.

Что же дальше?

Дальше в ELLS будут добавлены остальные формы, необходимые для более полноценного использования(но все еще оставляя его встраиваемым скриптовым языком).

Хотелось бы привести код к более функциональному стилю. Больше всего меня смущает код окружения, построенный через мутабельные HashMap'ы.

Как один из вариантов развития - хочется попробовать динамическую транспиляцию в Scala и компилирование на лету через reflect.Toolbox - тем самым можно попробовать упростить интеграцию с проектом, повысить производительность и получить множество других приемуществ.

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

Спасибо всем, кто интересовался этим проектом все это время и писал мне в телеграм. Надеюсь на этом все не заглохнет :-)