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

.NETF#

Хочу представить сообществу перевод моей статьи на 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# – это просто. Конечно, тот пример что я привел выше упрощен по сравнению с тем что мы реально испольуем. Но это, как говорится, секреты фирмы. До новых встреч!

Tags:dslfsharpf.netprojectautomationestimationmanagementdomain-specific languages
Hubs: .NET F#
+12
5k 37
Comments 35

Popular right now

.NET-разработчик
from 200,000 ₽Банк «Открытие»Москва
.NET developer
to 200,000 ₽SibedgeМоскваRemote job
.NET C# Software Engineer
from 3,500 to 4,000 $Hand2NoteRemote job
Программист .net
from 70,000 to 120,000 ₽Мечел-СервисЧелябинскRemote job
.net разработчик
from 120,000 to 170,000 ₽БАРС ГрупНижний НовгородRemote job