Дмитpий Hecтepук

Блог о программировании — C#, F#, C++, архитектура, и многое другое

Создание DSL на языке F#

with one comment

Хочу представить сообществу перевод моей статьи на CodeProject, в которой я описываю процесс создания DSLей с использованием языка F#.

Введение

Если честно, мне уже изрядно поднадоели разговоры о DSLях в чисто академическом ключе. Хочется увидеть конкретный пример того, как это счастье используется «в продакшн». Да и вообще, саму концепцию можно объяснить и реализовать намного более доходчиво и прямолинейно чем делают авторы таких фреймворков как Oslo или MPS. Собственно тут я как раз и хочу показать решение которое вовсе не академическое а именно производственное, и служит конкретным целям.

Начнем с того, что обсудим что же такое DSL. DSL – доменно специфичный язык – то есть способ описания той или иной предметной специфики (которая часто связана с конкретной индустрией) с помощью такого языка, который могут понять не только разработчики, но и эксперты в предметной области. Важно в этом языке то, что те кто его используют не должны думать о фигурных скобочках, точках с запятой и прочих прелестях программирования. То есть у них должна быть возможность писать на «простом английском» (русском, японском, и т.д.)

В этом очерке мы будем использовать язык F# для написания DSLи которая помогает нам делать оценку трудоемкости проектов. Более заумная версия этой DSLины используется у нас на производстве. Сразу скажу, что тот код который я покажу далеко не идеальный пример использования F#, так что все «камни в огород» в плане стиля программирования буду игнорировать. Суть-то не в этом. Впрочем, если есть желание пооптимизировать – пожалуйста.

Ах да, и вот еще что – сразу дам ссылочки на оригинал статьи и исходный код. Код – это по сути дела один .fs файл. Надеюсь у вас получится его скомпилировать. Для того чтобы оценить то, как он работает, вам потребуется Project 2007. Если у вас его нет, спросите ближесидящего PMа.

Итак, в путь!

Описание Проблемы

Когда кому-то нужно заказное ПО, этот кто-то (обычно именуется «заказчик») шлет разным фирмам так называемый RFP (request for proposal), то есть по сути дела описание своего проекта. На этот запрос фирмы-разработчики делают проектный план (если инфы достаточно – если нет, то начинают общаться), пакуют его в красивый PDF и отсылают назад, причем естественно чем быстрее произведена оценка (эстимейт), чем она качественней и чем лучше преподнесена, тем больше вероятность что клиент будет с вами общаться. Получается что в интересах всей фирмы сделать этот эстимейт хорошо и быстро.

Кто-то должен делать этот эстимейт… обычно «крайним» является какой-нибудь релаксирующий под музыку PM, который достаточно знает технологический стек и имеет хоть чуть-чуть опыта чтобы прикинуть что и так (механизм peer review, если он налажен, все равно сгладит все его косяки). Так вот, наш РМ должен оценить этапы проекта и сделать красивый временной график (кажется это называется GANTT chart) чтобы наглядно показать на что пойдут усилия разработчиков, тестировщиков, и свои тоже. Тут возникает проблема.

Проблема в том что MS Project, та тулза которой создается это счастье, не очень то быстра на подъем когда нужно постоянно реструктурировать оценки, менять таски местами, корректировать ресурсы, оверхед, ну и т.д. Все становится слишком напряжно, особенно если вы придерживаетесь правила что «каждый клиент должен получить эстимейт в пределах одного дня своей временной зоны». Приходится изворачиваться, и наш DSL – это попытка упростить и ускорить оценочную деятельность для всех участников.

Выбор Языка

Проблему мы описали, теперь о решении. В принципе, для описания проекта можно сделать «свободную» DSL где можно использовать любой синтаксис и потом парсить его с помощью умных фреймворков, но это как-то скучно если учесть что эти фреймворки ничего не добавят в результат, зато наверняка принесут немного головной боли. Поэтому более простым подходом будет выбор языка (в нашем случае – языка в .Net стеке) который позволит писать на «почти Английском языке» и не будет сильно напрягать нетехнический персонал (хотя если РМ не умеет программировать, то это не к нам).

