Skip to content

Latest commit

 

History

History
438 lines (277 loc) · 17.5 KB

README.md

File metadata and controls

438 lines (277 loc) · 17.5 KB

dmnmd: A Command-Line Interface to DMN Decision Tables In Plain Text

Haskell

Show me your flowchart and conceal your tables, and I shall continue to be mystified. Show me your tables, and I won't usually need your flowchart; it'll be obvious." -- Fred Brooks, The Mythical Man Month (1975)

Inspiration

Watch this real quick: https://youtu.be/Pe34U9QuhXA ... Wouldn't it be nice to be able to do this in your text editor?

DMNMD embeds DMN in plain text source code or Markdown

The semantics are DMN.

The syntax is Markdown.

The input is plain-text.

The output is JS. (And, in future, XML, Python, English, LegalRuleML...)

The interface is CLI. No mouse needed!

Installing from Source

At the moment, dmnmd is an executable program written in Haskell. In future it may switch to Python.

OS X:

brew install haskell-stack pkg-config pcre; stack upgrade

Linux:

{ yum, apt-get, ... } install haskell-stack pkg-config libpcre3-dev; stack upgrade

Both:

git clone [email protected]:smucclaw/dmnmd.git
cd dmnmd/languages/haskell
stack test
stack build
stack install

In future packaged binaries will be made available.

Examples

This README contains decision tables formatted in plain text, in Markdown table syntax.

The DMN CLI (dmnmd) interpreter parses, evaluates, and translates them into alternative formats.

Example 1: What's for dinner?

This example is taken from Camunda's DMN Tutorial.

U Season Dish # Annotation
1 Fall Spareribs
2 Winter Roastbeef
3 Spring Steak
4 Summer Light Salad and a nice Steak Hey, why not?

A plain text version formatted for Markdown looks literally like this:

| U | Season | Dish                         | # Annotation  |
|---|--------|------------------------------|---------------|
| 1 | Fall   | Spareribs                    |               |
| 2 | Winter | Roastbeef                    |               |
| 3 | Spring | Steak                        |               |
| 4 | Summer | Light Salad and a nice Steak | Hey, why not? |

Example 2: How many guests are coming?

U Season Guest Count Dish (out) # Annotation
1 Fall <= 8 Spareribs
2 Winter <= 8 Roastbeef
3 Spring <= 4 Dry Aged Gourmet Steak
4 Spring [5..8] Steak
5 Fall, Winter, Spring > 8 Stew
6 Summer - Light Salad and a nice Steak Hey, why not?
| U | Season               | Guest Count | Dish (out)                   | # Annotation  |
|---|----------------------|-------------|------------------------------|---------------|
| 1 | Fall                 | <= 8        | Spareribs                    |               |
| 2 | Winter               | <= 8        | Roastbeef                    |               |
| 3 | Spring               | <= 4        | Dry Aged Gourmet Steak       |               |
| 4 | Spring               | [5..8]      | Steak                        |               |
| 5 | Fall, Winter, Spring | > 8         | Stew                         |               |
| 6 | Summer               | -           | Light Salad and a nice Steak | Hey, why not? |

Yeah, DMN allows spaces in variable names. What could possibly go wrong?

But if you think of them as properties in a dictionary, that's not so bad.

XML source

The canonical DMN XML representation of this example is available at https:/camunda/camunda-bpm-examples/blob/master/dmn-engine/dmn-engine-java-main-method/src/main/resources/org/camunda/bpm/example/dish-decision.dmn11.xml

Background

Decision Model & Notation is an XML-based standard from OMG. One accessible tutorial is available here.

To help author DMN, a number of vendors provide graphical user interfaces as part of their conforming implementations. It is also possible to import decision tables authored in a spreadsheet.

What are decision tables? An ancient magic from an earlier age of computing, powerful but little known among developers today. See Hillel Wayne's introduction. It may be making a comeback, though: a handful of packages have appeared on npm in the last few years.

The Unix philosophy emphasizes the value of flat text files. While XML technically qualifies as text, many consider it "unwieldy": hence the popularity of JSON and YAML.

Command-line utilities such as json (on NPM) help manipulate JSON. dmnmd is intended to be the moral equivalent for manipulating DMN in Markdown.

Imports

from Markdown

Supported. This is the native format for dmnmd.

ASCII has its limitations. In graphical decision tables, output columns are separated from input columns by a double bar; most GUI implementations use colour and other formatting to distinguish input, output, and annotation columns. In dmnmd syntax, output columns are optionally labeled with an (out); annotation columns are prefixed with a #. By default, if the columns are unlabeled, the rightmost column will be taken to be the output, and columns to the left will be taken to be inputs. (Leaving out annotation columns.)

You can also prefix output columns with a > character.

Columns are optionally typed using a colon. You will see Column Name : String, Column Name : Number, and Column Name : Boolean. If you omit the type definition, dmnmd will attempt to infer the type.

In some decision tables, the input and outputs are enumerated in a sort of sub-header row. The order matters.

This implementation only supports vertical layout. Horizontal and crosstab layouts may appear in a future version if there is demand.

