lang: Add class and include statements

This adds support for the class definition statement and the include
statement which produces the output from the corresponding class.

The classes in this language support optional input parameters.

In contrast with other tools, the class is *not* a singleton, although
it can be used as one. Using include with equivalent input parameters
will cause the class to act as a singleton, although it can also be used
to produce distinct output.

The output produced by including a class is actually a list of
statements (a prog) which is ultimately a list of resources and edges.
This is different from functions which produces values.
This commit is contained in:
James Shubin
2018-06-12 14:44:29 -04:00
parent 83dab30ecf
commit c62b8a5d4f
7 changed files with 1160 additions and 28 deletions

View File

@@ -85,8 +85,9 @@ These docs will be expanded on when things are more certain to be stable.
There are a very small number of statements in our language. They include:
- **bind**: bind's an expression to a variable within that scope
- **bind**: bind's an expression to a variable within that scope without output
- eg: `$x = 42`
- **if**: produces up to one branch of statements based on a conditional
expression
@@ -114,6 +115,31 @@ expression
File["/tmp/hello"] -> Print["alert4"]
```
- **class**: bind's a list of statements to a class name in scope without output
```mcl
class foo {
# some statements go here
}
```
or
```mcl
class bar($a, $b) { # a parameterized class
# some statements go here
}
```
- **include**: include a particular class at this location producing output
```mcl
include foo
include bar("hello", 42)
include bar("world", 13) # an include can be called multiple times
```
All statements produce _output_. Output consists of between zero and more
`edges` and `resources`. A resource statement can produce a resource, whereas an
`if` statement produces whatever the chosen branch produces. Ultimately the goal
@@ -215,6 +241,82 @@ to express a relationship between three resources. The first character in the
resource kind must be capitalized so that the parser can't ascertain
unambiguously that we are referring to a dependency relationship.
#### Class
A class is a grouping structure that bind's a list of statements to a name in
the scope where it is defined. It doesn't directly produce any output. To
produce output it must be called via the `include` statement.
Defining classes follows the same scoping and shadowing rules that is applied to
the `bind` statement, although they exist in a separate namespace. In other
words you can have a variable named `foo` and a class named `foo` in the same
scope without any conflicts.
Classes can be both parameterized or naked. If a parameterized class is defined,
then the argument types must be either specified manually, or inferred with the
type unification algorithm. One interesting property is that the same class
definition can be used with `include` via two different input signatures,
although in practice this is probably fairly rare. Some usage examples include:
A naked class definition:
```mcl
class foo {
# some statements go here
}
```
A parameterized class with both input types being inferred if possible:
```mcl
class bar($a, $b) {
# some statements go here
}
```
A parameterized class with one type specified statically and one being inferred:
```mcl
class baz($a str, $b) {
# some statements go here
}
```
Classes can also be nested within other classes. Here's a contrived example:
```mcl
class c1($a, $b) {
# nested class definition
class c2($c) {
test $a {
stringptr => printf("%s is %d", $b, $c),
}
}
if $a == "t1" {
include c2(42)
}
}
```
Defining polymorphic classes was considered but is not currently allowed at this
time.
Recursive classes are not currently supported and it is not clear if they will
be in the future. Discussion about this topic is welcome on the mailing list.
#### Include
The `include` statement causes the previously defined class to produce the
contained output. This statement must be called with parameters if the named
class is defined with those.
The defined class can be called as many times as you'd like either within the
same scope or within different scopes. If a class uses inferred type input
parameters, then the same class can even be called with different signatures.
Whether the output is useful and whether there is a unique type unification
solution is dependent on your code.
### Stages
The mgmt compiler runs in a number of stages. In order of execution they are:
@@ -596,6 +698,39 @@ someListOfStrings := &types.ListValue{
If you don't build these properly, then you will cause a panic! Even empty lists
have a type.
### Is the `class` statement a singleton?
Not really, but practically it can be used as such. The `class` statement is not
a singleton since it can be called multiple times in different locations, and it
can also be parameterized and called multiple times (with `include`) using
different input parameters. The reason it can be used as such is that statement
output (from multple classes) that is compatible (and usually identical) will
be automatically collated and have the duplicates removed. In that way, you can
assume that an unparameterized class is always a singleton, and that
parameterized classes can often be singletons depending on their contents and if
they are called in an identical way or not. In reality the de-duplication
actually happens at the resource output level, so anything that produces
multiple compatible resources is allowed.
### Are recursive `class` definitions supported?
Recursive class definitions where the contents of a `class` contain a
self-referential `include`, either directly, or with indirection via any other
number of classes is not supported. It's not clear if it ever will be in the
future, unless we decide it's worth the extra complexity. The reason is that our
FRP actually generates a static graph which doesn't change unless the code does.
To support dynamic graphs would require our FRP to be a "higher-order" FRP,
instead of the simpler "first-order" FRP that it is now. You might want to
verify that I got the [nomenclature](https://github.com/gelisam/frp-zoo)
correct. If it turns out that there's an important advantage to supporting a
higher-order FRP in mgmt, then we can consider that in the future.
I realized that recursion would require a static graph when I considered the
structure required for a simple recursive class definition. If some "depth"
value wasn't known statically by compile time, then there would be no way to
know how large the graph would grow, and furthermore, the graph would need to
change if that "depth" value changed.
### I don't like the mgmt language, is there an alternative?
Yes, the language is just one of the available "frontends" that passes a stream