Went is a wrapper library over purescript-gojs, a library containing bare bindings to GoJS. It supports defining diagrams declaratively with a monadic DSL that looks similar to GoJS' make functionality.
The motivation for Went is twofold:
- To support writing GoJS diagrams in PureScript applications. (duh)
- As long as we're doing that, to address shortcomings of JavaScript and TypeScript via added type safety. For instance,
Panels in GoJS that are of typeAutoshould not have theirisOppositeproperty ever set - this is not currently expressible in that library, whereas in Went it is, by moving as much information as possible to the type level.
Consider the following typical GoJS code:
function nodeStyle() {
return [
new go.Binding("location", "loc", go.Point.parse).makeTwoWay(go.Point.stringify),
{
locationSpot: go.Spot.Center
}
];
}
function textStyle() {
return {
font: "bold 11pt Lato, Helvetica, Arial, sans-serif",
stroke: "#F8F8F8"
}
}
const $ = go.GraphObject.make;
const myNode = $(go.Node, "Table", nodeStyle(),
{rowCount: 2},
$(go.Panel, "Spot",
$(go.Shape, "Circle",
{ desiredSize: new go.Size(70, 70), fill: "#282c34", stroke: "#09d3ac", strokeWidth: 3.5 }),
$(go.TextBlock, "Start", textStyle(),
new go.Binding("text"))
))The make function is variadic and takes many different kinds of arguments: JavaScript class-inheriting objects, plain objects/records, arrays, strings, Bindings, etc.
In Went, the "variadicity" corresponds to monadic actions, and this code translates to the following:
nodeStyle = do
binding @"location" @"loc" (Just Point.parse_) (Just Point.stringify_)
set { locationSpot: Spot.center }
textStyle = set
{ font: "bold 11pt Lato, Helvetica, Arial, sans-serif"
, stroke: "#F8F8F8"
}
myNode = node @Table' $ do
nodeStyle
set {rowCount: 2}
panel @Spot' $ do
shape Circle $ do
set { desiredSize: SizeBoth 70.0, fill: "#282c34", stroke: "#09d3ac", strokeWidth: 3.5 }
textBlock "Start" $ do
textStyle
binding1 @"text"There's several things to notice:
"Table"andSpothave gone from plain strings passed to calls tomakein the context of making aNodeand aPanelrespectively to types which are applied to the functionsnodeandpanel. This allows us to define row types in the implementation of each of these constructors' that respect certain sets of fields and not others.Shape's argument went from a string to a sum type.- Plainly passing a record to
makecorresponds to calling the functionsetwith that record as an argument. - The arguments to the creation of
Bindingobjects are now function calls; whereas the constructorBinding()in GoJS expects strings as its arguments,Went's version expects type-level strings (denoted by the@symbol before the string). This allows us to onlybindactual properties to actual fields of aModel'snodeDataArray's elements.
Furthermore, because myNode has as its layout the Table layout, it has access to certain fields like rowCount :: Int. GraphObjects in its visual tree can also have other fields, like row :: Int, which determines which row of a table the object should be rendered in. In regular GoJS, the fact that these fields only make sense in the context of a Table Panel and its children is not expressible; in Went, trying to set rowCount in a Panel of any other type results in a compilation error!
Several more examples can be found in this repo.
- Not everything can be declarative. GoJS diagrams can be customized extensively. One of the ways this is done is by passing functions as properties of certain objects, an example is
mouseEnterofGraphObjectobjects. Because these fields are sent to the FFI, they must run in the plainEffectmonad and therefore rely onpurescript-gojsto perform their logic, where Went's interface and added type safety is not present, and the code is necessarily very imperative. - Bundle sizes are currently quite large, especially when used with frameworks like Halogen. This is a general problem in PureScript.
- Type errors, especially due to type inference, can be mysterious: when in doubt, try to give type annotations and use the PureScript compiler's built-in linting to generate them. For example, often when dealing with
purescript-gojs-related code, types MUST be given explicitly, usually when a function is polymorphic in its output type. This is a consequence of GoJS's heavily objected-oriented design. Went andpurescript-gojscode "wants" to be monomorphic in order to be compatible with this.
This library is in an experimental phase. We're still ironing out several of its details and filling them in.
- "Monadic" fields setting - for example, creating a
GraphObjector anAdornmentfor aShapeor aPart(examples:pathPatternandselectionAdornmentTemplate) is not supported yet; only "plain" fields (numbers, booleans, sum types etc). This can already go quite a long way however. - Inheriting from classes can be modeled via modifying the behavior of its methods with the
Overrideconstruct, but if an intended subclass has extra fields for example, then it will need its ownSettableinstance and possiblyIsPanel,IsNodeetc. GoJS only contains examples of extending Tools, Layouts and Links, but in principle any class can be inherited from since Typescript is object-oriented - our aim (at least with this release) is to support the most common use cases. - Several fields/methods from Diagram (and therefore Overview and Palette) are not yet supported; in particular, Diagram's
attachmethod (whose argument can also be given as an argument to the constructor) supports many type-checked deeply nested properties. - Several fields are not polymorphic enough; i.e. they do not use the existential approach delineated above.
- There's a lot of documentation missing (prototypes, overrides, the different
Make*monads), but hopefully the extensive GoJS docs can fill in a lot of the missing pieces.
Not supported:
- Defining custom panel layouts
- Extending Links, LayoutNetworks
- Nested records in Settable instances must be filled out totally
- Diagram's settable instance is not type safe, it accepts anything due to us wanting to nest sets very deeply sometimes