The above is perhaps best explained by an example; see figure 8.19 of the DMN 1.3 spec.

Example 3: Routing Rules

For hit policy "O", the order of results in the output is determined by the order of the column enums.

The column enums are giving in a subhead row between the top row and body data row "1".

O Age Risk Category Debt Review :Boolean > Routing > Review level Reason (out)
LOW, MEDIUM, HIGH DECLINE, REFER, ACCEPT LEVEL 2, LEVEL 1, NONE
1 - - - ACCEPT NONE Acceptable
2 <18 DECLINE NONE Applicant too young
3 HIGH REFER LEVEL 1 High risk application
4 True REFER LEVEL 2 Applicant under debt review

This example comes from the DMM 1.3 specification, page 96.

Note that advanced hit policies are not yet implemented for code generation, only for evaluation.

In the IDE

JetBrains MPS is a language workbench and an IDE from the future. Look how decision tables live right in the IDE: https://www.youtube.com/watch?v=Pe34U9QuhXA

from XML

On the roadmap.

$ dmnmd --from=example1.dmn --to=example1.md

Exports

Interactive evaluation is intended for quick testing in development. For real-world use, you probably want code generation to an operational language like Python or Javascript. Or extraction to SQL or JSON (suitable for NoSQL) or to XML (as OMG intended). Or to natural language!

to Typescript

$ dmnmd README.md --to=ts

By default, generates Typescript.

You can output Javascript instead by saying --to=js

Options:

--props Normally, functions expect as many parameters as there are input columns. with --props, functions expect input in a single props object; a "Props" type is generated.

% stack exec -- dmnmd README.md --to=ts --pick="Example 2" -r
type Props_Example_2 = {
    "Season" : string;
    "Guest Count" : number;
}
type Return_Example_2 = {
    "Dish" : string;
}
export function Example_2 ( props : Props_Example_2 ) : Return_Example_2 {
  if (props["Season"]==="Fall" && props["Guest Count"] <=8.0) { // 1
    return {"Dish":"Spareribs"};
  }
  else if (props["Season"]==="Winter" && props["Guest Count"] <=8.0) { // 2
    return {"Dish":"Roastbeef"};
  }
  else if (props["Season"]==="Spring" && props["Guest Count"] <=4.0) { // 3
    return {"Dish":"Dry Aged Gourmet Steak"};
  }
  else if (props["Season"]==="Spring" && (5.0<=props["Guest Count"] && props["Guest Count"]<=8.0)) { // 4
    return {"Dish":"Steak"};
  }
  else if ((props["Season"]==="Fall" || props["Season"]==="Winter" || props["Season"]==="Spring") && props["Guest Count"] > 8.0) { // 5
    return {"Dish":"Stew"};
  }
  else if (props["Season"]==="Summer") { // 6
    return {"Dish":"Light Salad and a nice Steak"};
    // Hey, why not?
  }
}

We use "props" here as a synonym for the more proper term "context".

to Javascript

This works today, modulo full support for hit policies.

% stack exec -- dmnmd README.md --pick="Example 2" --to=js
export function Example_2 ( Season, Guest_Count ) {
  if (Season==="Fall" && Guest_Count <=8.0) { // 1
    return {"Dish":"Spareribs"};
  }
  else if (Season==="Winter" && Guest_Count <=8.0) { // 2
    return {"Dish":"Roastbeef"};
  }
  else if (Season==="Spring" && Guest_Count <=4.0) { // 3
    return {"Dish":"Dry Aged Gourmet Steak"};
  }
  else if (Season==="Spring" && (5.0<=Guest_Count && Guest_Count<=8.0)) { // 4
    return {"Dish":"Steak"};
  }
  else if ((Season==="Fall" || Season==="Winter" || Season==="Spring") && Guest_Count > 8.0) { // 5
    return {"Dish":"Stew"};
  }
  else if (Season==="Summer") { // 6
    return {"Dish":"Light Salad and a nice Steak"};
    // Hey, why not?
  }
}

On the roadmap: a fully native version which allows direct evaluation of decison tables as functions. Should be about a week's worth of work, accelerated by the availability of the js-feel package.

The vision: after you npm i --save dmnmd, you can define a function dinner by saying:

const dinner = dmnmd(`
| U | Season | Dish                         | # Annotation  |
|---|--------|------------------------------|---------------|
| 1 | Fall   | Spareribs                    |               |
| 2 | Winter | Roastbeef                    |               |
| 3 | Spring | Steak                        |               |
| 4 | Summer | Light Salad and a nice Steak | Hey, why not? |
`)

You should then be able to call dinner({Season:"Fall"}) and get back {Dish:"Spareribs"}.

to Haxe

Perhaps [Haxe])(https://www.haxe.org/) can help us achieve a longer list of transpilation targets.

to XML

On the roadmap.

$ dmnmd README.md --to=xml

Exports to XML conforming to the DMN 1.3 specification.

to Flora-2

On the roadmap.

$ dmnmd README.md --to=flora2

Example 4: Grocery Delivery Boxes

This learning exercise is detailed at ex-20200527-grocery.

to other Prologs

to XLSX

On the roadmap.

$ dmnmd README.md --to=xlsx

Exports to an Excel spreadsheet.

to SQL

On the roadmap.

$ dmnmd README.md --to=sql

dmnmd outputs DDL, DML, and DQL statements suitable for SQLite, Postgres, and others:

CREATE TABLE example_1 (row_id primary key, season text, dish text, annotation text);
INSERT INTO  example_1 (row_id, season, dish, annotation) VALUES
    (1, "Fall",   "Spareribs", NULL),
    (2, "Winter", "Roastbeef", NULL),
    (3, "Spring", "Steak",     NULL),
    (4, "Summer", "Light Salad and a nice Steak", "Hey, why not?");

-- Query
SELECT row_id, dish, annotation FROM example_1 WHERE Season = ?;

What to do about rows that contain FEEL expressions? For lack of a better place, that logic goes into the query.

to Python

On the roadmap.

$ dmnmd README.md --to=python

to Natural Language

On the roadmap.

$ dmnmd README.md --to=english --dialect=Horn

The dish is Spareribs when the Season is Fall.

The dish is Roastbeef when the Season is Winter.

The dish is Steak when the Season is Spring.

The dish is Light Salad and a nice Steak when the Season is Summer. (Hey, why not?)

Brevity is a parameter which makes the output more concise.

$ dmnmd README.md --to=english --dialect=Horn --brevity=2

The dish is Spareribs when the Season is Fall, Roastbeef in Winter, Steak in Spring, and (Hey, why not?) Light Salad and a nice Steak in Summer.

More brevity requires more tacit knowledge. This is safer when we have an accompanying ontology to refer to.

$ dmnmd README.md --to=english --dialect=Horn --brevity=3

Spareribs in the Fall; Roastbeef in Winter; Steak in Spring; and (Hey, why not?) Light Salad and a nice Steak otherwise.

It is characteristic of natural language that utterances omit "common sense" world knowledge, and employ other linguistic shorthand which is obvious to native speakers and often challenging to others.

The above dialect is Horn, which uses an "output if input" ordering . Omitting that option, we get an "input then output" ordering:

$ dmnmd README.md --to=english --brevity=3

In the Fall, Spareribs; in Winter, Roastbeef; in Spring, Steak; and in Summer, Light Salad and a nice Steak (Hey, why not?).

By default, brevity is 1.

$ dmnmd README.md --to=english

When the Season is Fall, the Dish is Spareribs.

When the Season is Winter, the Dish is Roastbeef.

When the Season is Spring, the Dish is Steak.

When the Season is Summer, the Dish is Light Salad and a nice Steak (Hey, why not?).

Some linguistic magic happens behind the scenes. Different parameters take different determiners.

See languages/gf/ for more.

to LegalRuleML

On the roadmap.

$ dmnmd README.md --to=legalruleml --brevity=4

... <lrml:...> ...

to Prolog

On the roadmap.

$ dmnmd README.md --to=prolog --brevity=4

dish("Spareribs") :- season("Fall").
dish("Roastbeef") :- season("Winter").
dish("Steak")     :- season("Spring").
dish("Light salad and a nice Steak") :- season("Summer").

Co-requisites

Your IDE may need a plugin to work with Markdown tables.

Evaluation

A decision table is basically a function. Let's run it.

Interactively, on the command line:

$ dmnmd -q README.md --pick "Example 1"
Example 1> Fall
Example 1: "Dish":"Spareribs"

Example 1> Winter
Example 1: "Dish":"Roastbeef"

Example 1> Spring
Example 1: "Dish":"Steak"

Example 1> Summer
Example 1: "Dish":"Light Salad and a nice Steak"

Example 1> ^D



$ dmnmd -q README.md --pick "Example 2"
Example 2> Fall, 7
Example 2: "Dish":"Spareribs"

Example 2> Fall, 9
Example 2: "Dish":"Stew"

Example 2> Summer, 10
Example 2: "Dish":"Light Salad and a nice Steak"

Coming soon: Batch-mode.

$ echo "Winter" | dmnmd README.md --dt="Example 1"
Roastbeef

Coming soon: JSON in, JSON out.

$ echo '{ "Season": "Winter" }' | dmnmd README.md --pick "Example 1" -j
{ "Season": "Winter", "Dish": "Roastbeef" }

Extensions

This implementation aims to extend DMN with higher-order functional programming capabilities. Input cells already can be what a functional programmer would call a "function section" -- a partially applied binary function curried to expect a single argument. Strictly speaking, DMN 1.3 output columns need to be "plain" values: strings, Booleans, and numbers. This implementation proposes to allow the same expressive range for output columns as input columns, so you could return a range, such as [20..40], if you wanted.

Paper: https://t.co/Oap8NMywyJ?amp=1 "Adding Constraint Tables to the DMN Standard: Preliminary Results"

Automated Reasoning

https://www.researchgate.net/publication/301836662_Semantics_and_Analysis_of_DMN_Decision_Tables