Из популярных языков для DSLей конечно нужно отметить Boo, который неплохо пропиарил Ayende в своей книге. Boo – очень мощный язык, но в данном случае его метапрограммисткая мощь нам не потребуется. Еще есть язык Ruby который тоже популярен в плане DSLей но я к сожалению с ним не знаком (досадное упущение), поэтому не могу его порекоммендовать. Ну и последний выбор, на котором я и остановился, это F#.

Почему F# хорош для DSLей? Потому что его синтаксис не нагружает разум. Можно писать почти на чистом английском. DSL читаем кем угодно. Единственная проблема – это то, что F# ориентирован на неизменчивость переменных (immutability), поэтому в нашем контексте некоторые его конструкты будут выглядеть немного неестественно. Но, как я уже сказал, суть не в этом – ведь DSL это всего лишь трансформатор, «кондуит сознания».

Первый Стейтмент

Начнем с простого. Вот как выглядит первая строчка в описании проекта:

project "Write F# DSL Article" starts_on "16/8/2009"

То что вы видите выше – совершенно легальное выражение в F#. Мы просто вызываем метод project и передаем ему три параметра – имя проекта, некой токен (пустышку, которая служит англосинтактическим сахаром), и время начала проекта. Фактически мы делаем примерно то же, что делают с тестами в BDD – а именно, пытаются сделать их читабельными для нетехнарей.

DSLина которую мы пишем сама по себе основана на ООР. Наша цель – через DSL поддержать все те конструкции, к которым привыкли РМы. Одна из этих конструкций – проект, поэтому с него пожалуй и начнем:

type Project() =
  [<DefaultValue>] val mutable Name : string
  [<DefaultValue>] val mutable Resources : Resource list
  [<DefaultValue>] val mutable StartDate : DateTime
  [<DefaultValue>] val mutable Groups : Group list

Вот, я же предупреждал что F# не будет смотреться слишком шикарно если писать с поддержкой mutability. Странные конструкции выше – это публичные поля, которые можно изменять. В плане коллекций я воспользовался F#овским list вместо List<T> из System.Collections.Generic. Разницы особой нет.

В отличии от C#, в F# у нас есть нечто, что на первый взгляд можно именовать «global scope», то есть декларировать переменные и функции можно как бы «на верхнем уровне», без всяких явно описанных классов, модулей и пространств имен. Давайте этим незамедлительно воспользуемся:

/* Делаем проект. Переменная my_project будет в последствии переопределена. */
let mutable my_project = new Project()

Мы только что создали «переменную дефолтного проекта». Естественно что терминология в F# немного другая, но не суть. Имя мы выбрали такое, чтобы в конце можно было пафосно написать prepare my_project и запустить автогенерацию проектного плана. А пока давайте посмотрим на функцию project, с которой все и начинается.

/* Поддержка создания нового проекта.
Как и обещалось, переменная my_project переписывается.
Полезно если нужно делать несколько проектов подряд. */
let project name startskey start =
  my_project <- new Project()
  my_project.Name <- name
  my_project.Resources <- []
  my_project.Groups <- []
  my_project.StartDate <- DateTime.Parse(start)

Ну вот. В принципе, на этом этапе можно смело бросать читать статью и идти экспериментировать – ведь всю суть создания DSLей на F# мы только что показали. Дальше будет разбор семантики и собственно демонстрация того, как разруливаются разные тонкости.

Работа со Списками

Работа в проекте выполняется ресурсами, то есть людьми. Вы ресурс, и я ресурс – не очень-то приятно, не так ли? Тем не менее, у каждого ресурса есть некий титул (к пр. «Junior Developer»), имя («John») а также рейт – сколько долларов в час фирма хочет получать в месяц за работу этого ресурса. Давайте сначала посмотрим на определение этого самого ресурса:

type Resource() =
  [<DefaultValue>] val mutable Name : string
  [<DefaultValue>] val mutable Position : string
  [<DefaultValue>] val mutable Rate : int

Теперь можно посмотреть на то, как будет выглядеть создание ресурса в нашей DSL:

resource "John" isa "Junior Developer" with_rate 55

Конечно же, для поддержки выражения выше мы используем то же шаманство что и для проектов, а именно:

let resource name isakey position ratekey rate =
  let r = new Resource()
  r.Name <- name
  r.Position <- position
  r.Rate <- rate
  my_project.Resources <- r :: my_project.Resources

Как вы уже наверное догадались, мы создаем ресурс и добавляем его в начало списка. Это значит что когда придет время «выстраивать» ресурсы и прочие элементы которые хранятся в списках, каждый список придется разворачивать задом на перед. Для меня это не проблема, но если вам не нравится – используйте List<T>.

Строковые Ссылки

Следующая концепция в нашей DSL – это группы заданий. Группу заданий в проекте обычно выполняет один человек, что способствует поддержанию «когнитивного фокуса». Группу мы определяем вот так:

group "Project Coordination" done_by "Dmitri"

А вот как выглядит объект, который содержит данные о группе:

type Group() =
  [<DefaultValue>] val mutable Name : string
  [<DefaultValue>] val mutable Person : Resource
  [<DefaultValue>] val mutable Tasks : Task list

Видите – группы ссылается на объект типа Resource, а мы передаем имя (строку). Но это не проблема, так как поиск в списках никто не отменял:

let group name donebytoken resource =
  let g = new Group()
  g.Name <- name
  g.Person <- my_project.Resources |> List.find(fun f -> f.Name = resource)
  /* Ищем тот ресурс что нам нужен. */
  my_project.Groups <- g :: my_project.Groups

В отличии от LINQ, не надо вызывать Single() чтобы получить результат поиска.

Побольше Гибкости

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

task "PayPal Integration" takes 2 weeks

Это тоже реально в F#! Для начала, мы делаем так, чтобы те токены которые мы обычно используем для «сахара» содержали внятные значения:

let hours = 1
let hour = 1
let days = 2
let day = 2
let weeks = 3
let week = 3
let months = 4
let month = 4

Теперь мы можем определить наш Task:

type Task() =
  [<DefaultValue>] val mutable Name : string
  [<DefaultValue>] val mutable Duration : string

А добавление таска в группу выглядит вот так:

let task name takestoken count timeunit =
  let t = new Task()
  t.Name <- name
  let dummy = 1 + count
  /* Это хак. Суть в том, чтобы заставить компилятор думать что count - это int.
     Иначе банально не сработает угадываение типов, ведь в качестве подсказок есть 
     только String.Format ниже. */
  match timeunit with
  | 1 -> t.Duration <- String.Format("{0}h", count)
  | 2 -> t.Duration <- String.Format("{0}d", count)
  | 3 -> t.Duration <- String.Format("{0}wk", count)
  | 4 -> t.Duration <- String.Format("{0}mon", count)
  | _ -> raise(ArgumentException("only spans of hour(s), day(s), week(s) and month(s) are supported"))
  /* А вот тут мы сохраняем продолжительность таска как строковый литерал,
который угоден Project'у. */
  let g = List.hd my_project.Groups
  g.Tasks <- t :: g.Tasks

В коде выше мы в зависимости от временной константы подстраиваем продолжительность таска. Для того чтобы найти ту группу, в которую нужно добавить таск, мы используем List.hd – ведь группы тоже задом наперед.

Автогенерация Проекта

Ну вот и все! Теперь мы можем вызвать одну помпезную комманду чтобы сгенерировать наш проектный план:

prepare my_project

