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

Если честно, мне уже изрядно поднадоели разговоры о DSLях в чисто академическом ключе. Хочется увидеть конкретный пример того, как это счастье используется «в продакшн». Да и вообще, саму концепцию можно объяснить и реализовать намного более доходчиво и прямолинейно чем делают авторы таких фреймворков как Oslo или MPS. Собственно тут я как раз и хочу показать решение которое вовсе не академическое а именно производственное, и служит конкретным целям.
Начнем с того, что обсудим что же такое DSL. DSL – доменно специфичный язык – то есть способ описания той или иной предметной специфики (которая часто связана с конкретной индустрией) с помощью такого языка, который могут понять не только разработчики, но и эксперты в предметной области. Важно в этом языке то, что те кто его используют не должны думать о фигурных скобочках, точках с запятой и прочих прелестях программирования. То есть у них должна быть возможность писать на «простом английском» (русском, японском, и т.д.)
В этом очерке мы будем использовать язык F# для написания DSLи которая помогает нам делать оценку трудоемкости проектов. Более заумная версия этой DSLины используется у нас на производстве. Сразу скажу, что тот код который я покажу далеко не идеальный пример использования F#, так что все «камни в огород» в плане стиля программирования буду игнорировать. Суть-то не в этом. Впрочем, если есть желание пооптимизировать – пожалуйста.
Ах да, и вот еще что – сразу дам ссылочки на оригинал статьи и исходный код. Код – это по сути дела один
Итак, в путь!

Когда кому-то нужно заказное ПО, этот кто-то (обычно именуется «заказчик») шлет разным фирмам так называемый 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 это всего лишь трансформатор, «кондуит сознания».

Начнем с простого. Вот как выглядит первая строчка в описании проекта:
То что вы видите выше – совершенно легальное выражение в F#. Мы просто вызываем метод
DSLина которую мы пишем сама по себе основана на ООР. Наша цель – через DSL поддержать все те конструкции, к которым привыкли РМы. Одна из этих конструкций – проект, поэтому с него пожалуй и начнем:
Вот, я же предупреждал что F# не будет смотреться слишком шикарно если писать с поддержкой mutability. Странные конструкции выше – это публичные поля, которые можно изменять. В плане коллекций я воспользовался F#овским
В отличии от C#, в F# у нас есть нечто, что на первый взгляд можно именовать «global scope», то есть декларировать переменные и функции можно как бы «на верхнем уровне», без всяких явно описанных классов, модулей и пространств имен. Давайте этим незамедлительно воспользуемся:
Мы только что создали «переменную дефолтного проекта». Естественно что терминология в F# немного другая, но не суть. Имя мы выбрали такое, чтобы в конце можно было пафосно написать
Ну вот. В принципе, на этом этапе можно смело бросать читать статью и идти экспериментировать – ведь всю суть создания DSLей на F# мы только что показали. Дальше будет разбор семантики и собственно демонстрация того, как разруливаются разные тонкости.

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

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

Группы тасков (заданий) состоят из, эммм, заданий. А задание неплохо определять вот так:
Это тоже реально в F#! Для начала, мы делаем так, чтобы те токены которые мы обычно используем для «сахара» содержали внятные значения:
Теперь мы можем определить наш
А добавление таска в группу выглядит вот так:
В коде выше мы в зависимости от временной константы подстраиваем продолжительность таска. Для того чтобы найти ту группу, в которую нужно добавить таск, мы используем

Ну вот и все! Теперь мы можем вызвать одну помпезную комманду чтобы сгенерировать наш проектный план:
Далее идет самый сложный кусочек – использование Office Automation и F# в тандеме для генерации плана из нашей DSLки. Я постарался прокомментировать код чтобы было понятно что к чему.
Ну вот мы и «развернули» списки с помощью
А вот как может выглядеть полное описание проекта с использованием нашей DSL:

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



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