Хочу представить сообществу перевод моей статьи на 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"<br/>
То что вы видите выше – совершенно легальное выражение в F#. Мы просто вызываем метод project
и передаем ему три параметра – имя проекта, некой токен (пустышку, которая служит англосинтактическим сахаром), и время начала проекта. Фактически мы делаем примерно то же, что делают с тестами в BDD – а именно, пытаются сделать их читабельными для нетехнарей.
DSLина которую мы пишем сама по себе основана на ООР. Наша цель – через DSL поддержать все те конструкции, к которым привыкли РМы. Одна из этих конструкций – проект, поэтому с него пожалуй и начнем:
type Project() =<br/>
[<DefaultValue>] val mutable Name : string<br/>
[<DefaultValue>] val mutable Resources : Resource list<br/>
[<DefaultValue>] val mutable StartDate : DateTime<br/>
[<DefaultValue>] val mutable Groups : Group list<br/>
Вот, я же предупреждал что F# не будет смотреться слишком шикарно если писать с поддержкой mutability. Странные конструкции выше – это публичные поля, которые можно изменять. В плане коллекций я воспользовался F#овским list
вместо List<T>
из System.Collections.Generic
. Разницы особой нет.
В отличии от C#, в F# у нас есть нечто, что на первый взгляд можно именовать «global scope», то есть декларировать переменные и функции можно как бы «на верхнем уровне», без всяких явно описанных классов, модулей и пространств имен. Давайте этим незамедлительно воспользуемся:
<br/>
let mutable my_project = new Project()<br/>
Мы только что создали «переменную дефолтного проекта». Естественно что терминология в F# немного другая, но не суть. Имя мы выбрали такое, чтобы в конце можно было пафосно написать prepare my_project
и запустить автогенерацию проектного плана. А пока давайте посмотрим на функцию project
, с которой все и начинается.
<br/>
let project name startskey start =<br/>
my_project <- new Project()<br/>
my_project.Name <- name<br/>
my_project.Resources <- []<br/>
my_project.Groups <- []<br/>
my_project.StartDate <- DateTime.Parse(start)<br/>
Ну вот. В принципе, на этом этапе можно смело бросать читать статью и идти экспериментировать – ведь всю суть создания DSLей на F# мы только что показали. Дальше будет разбор семантики и собственно демонстрация того, как разруливаются разные тонкости.
Работа в проекте выполняется ресурсами, то есть людьми. Вы ресурс, и я ресурс – не очень-то приятно, не так ли? Тем не менее, у каждого ресурса есть некий титул (к пр. «Junior Developer»), имя («John») а также рейт – сколько долларов в час фирма хочет получать в месяц за работу этого ресурса. Давайте сначала посмотрим на определение этого самого ресурса:
type Resource() =<br/>
[<DefaultValue>] val mutable Name : string<br/>
[<DefaultValue>] val mutable Position : string<br/>
[<DefaultValue>] val mutable Rate : int<br/>
Теперь можно посмотреть на то, как будет выглядеть создание ресурса в нашей DSL:
resource "John" isa "Junior Developer" with_rate 55<br/>
Конечно же, для поддержки выражения выше мы используем то же шаманство что и для проектов, а именно:
let resource name isakey position ratekey rate =<br/>
let r = new Resource()<br/>
r.Name <- name<br/>
r.Position <- position<br/>
r.Rate <- rate<br/>
my_project.Resources <- r :: my_project.Resources<br/>
Как вы уже наверное догадались, мы создаем ресурс и добавляем его в начало списка. Это значит что когда придет время «выстраивать» ресурсы и прочие элементы которые хранятся в списках, каждый список придется разворачивать задом на перед. Для меня это не проблема, но если вам не нравится – используйте List<T>
.
Следующая концепция в нашей DSL – это группы заданий. Группу заданий в проекте обычно выполняет один человек, что способствует поддержанию «когнитивного фокуса». Группу мы определяем вот так:
group "Project Coordination" done_by "Dmitri"<br/>
А вот как выглядит объект, который содержит данные о группе:
type Group() =<br/>
[<DefaultValue>] val mutable Name : string<br/>
[<DefaultValue>] val mutable Person : Resource<br/>
[<DefaultValue>] val mutable Tasks : Task list<br/>
Видите – группы ссылается на объект типа Resource
, а мы передаем имя (строку). Но это не проблема, так как поиск в списках никто не отменял:
let group name donebytoken resource =<br/>
let g = new Group()<br/>
g.Name <- name<br/>
g.Person <- my_project.Resources |> List.find(fun f -> f.Name = resource)<br/>
<br/>
my_project.Groups <- g :: my_project.Groups<br/>
В отличии от LINQ, не надо вызывать Single()
чтобы получить результат поиска.
Группы тасков (заданий) состоят из, эммм, заданий. А задание неплохо определять вот так:
task "PayPal Integration" takes 2 weeks<br/>
Это тоже реально в F#! Для начала, мы делаем так, чтобы те токены которые мы обычно используем для «сахара» содержали внятные значения:
let hours = 1<br/>
let hour = 1<br/>
let days = 2<br/>
let day = 2<br/>
let weeks = 3<br/>
let week = 3<br/>
let months = 4<br/>
let month = 4<br/>
Теперь мы можем определить наш Task
:
type Task() =<br/>
[<DefaultValue>] val mutable Name : string<br/>
[<DefaultValue>] val mutable Duration : string<br/>
А добавление таска в группу выглядит вот так:
let task name takestoken count timeunit =<br/>
let t = new Task()<br/>
t.Name <- name<br/>
let dummy = 1 + count<br/>
<br/>
match timeunit with<br/>
| 1 -> t.Duration <- String.Format("{0}h", count)<br/>
| 2 -> t.Duration <- String.Format("{0}d", count)<br/>
| 3 -> t.Duration <- String.Format("{0}wk", count)<br/>
| 4 -> t.Duration <- String.Format("{0}mon", count)<br/>
| _ -> raise(ArgumentException("only spans of hour(s), day(s), week(s) and month(s) are supported"))<br/>
<br/>
let g = List.hd my_project.Groups<br/>
g.Tasks <- t :: g.Tasks<br/>
В коде выше мы в зависимости от временной константы подстраиваем продолжительность таска. Для того чтобы найти ту группу, в которую нужно добавить таск, мы используем List.hd
– ведь группы тоже задом наперед.
Ну вот и все! Теперь мы можем вызвать одну помпезную комманду чтобы сгенерировать наш проектный план:
prepare my_project<br/>
Далее идет самый сложный кусочек – использование Office Automation и F# в тандеме для генерации плана из нашей DSLки. Я постарался прокомментировать код чтобы было понятно что к чему.
let prepare (proj:Project) =<br/>
let app = new ApplicationClass()<br/>
app.Visible <- true<br/>
let p = app.Projects.Add()<br/>
p.Name <- proj.Name<br/>
<br/>
proj.Resources |> List.iter(fun r -><br/>
let r2 = p.Resources.Add()<br/>
r2.Name <- r.Position // position, not name :)
let tables = r2.CostRateTables<br/>
let table = tables.[1]<br/>
table.PayRates.[1].StandardRate <- r.Rate<br/>
table.PayRates.[1].OvertimeRate <- (r.Rate + (r.Rate >>> 1)))<br/>
<br/>
let root = p.Tasks.Add()<br/>
root.Name <- proj.Name<br/>
<br/>
proj.Groups |> List.rev |> List.iter(fun g -> <br/>
let t = p.Tasks.Add()<br/>
t.Name <- g.Name<br/>
t.OutlineLevel <- 2s<br/>
<br/>
t.ResourceNames <- g.Person.Position<br/>
<br/>
let tasksInOrder = g.Tasks |> List.rev<br/>
tasksInOrder |> List.iter(fun t2 -><br/>
let t3 = p.Tasks.Add(t2.Name)<br/>
t3.Duration <- t2.Duration<br/>
t3.OutlineLevel <- 3s<br/>
<br/>
let idx = tasksInOrder |> List.findIndex(fun f -> f.Equals(t2))<br/>
if (idx > 0) then <br/>
t3.Predecessors <- Convert.ToString(t3.Index - 1)<br/>
)<br/>
)<br/>
Ну вот мы и «развернули» списки с помощью List.rev
– не самая быстрая операция, конечно, но это не важно. Главное, что скрипт работает и генерит проекты – определяет ресурсы, группы тасков и сами таски. А что еще РМу надо? (На самом деле много чего :)
А вот как может выглядеть полное описание проекта с использованием нашей DSL:
project "F# DSL Article" starts "01/01/2009"<br/>
resource "Dmitri" isa "Writer" with_rate 140<br/>
resource "Computer" isa "Dumb Machine" with_rate 0<br/>
group "DSL Popularization" done_by "Dmitri"<br/>
task "Create basic estimation DSL" takes 1 day<br/>
task "Write article" takes 1 day<br/>
task "Post article and wait for comments" takes 1 week<br/>
group "Infrastructure Support" done_by "Computer"<br/>
task "Provide VS2010 and MS Project" takes 1 day<br/>
task "Download and deploy TypograFix" takes 1 day<br/>
task "Sit idly while owner waits for comments" takes 1 week<br/>
prepare my_project<br/>
Надеюсь этот очерк показал вам, что делать DSL в F# – это просто. Конечно, тот пример что я привел выше упрощен по сравнению с тем что мы реально испольуем. Но это, как говорится, секреты фирмы. До новых встреч!