Далее идет самый сложный кусочек – использование Office Automation и F# в тандеме для генерации плана из нашей DSLки. Я постарался прокомментировать код чтобы было понятно что к чему.

let prepare (proj:Project) =
  let app = new ApplicationClass()
  app.Visible <- true
  let p = app.Projects.Add()
  p.Name <- proj.Name
  /* Добавляем в проект ресурсы.
     Стоимость ресурса в овертайм считаем как полтора рейта. */
  proj.Resources |> List.iter(fun r ->
    let r2 = p.Resources.Add()
    r2.Name <- r.Position // position, not name :)
    let tables = r2.CostRateTables
    let table = tables.[1]
    table.PayRates.[1].StandardRate <- r.Rate
    table.PayRates.[1].OvertimeRate <- (r.Rate + (r.Rate >>> 1)))
  /* Делаем корневой таск с именем проекта. */
  let root = p.Tasks.Add()
  root.Name <- proj.Name
  /* Добавляем группы. */
  proj.Groups |> List.rev |> List.iter(fun g -> 
    let t = p.Tasks.Add()
    t.Name <- g.Name
    t.OutlineLevel <- 2s
    /* Кто отвечает за группу? */
    t.ResourceNames <- g.Person.Position
    /* Добавляем задания. */
    let tasksInOrder = g.Tasks |> List.rev
    tasksInOrder |> List.iter(fun t2 ->
        let t3 = p.Tasks.Add(t2.Name)
        t3.Duration <- t2.Duration
        t3.OutlineLevel <- 3s
        /* Ищем предыдущий таск. Если такой имеется, делаем связку
           между этим таском и предыдушим. Предполагается что все такси следуют один за другим. */
        let idx = tasksInOrder |> List.findIndex(fun f -> f.Equals(t2))
        if (idx > 0) then 
          t3.Predecessors <- Convert.ToString(t3.Index - 1)
      )
    )

Ну вот мы и «развернули» списки с помощью List.rev – не самая быстрая операция, конечно, но это не важно. Главное, что скрипт работает и генерит проекты – определяет ресурсы, группы тасков и сами таски. А что еще РМу надо? (На самом деле много чего :)

А вот как может выглядеть полное описание проекта с использованием нашей DSL:

project "F# DSL Article" starts "01/01/2009"
resource "Dmitri" isa "Writer" with_rate 140
resource "Computer" isa "Dumb Machine" with_rate 0
group "DSL Popularization" done_by "Dmitri"
task "Create basic estimation DSL" takes 1 day
task "Write article" takes 1 day
task "Post article and wait for comments" takes 1 week
group "Infrastructure Support" done_by "Computer"
task "Provide VS2010 and MS Project" takes 1 day
task "Download and deploy TypograFix" takes 1 day
task "Sit idly while owner waits for comments" takes 1 week
prepare my_project

Заключение

Надеюсь этот очерк показал вам, что делать DSL в F# – это просто. Конечно, тот пример что я привел выше упрощен по сравнению с тем что мы реально испольуем. Но это, как говорится, секреты фирмы. До новых встреч!

Реклама

Written by Dmitri

29 августа 2009 в 10:14

Опубликовано в .NET, dsl, f#

Один ответ

Subscribe to comments with RSS.

  1. Больше всего в статье понравились рейты :)

    Idsa

    10 сентября 2010 at 9:35


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

Заполните поля или щелкните по значку, чтобы оставить свой комментарий:

Логотип WordPress.com

Для комментария используется ваша учётная запись WordPress.com. Выход / Изменить )

Фотография Twitter

Для комментария используется ваша учётная запись Twitter. Выход / Изменить )

Фотография Facebook

Для комментария используется ваша учётная запись Facebook. Выход / Изменить )

Google+ photo

Для комментария используется ваша учётная запись Google+. Выход / Изменить )

Connecting to %s

%d такие блоггеры, как: