I want to provide the community with a translation of my article on CodeProject, in which I describe the process of creating a DSLay using the F # language.
To be honest, I was pretty much fed up with talking about DSLs in a purely academic manner. I would like to see a concrete example of how this happiness is used “in production”. Anyway, the concept itself can be explained and implemented much more intelligibly and straightforwardly than the authors of such frameworks as Oslo or MPS. Actually here I just want to show a solution that is not at all academic but a production one, and serves specific purposes.
Let's start by discussing what DSL is. DSL is a domain-specific language — that is, a way of describing a particular subject specificity (which is often associated with a specific industry) using a language that not only developers, but also subject matter experts can understand. It is important in this language that those who use it should not think about curly brackets, semicolons, and other delights of programming. That is, they should be able to write in “simple English” (Russian, Japanese, etc.)
In this essay, we will use the F # language to write DSL and which helps us to make an assessment of the complexity of projects. A more sophisticated version of this DSL is used in our production. I’ll say right away that the code that I’ll show is far from a perfect example of using F #, so I’ll ignore all the “stones in the garden” in terms of programming style. The point is not the point. However, if there is a desire to optimize - please.
Oh yeah, and another thing - I will immediately give a reference to the original article and the source code . The code is essentially one .fs
file. I hope you can compile it. In order to evaluate how it works, you will need Project 2007. If you don’t have it, ask a nearby PM.
So go!
When someone needs custom software, this someone (usually referred to as "customer") sends to various companies the so-called RFP (request for proposal), that is, in essence, a description of their project. Developers make a project plan for this request (if information is enough - if not, they start communicating), they pack it into a beautiful PDF and send it back, and naturally the faster the assessment is done (estimate), the better it is and the better it is presented more likely that the client will communicate with you. It turns out that in the interests of the entire company to make this estimate well and quickly.
Someone should do this estimate ... usually the “extreme” is some kind of relaxing PM music, who knows enough technological stack and has at least a little bit of experience to estimate that (the peer review mechanism, if it is adjusted, will smooth out all his shoals). So, our RM should evaluate the stages of the project and make a beautiful timeline (it seems this is called the GANTT chart) in order to visually show what the efforts of developers, testers, and their own will go. There is a problem.
The problem is that MS Project, the tool of which this happiness is created, is not very quick to rise when it is necessary to constantly restructure estimates, change jobs, adjust resources, overhead, well, etc. Everything becomes too stressful, especially if you adhere to the rule that “every client should get an estimate within one day of his time zone”. We have to dodge, and our DSL is an attempt to simplify and speed up the evaluation activities for all participants.
We have described the problem, now about the solution. In principle, to describe the project, you can make a "free" DSL where you can use any syntax and then parse it using smart frameworks, but this is somehow boring if you consider that these frameworks will not add anything to the result, but they will surely bring some headache. Therefore, a simpler approach would be to choose a language (in our case, a language in the .Net stack) that allows writing in “almost English” and will not strain non-technical staff (although if RM cannot program, then this is not for us).
Of the popular languages ​​for DSL, of course you need to mention Boo, who well propyaril Ayende in his book . Boo is a very powerful language, but in this case we will not need its metaprogramming power. There is also Ruby, which is also popular in terms of DSL, but unfortunately I don’t know it (unfortunate omission), so I can’t recommend it. Well, the last choice I chose is F #.
Why is F # good for DSLay? Because its syntax does not burden the mind. You can write almost pure English. DSL is readable by anyone. The only problem is that F # is oriented towards immutability variables, so in our context some of its constructs will look a bit unnatural. But, as I have already said, this is not the point - after all, DSL is just a transformer, “conduit of consciousness”.
Let's start with the simple. Here is the first line in the project description:
project "Write F# DSL Article" starts_on "16/8/2009" <br/>
What you see above is a completely legal expression in F #. We simply call the project
method and pass it three parameters — the name of the project, a certain token (a dummy that serves as anglo-syntactic sugar), and the start time of the project. In fact, we do about the same things that they do with tests in BDD - namely, they try to make them readable for non-technicals.
The DSLina we write is itself based on OER. Our goal is to support through DSL all those constructions that RMs are used to. One of these structures is a project, so perhaps we will begin with it:
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/>
Here, I warned that F # will not look too smart if writing with support for mutability. The weird structures above are public fields that can be changed. In terms of collections, I used F # ovsky list
instead of List<T>
from System.Collections.Generic
. There is no particular difference.
Unlike C #, in F # we have something that at first glance can be called a “global scope”, that is, variables and functions can be declared “at the top level”, without any explicitly described classes, modules and namespaces. Let's take advantage of this immediately:
<br/>
let mutable my_project = new Project()<br/>
We have just created a “default project variable”. Naturally, the terminology in F # is slightly different, but not the essence. We chose a name so that at the end it could be pathetic to write prepare my_project
and run the autogeneration of the project plan. In the meantime, let's look at the project
function, with which it all begins.
<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/>
Here you go. In principle, at this stage you can safely throw an article and go to experiment - after all, we have just shown the whole essence of creating F # DSLay. Next will be the analysis of semantics and the actual demonstration of how various subtleties are settled.
Work in the project is performed by resources, that is, by people. You are a resource, and I am a resource - not very pleasant, is it? However, each resource has a certain title (for example, “Junior Developer”), a name (“John”) and also a rate - how many dollars per hour the company wants to receive per month for the work of this resource. Let's first look at the definition of this resource itself:
type Resource() =<br/>
[<DefaultValue>] val mutable Name : string<br/>
[<DefaultValue>] val mutable Position : string<br/>
[<DefaultValue>] val mutable Rate : int<br/>
Now you can look at how resource creation will look in our DSL:
resource "John" isa "Junior Developer" with_rate 55<br/>
Of course, to support the expression above, we use the same shamanism as for projects, namely:
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/>
As you may have guessed, we create a resource and add it to the top of the list. This means that when the time comes to “build” resources and other elements that are stored in lists, each list will have to be expanded backwards. This is not a problem for me, but if you don’t like it, use List<T>
.
The next concept in our DSL is task groups. A group of tasks in a project is usually performed by one person, which contributes to the maintenance of a “cognitive focus.” We define the group like this:
group "Project Coordination" done_by "Dmitri" <br/>
And here is the object that contains data about the group:
type Group() =<br/>
[<DefaultValue>] val mutable Name : string<br/>
[<DefaultValue>] val mutable Person : Resource<br/>
[<DefaultValue>] val mutable Tasks : Task list<br/>
You see - the group refers to an object of type Resource
, and we pass the name (string). But this is not a problem, since nobody canceled the search in the lists:
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/>
Unlike LINQ, you do not need to call Single()
to get the search result.
Task groups (tasks) consist of, ummm, tasks. And the task is not bad to define like this:
task "PayPal Integration" takes 2 weeks<br/>
This is also real in F #! To begin with, we make sure that the tokens that we usually use for “sugar” contain distinct values:
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/>
Now we can define our Task
:
type Task() =<br/>
[<DefaultValue>] val mutable Name : string<br/>
[<DefaultValue>] val mutable Duration : string<br/>
And adding a task to the group looks like this:
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/>
In the code above, depending on the time constant, we adjust the duration of the task. In order to find the group to which we need to add a task, we use List.hd
- after all, the groups are also backwards.
Well that's all! Now we can call one pompous command to generate our project plan:
prepare my_project<br/>
Next comes the most difficult piece - using Office Automation and F # in tandem to generate a plan from our DSLK. I tried to comment on the code so that it was clear what was happening.
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/>
Well, we “expanded” the lists using List.rev
- not the fastest operation, of course, but it doesn’t matter. The main thing is that the script works and generates projects - it defines the resources, task groups and tasks themselves. What else does RM need? (Actually a lot of things :)
And this is how a complete project description can look like using our 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/>
I hope this essay showed you that doing DSL in F # is easy. Of course, the example that I gave above is simplified compared to what we actually use. But this, as they say, the secrets of the company. See you again!
Source: https://habr.com/ru/post/68313/
All Articles