Since VAX is not bounded to any specific domain language — you create your own!
Table of contents:
An advanced schema skeleton has following sections:
colors:
# Color aliases for future referencing
types:
# Define types, their inheritance and type parameters
groups:
# Define groups of components
components:
# Define your components and their structure
dictionaries:
# Define key-value dictionaries for using in value pickers
No section is required, but obviously you'll want to define two main sections, which are types
and components
.
Here you define a list of color aliases that can be referenced throughout the remaining schema:
colors:
relation: "#cf3"
table: "#0ff"
coreNode: "0-#f80-#da0:50-#520"
tableNode: "0-#0ff-#0dd:20-#111"
types:
FromClause:
color: @relation # Will be substitued with "#cf3"
title: FROM
components:
Select:
group: core
title: Запрос (SELECT)
color: @coreNode # Here we use a color alias again
# ...
Color formats are enlisted at RaphaelJS docs (bottom section of Element.attr()
).
Types in VAX are somewhat similar to the types that inhabit statically-typed programming languages, like Java/C++/Scala. Especially Scala :)
Where do you use types? Nodes in VAX have input and output sockets. The type of an output socket must conform to the corresponding type of the input socket. Types help to build valid blueprints, which are easier to validate and interpert. They also guide end users when they're blueprinting.
A VAX type can either extend others or be parameterized:
types:
# Any is a supertype
Expr: # Expr extends Any
color: "#fff" # The wires of this type will be colored as "#fff"
title: Expression # Provides a meaningful title, that'll be displayed on a blueprint
Integral:
color: @num # Use color aliases for your convenience
extends: Expr # Integral extends Expr and Any
Numeric:
extends: Integral # Numeric extends Integral => Numeric extends Expr, and Numeric also extends Any
color: @num
String:
extends: [Expr]
color: @str
title: Text
NumericString:
extends: [Numeric, String] # Here we define multi-inheritance
color: @str
title: Text with a number
List:
typeParams: [A] # This is like a type constructor, so we'll have to specify 'A' with a real type, e.g. List[Numeric]
color: @arr
Map:
typeParams: [A,B] # Yep, we can have multiple type parameters, e.g. Map[String,Numeric], but I doubt that you'll need one :)
Apparently, you extend types from others and define paremetrized types. Also don't forget to set colors and titles.
Let's see how components use types:
components:
ToString: # def ToString(I:Any):String = I.toString
in:
I: Any # Just a usual type
out:
O: String
Repeat: # def Repeat[T](I: T):T = I
typeParams: [T] # The editor will ask you to specify the type parameter 'T'
in:
I: @T # Uses the specified parameterised type
out:
O: @T
Plus: # def Plus[T <: Expr](A: T, B: T): T = A + B
typeParams: [T]
typeBounds: {T: {<: Expr}} # Type parameter 'T' is upper bounded by type 'Expr'
# which means we have to provide a type that inherits from 'Expr'
in:
A: @T
B: @T
out:
O: @T
ListLength: # def ListLength(L: List[Any]):Numeric = L.size
in:
L: List[Any] # Don't forget about the supertype Any
out:
O: Numeric
ZipWithIndeces: # def ZipWithIndices[T](A: List[T]):Map[Numeric,T] = ...
typeParams: [T]
in:
A: List[@T]
out:
M: Map[Numeric,@T] # Crazy stuff!
While you are defining components you reference types within input and output sockets. You can also have type parameters on a particular component and the use it by alias, e.g. @T
.
The weird syntax typeBounds: {T: {<: Expr}}
in terms of YAML means:
typeBounds:
T:
"<": Expr
and defines an upper type bound from 'Expr' for type parameter 'T'.
When a user wants to create a node from a component that has type parameters, the editor will ask to specify them, and then will substitute the type aliases with the real types. E.g. for a component:
IfThenElse:
typeParams: [T]
in:
Condition: Boolean
onTrue: @T
onFalse: @T
out:
O: @T
We get:
When the number of components increases, it gets hard to navigate through them. You can put components into related groups, which are then displayed in component selector:
groups:
core: Core elements
ops: Basic calculations
bool: Logic operations
str: Working with strings
components:
Result:
group: core # The component 'Result' is now in the group 'core'
in:
I: Any
ToString:
group: str # Putting 'ToString' into to the string operations group, 'str' :)
in:
I: Any
out:
O: String
If you don't specify the group of a component, it gets placed into the group 'Uncategorized', which is one of the system groups:
return _.defaults(schema.groups, {
'_default': 'Uncategorized',
'_userFunctions': 'User functions',
'_ufElements': 'User functions elements'
});
It's recommended that you don't use leading underscore in your group names.
Components are building blocks of your language or domain. In conjunction with the types, you can create a very rich language. You can use type system constraints to be sure you get valid blueprints. End users can go even further and organise basic components into functions and reuse them later, thus enriching the design language.
So a component is a template for a real blueprint node. The template contains: input and output sockets definitions, attributes definitions, type parameters with type bounds and of course stuff like title, color and group. See the editor section for a visual reference.
A minimal component definition doesn't really require anything:
components:
Useless: # A unique name, the one you'll see within serialized tree
In order to be useful a node needs at least one input or output socket definition:
components:
ToString:
in: # input socket section
I: Any # We define an input with a name 'I' that accepts a value of type 'Any'
out: # output socket section
O: String # We define an ouput with name 'O' that produces a value of type 'String'
Socket names should be unique within their respective section.
You can also give titles to your sockets:
components:
Plus:
in:
L:
title: Left
type: Expr
R:
title: Right
type: Expr
out:
R:
title: Result
type: Expr
To make your component more user-friendly, you can give it a title, a color and a group:
components:
Result:
title: Resulting value # You'll see that in the editor
color: @coreNode # The color of your node, color aliases are allowed
group: core # The name of the group, which contains this component
in:
I: Any
Sometimes you need to provide a node with an attribute that can be set by a user on the blueprint. Minimal attribute definitions consist of a name and a type:
components:
NumberLiteral:
attrs: # attributes section
V: Numeric # Unique attribute name within component
out:
O: Numeric
It may seem that types don't make sense for attributes, and it's true. In fact they don't affect anything at all, for now. They're reserved for future use,
like "socketable" attributes and the ability for users to define attributes in the user functions.
A good rule of thumb is to use closest meaningful type or just resort to Any
.
As you might expected attributes can be configured with a title and a default value:
components:
NumberLiteral:
attrs:
V:
type: Numeric
title: Value
default: 1
out:
O: Numeric
By default, for modifying an attribute's value, the editor uses just a simple <input>
element. But you can define your own value pickers. E.g. there's a built-in value picker called
dictionary
, which requires a defined dictionary in your schema:
components:
TrigonometricFn:
title: Trigonometic function
in:
I: Numeric
attrs:
Fn:
type: Any
title: Function
default: sin
valuePicker:
type: dictionary
dictionary: TrigonometricFunctions
out:
O: Numeric
dictionaries:
TrigonometricFunctions: # Referenceable unique name of a dictionary
title: Trigonometic functions of a single argument
values: # Name -> Title
sin: sin(X)
cos: cos(X)
tg: tan(X)
ctg: cot(X)
In order to register your own value picker you have to resort to the registerValuePicker(type, valuePicker)
function on a VAX
object, e.g.:
// we have to provide an object with two functions: 'getValueTitle' and 'invoke'
var promptValuePicker = {
getValueTitle: function(value, options) // used to map your plain value to a title, options are passed from your schema
{
return value.toString();
},
invoke: function(value, callback, options) // 'value' holds a current state of an attribute, 'options' are passed from schema
{
val newValue = prompt(options.promptMessage, value);
callback(newValue); // don't forget to trigger callback with a new value from your picker
}
}
var myVAX = new VAX('containerId', {schema: /*...*/}); // create your VAX object
myVAX.registerValuePicker('prompt', promptValuePicker); // 'prompt' will be our picker name
With this value picker defined, we can use it in our schema:
components:
PromptNumber:
attrs:
V:
type: Numeric
title: Value
default: 1
valuePicker: # this is what is passed as 'options'
type: prompt # picker name
promptMessage: "Provide a number, please ... "
out:
O: Numeric
Attributes may be substituted with input sockets. Also future versions of the editor will provide input sockets that can be set with default values just like attributes.