.NET

индекс
121,07

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

Хочу представить сообществу перевод моей статьи на 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», то есть декларировать переменные и функции можно как бы «на верхнем уровне», без всяких явно описанных классов, модулей и пространств имен. Давайте этим незамедлительно воспользуемся:


let mutable my_project = new Project()

Мы только что создали «переменную дефолтного проекта». Естественно что терминология в F# немного другая, но не суть. Имя мы выбрали такое, чтобы в конце можно было пафосно написать prepare my_project и запустить автогенерацию проектного плана. А пока давайте посмотрим на функцию 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
  
  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"))
  
  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# – это просто. Конечно, тот пример что я привел выше упрощен по сравнению с тем что мы реально испольуем. Но это, как говорится, секреты фирмы. До новых встреч!
+12
29 августа 2009, 21:31
27

комментарии (35)

–3
iamdeuterium #
сфера применения?
+2
scandi #
КАТ! Кат пожалуста!
0
FanKiLL #
Don't panic^))
+1
Kalan #
И картинки в текст, если можно.
–1
mezastel #
Это как?
+1
Kalan #
Заголовки и комментарии лучше печатать текстом, а не вставлять картинками. Тогда их можно будет копировать и т.п.
–2
mezastel #
А зачем их копировать?
0
Kalan #
Ну мало ли зачем.

Ещё есть поисковики, которые не умеют распознавать текст, ну и прочее.
0
Ai_boy #
Класс! Я же говорю что за F# будущее… А если еще вспомнить о том что Unicode — это круто, то можно получить что-то вроде:

проект «Продажа сипулек» начало в «01.01.2009»
группа «Продавцы» во главе «Бобайский Дмитрий Иванович»
задание «Продать как можно больше сипулек» займет 1 неделю
подготовить проект
0
mezastel #
Вполне реально, причем без переписывания бизнес-логики.
0
Ai_boy #
Да вообще ничего переписывать не нужно… достаточно добавить…

let во p1 = p1 //пропускаем предлог
let начало p1 p2 = startskey p2
let главе = donebytoken

И можно спокойно использовать тот же самый код :)

… во главе «Максим Викторович»…
… начало в «01.01.2009»…
+4
VoidEx #
Исключительно ради интереса и сравнения написал то же на Haskell.
0
mezastel #
Меня смущает это:
 
		
+1
mezastel #
Меня смущает это:

test = makeProject $ do
0
VoidEx #
Меня тоже, но частично.
Насколько я понимаю, файл с описанием проекта так или иначе должен быть преобразован, хотя бы чтобы добавить соответствующие импорты, а тогда не так уж принципиально становится, что именно добавлять.
–1
mikhanoid #
Хм… А почему это называется DSL? По той лишь причине, что вызов функций не требует разделять аргументы запятыми? Так тогда bash — с той же лёгкостью позволяет создать DSL.
+4
mezastel #
Потому что это доменно-специфичный язык.
0
mikhanoid #
Тогда и любая библиотека функций для C — это DSL. Никакой разницы же.
+2
ApeCoder #
это internal DSL, для external в F# есть из коробки fslex и fsyacc
+2
grep0 #
Судя по избытку mutable в исходниках, вы очень странно понимаете идею функциональных языков
–5
stas_agarkov #
вам это не кажется АХИНЕЕЙ????????????????????????77777777777777777777777777
+4
Indalo #
За что минусуют статью?
НЛО прилетело и опубликовало эту надпись здесь
+1
mezastel #
На СР тоже порядка 30% пользователей поставили трешку. Тут минуснула где-то половина. Это нормально.
0
naething #
Рискну предположить, что за заголовки в виде картинок
+1
mezastel #
Да нет, что вы :)
0
Ai_boy #
Мне кажеться что просто все кто более или менее тепло относился к .Net давно покинули этот ресурс… Остальным же религия не позволяет поставить + в статье, хоть как-то связанной с MS…

PS: при этом НАМНОГО больше плюсов набирает статья о том что вышел очередной дистр Linux… Да на копипаст ссылки из официального RSS потрачено больше сил чем на эту статью… ппц o___O
+1
mezastel #
Интересно, куда же они ушли.
–1
AndreyTS #
Никуда мы не ушли :-D
0
A1lfeG #
Спят наверное спокойно.
Для многих статья вышла глубокой ночью :)
0
Mephistophele #
мы затаились и мы добрые :)
0
mezastel #
А зачем затаились-то? Напишите что-нть интересное, а мы порадуемся.
0
googman #
Спасибо! Познавательно. Термин «неизменчивость переменных» улыбнул. :)
0
runnig #
спасибо за статью!
0
xni #
Как тег, наверное, стоило ы ещё добавить Domain Specific Languages, а то еле нашёл через гугл, потому что не использовал при запросе аббревиатуру.

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