WebSharper Interface Generator
The WebSharper Interface Generator (WIG) is a tool (build task) for generating WebSharper bindings to JavaScript libraries. Bindings allow developers to access these libraries from typed F# code that gets compiled to JavaScript by WebSharper. While it is possible to create bindings manually, WIG allows to write the binding definitions in F#, making full use of the language to streamline repetitive tasks.
Simply put, WIG takes an F# value
representing a set of classes, interfaces, and members together with
their documentation and mappings to JavaScript, and generates a
binding assembly from that definition. The binding is a .NET assembly
containing the generated types and method stubs annotated with raw
JavaScript code using the InlineAttribute custom attribute.
Getting Started
WIG is included with the WebSharper installer. To create a new project select the "Extension" template from WebSharper templates. This project file contains the line required for the WIG build task to run.
<WebSharperProject>InterfaceGenerator</WebSharperProject>
You get a small example in the Main.fs file, which ends in this:
[<Sealed>]
type Extension() =
interface IExtension with
member ext.Assembly =
Definition.Assembly
[<assembly: Extension(typeof<Extension>)>]
do ()
This exposes the value defined by Definition.Assembly to the WIG compiler.
The library that you write is used just as a generator for this value,
any other code that it has will have no effect on the final assembly produced by the
WIG build task.
Constructing Types
Defining classes, interfaces and member signatures requires an
abstraction for types. Types are represented as
WebSharper.InterfaceGenerator.Type.IType values.
These values can describe both types from other assemblies (external) or
type definitions in current WIG project.
Immutability and Identity
Adding members or attributes to a type declaration is mutable and returns the type declaration for chaining. However, all operators and helper functions work non-desctructively on member, attribute and resource definitions. This allows mutual recursion between types:
let A = Class "A"
let B = Class "B"
A |+> Instance [
"getB" => T<unit> ^-> B
] |> ignore // "GetB" method was addded to method list of A
B |+> Instance [
"getA" => T<unit> ^-> A
] |> ignore // "GetA" method was addded to method list of B
Example for immutability of members:
let GetCount = Method "getCount" (T<unit> ^-> T<int>)
let GetCountObs = GetCount |> Obsolete
A |+> Instance [ GetCount ]
|> ignore // A will have "GetCount" without the Obsolete attribute
B |+> Instance [ GetCountObs ]
|> ignore // B will have "GetCount" with the Obsolete attribute
Operator Reference
| Function | Operator | Description |
Method |
=> |
Defines a method from name and signature |
Property |
=@ |
Defines a property with a getter and setter |
Getter |
=? |
Defines a read-only property |
Setter |
=! |
Defines a write-only property |
? |
Defines a named parameter | |
^-> |
Defines a function type | |
-* |
Defines the type of the this parameter on a function |
|
*+ |
Defines the rest parameter (ParamArray in .NET) |
|
Type.ArrayOf |
!| |
Defines an array type from its element type |
!+ |
Defines arguments parameter (single ParamArray in .NET) |
|
!? |
Defines an optional parameter, property or return type | |
* |
Defines a tuple type or joins parameters | |
+ |
Defines an overloaded parameter or a Choice property or return type |
|
|=> |
Copies type definition identifier or applies attributes | |
|+> |
Adds members to a type definition |
Side cases
When defining tuples,
*expands the tuple if the left hand argument is already a tuple. If you want to define the type(A * B) * C, you must useType.Tuple [ A * B; C ].When defining functions like
A * B ^-> C, the tuple on the left hand side is automatically converted to aType.Parameterswhich creates multiple arguments from the tuple elements. If you want to describe a JavaScript function that do take a single 2-length array as argument, you must convert it to a single parameter explicitly:(A * B).Parameter ^-> C.
External Types
External types can be defined by using the T type function, for example
T<int>, <int>, T<list<string>>, <list<string>>, T<MyOtherLibrary.SomeType>.
Type Combinators
Simpler types can be combined to form more complex types, including arrays, tuples, function types, and generic instantiations.
Type.ArrayOf T<int>
// array, equivalent to T<int[]>
// alternate syntax: !| T<int>
T<int> * T<float> * T<string>
// tuple, equivalent to T<int * float * string>
// alternate syntax: Type.Tuple [T<int>; T<float>; T<string>]
T<int> ^-> T<unit>
// function, equivalent to T<int -> unit>
T<System.Collections.Generic.Dictionary<_,_>>.[T<int>, MyTypeDef]
// adding type parameters to a generic type
// compile-time error if number of parameters do not match
In addition, delegate types can be formed. WebSharper treats delegate
types specially: their are compiled to JavaScript functions accepting
the first argument through the implicit this parameter. For example
when this can be helpful, consider following JavaScript function:
function iterate(callback, array) {
for (var i = 0; i < array.length; i++) {
callback.call(array[i], i);
}
}
To bind this function to WebSharper one needs to provide a type for
the callback parameter, which is a function called with an element
of the array passed through the this implicit parameter and the
array index passed through the first parameter. This can be achieved
thus:
let callbackType = T<obj> -* T<int> ^-> T<unit>
let iterateType = callbackType * Type.ArrayOf T<obj> ^-> T<unit>
The type of the callback is then compiled to a delegate type in F#,
Func<obj,int,unit>.
Self Placeholder
The TSelf type value will be evaluated to the type the defined member is added to.
This allows creating a member, list of members or ClassMembers value (list of members
marked with Instance or Static) and reuse it by adding it to multiple type declarations.
Defining Members
The primary use of type values is the definition of member signatures, methods, constructors, properties and fields.
Methods
Method representations are constructed using the Method (short form:
=>) combinator that takes the name of the method and the
corresponding functional type. Some examples:
let methods =
[
Method "dispose" T<unit -> unit>
Method "increment" (T<int> ^-> T<int>)
"add" => T<int> * T<int> ^-> T<int>
]
Void return types and empty parameter lists are indicated by the
unit type, multiple parameters are indicated by tuple types. It is
an error to define a method with a non-functional type.
Parameter Names
By default, method parameters get autogenerated names. You can customize parameter names as follows:
let methods =
[
Method "dispose" (T<unit>?object ^-> T<unit>)
Method "increment" (T<int>?value ^-> T<int>)
"add" => T<int>?x * T<int>?y ^-> T<int>
]
Variable-Argument Signatures
F# supports variable-argument methods via the
System.ParamArrayAttribute annotation. WebSharper understands this
annotation and compiles such methods and delegates to
variable-argument accepting functions in JavaScript. Here is the
syntax to define a variable-argument signature:
let methods =
[
"t1" => !+ T<obj> ^-> T<unit>
"t2" => T<string> *+ T<obj> ^-> T<unit>
]
When compiled to F#, these methods will have the following signatures:
val t1 : ([<System.ParamArray>] args: obj []) -> unit
val t2 : string * ([<System.ParamArray>] args: obj []) -> unit
Optional Parameters
Parameters can be made optional:
Method "exit" (!? T<string>?reason ^-> T<unit>)
Signatures such as the one above generate multiple members by implicit overloading (see below).
Implicit Overloads
Type unions facilitate describing JavaScript methods that accept arguments of either-or types. Type unions are implemented by implicit overloading of generated members. For example:
"add" => (T<int> + T<string>) * (T<obj> + T<string>) ^-> T<unit>
This method can accept either string or an int as the first
argument, and either an obj value or a string as the second. Four
overloads are generated for this signature.
Properties
Properties can be generated with a getter, a setter or both. Below are the full and abbreviated syntax forms:
let properties =
[
Getter "ReadOnly" T<int>
Setter "WriteOnly" T<int>
Property "Mutable" T<int>
]
let shorthand =
[
"ReadOnly" =? T<int>
"WriteOnly" =! T<int>
"Mutable" =@ T<int>
]
Indexed properties
Properties can have indexers. "" =@ T<string> |> Indexed T<int> creates
an indexed property where x.[n] : string translates to x[n].
If the property name is not empty:
"Lines" =@ T<string> |> Indexed T<int> creates an indexed property where
x.Lines.[n] translates to x.Lines[n].
If you define a custom inline, use $index to refer to the index parameter.
Constructors
Constructors definitions are similar to methods but do not carry a return type. Examples:
let constructors =
[
Constructor T<unit>
Constructor (T<int>?width * T<int>?height)
]
JavaScript Object Expression
ObjectConstructor (T<int>?x * T<int>?y) defines a .NET constructor with
JavaScript inline { x = $x, y = $y }.
Interfaces
Interfaces are defined using the Interface keyword and then extended
with members.
Interface "IAccessible"
Member Definitions
Member definitions are appended using the |+> combinator, for
example:
Interface "IAccessible"
|+> [
"Access" => T<unit->unit>
"LastAccessTime" =? T<System.DateTime>
]
Interface definitions take a list<CodeModel.IInterfaceMember>.
Class definitions take a CodeModel.ClassMembers value, which can be
constructed from a list<CodeModel.IClassMember> using the Instance
and Static functions.
Inheritance
Interfaces can inherit or extend multiple other interfaces. The syntax is as follows:
Interface "IAccessible"
|=> Extends [T<System.IComparable>; T<System.IEnumerable<int>>]
|+> [
"Access" => T<unit->unit>
"LastAccessTime" =? T<System.DateTime>
]
Classes
Class definition is very similar to interface definition. It starts
with the Class keyword:
let Pear =
Class "Pear"
|+> Static [
"Create" => T<unit> ^-> TSelf
]
|+> Instance [
"Eat" => T<unit->unit>
"IsEaten" =? T<bool>
]
Class Inheritance
The syntax for class inheritance is as follows:
Class "ChildClass"
|=> Inherits BaseClass
Interface Implementation
The syntax for interface implementation is as follows:
Class "MyClass"
|=> Implements [T<System.IComparable>]
Nested Classes
Class nesting is allowed:
Class "MyClass"
|=> Nested [
Class "SubClass"
]
Generics
Generic Types
Generic types and interfaces are defined by prefixing the definition
with the code of the form Generic --- fun t1 t2 t3 ->. The
t1..t3 parameters can be used as types in the definition and
represent the generic parameters. The length of the operator should
be equal to the number of parameters (up to 4).
For example:
Generic -- fun t1 t2 ->
Interface "IDictionary"
|+> [
"Lookup" => t1 ^-> t2
"ContainsKey" => t1 ^-> T<bool>
"Add" => t1 * t2 ^-> T<unit>
"Remove" => t1 ^-> T<unit>
]
This compiles to the following signature:
type IDictionary<'T1,'T2> =
abstract member Lookup : 'T1 -> 'T2
abstract member ContainsKey : 'T1 -> bool
abstract member Add : 'T1 * 'T2 -> unit
abstract member Remove : 'T1 -> unit
This syntax can produce up to 4 type parameters.
To have more, the Generic - helper can be nested, or
use GenericN n - fun [t1; t2; .. tN] ->.
Although this gives an incomplete pattern match warning, there will be no
runtime errors if the matching the list for the provided length n.
Generic Methods
Similarly, generic methods are generated using lambda expressions, for example:
Generic - fun t ->
"length" => T<list<_>>.[t] ^-> T<int>
This code would generate the following F# signature:
val Length<'T> : list<'T> -> int
You can use Generic % to add the same generics to a list of members,
Generic * to add the same generics to a ClassMembers value.
Also Generic + ["a"; "b"; ...] --- ... specifies the names of the type parameters.
Type Constraints
You can now set type constraints on parameters using p.Constraints <- [...] inside the lambda passed to Generic -. Previous WithConstraints helper is removed as we want to have all helper functions named With... to be non-destructive.
Modifiers
Documentation Comments
Documentation comments can be added using the WithComment function.
Customizing JavaScript
By default, inline JavaScript definitions are inferred for all methods and properties from their names. This is intuitive and convenient but not fully general. Therefore it is possible to bypass the inferred inlines and customize the generated bindings.
Default Inline Generation
Default bindings are name-based. For example, a static function
called foo with three arguments on a class called Bar, produces
the JavaScript inline Bar.foo($0,$1,$2).
Generated .NET names are automatically capitalized, so that this
function is accessible as Bar.Foo from F#.
Qualified names can be used on classes that are accessible in JavaScript with a qualified name, for example:
Class "geometry.Point"
This generates a .NET class Point which binds all static members as
geometry.Point.foo() in JavaScript.
Inline Transformations
Functions
The FSharpFunc<'TArg, 'TRes> type (which is used for lambdas by the F# compiler)
always take one parameter (which can be a tuple).
In WebSharper translation, these become JavaScript functions taking a single
fixed-length array (although curried functions only used in local scope are optimized).
However, it is often required that we pass functions defined in F# to a JavaScript
library (for example event handlers, callbacks, functional-style libraries).
WIG automatically converts between these function calling conventions.
Erased unions
Union types (for example T<int> + T<string>) can create method overloads,
but also Union typed properties or method return types when the cases can be
distinguished in JavaScript using the typeof, Array.isArray and the
arr.length functions.
In F# this means either at most one array case or possibly multiple tuple cases
with all different length, at most one number type (including DateTime and
TimeSpan, which are proxied as a Number), string, bool, and at most one other
object type.
If there are cases which can't be separated, the type will default to obj.
Option
By using the !? operator on the type of a property, it will be an option
type in F# which is converted to and from an existing or missing field on a
JavaScript object.
On method returns, undefined is converted to None, all other values
(including null) to Some ....
Custom Inline Transformations
You can add a custom defined inline transformation with the WithInterop helper.
This takes a record with an In and an Out field, both string -> string.
For example:
let Index =
T<int> |> WithInterop {
In = fun s -> s + " - 1"
Out = fun s -> s + " + 1"
}
Use this Type.Type value instead of T<int> in your member
declarations where you want to handle an index as 1-based in your code,
but pass it to and get it from a 0-based value in a JavaScript library.
On method parameters and property setters the In function will be used on the
parameter or property value in the automatic inline. On method return values and
property getters the Out function will be used on the whole inline of the
method or property getter.
Erasing Inline Transformations
Use the WithNoInterop helper to clean any automatic and custom inline
transformations from a Type.Type value.
Customizations
Custom Names
The simplest form of customization allows to decouple the .NET name of
a member from the name used by the inline generation process. This is
done by the WithSourceName function. For example:
"ClonePoint" => Point ^-> Point
|> WithSourceName "clone"
This generates a method that is available as ClonePoint from .NET
but calls clone in JavaScript.
Custom Inline Methods
The method and constructor inlines can be set explicitly by using
WithInline.
Custom Inline Properties
Properties have separate inlines for the getter and the setter
methods. These can be set explicitly by using WithGetterInline and
WithSetterInline respectively.
Custom Inlines Using Transformations
To define a custom inline for a method that still makes use of the default or
custom inline transformations on parameters and return value, use the
WithInteropInline helper.
It takes a function typed (string -> string) -> string, use the provided
function on a parameter name or index to get its transformed inline.
For example, defining an on event setter with function argument detupling:
"onChange" => (T<int> * T<obj> ^-> T<unit>)?callback ^-> T<unit>
|> WithInteropInline (fun tr -> "$this.on('change', " + tr "callback" + ")"
Similar helpers exists for property getters and setters:
WithInteropGetterInline and WithInteropSetterInline.
For getters, provided function is only usable for transforming "index"
in the case of an indexed property, and for setters transforming "value" or "index".
Obsoleted Members and Types
Use the Obsolate helper to mark a type or member definition with
System.ObsoleteAttribute.
ObsolateWithMessage also sets a custom warning message.
Best Practices
The benefit of using F# for generating the bindings is that repetitive tasks or patterns can be distributed as functions. Several such patterns are pre-defined in the standard library.
Configuration Class Patterns
These patterns for constructing member lists are useful for describing JavaScript configuarion objects. Configuration objects typically are simple collections of fields, most of them optional, that are used to describe how another object it to be constructed or operate. Let us take a simple example:
let MyConfig : Class =
Class "classname"
|+> Pattern.RequiredFields [
"name", T<string>
]
|+> Pattern.OptionalFields [
"width", T<int>
"height", T<int>
]
This definition would produce a class useable from F# that would compile to simple JavaScript object literals:
MyConfig("Alpha", Width=140)
The Width property is write-only, but there is also a WidthOpt
property generated which allow access to the optional field typed as
an F# option value.
Pattern.ObsoleteFields can be used similarly, to create a list of
properties with the Obsolete attribute.
Enumeration Patterns
JavaScript functions often accept only a fixed set of constants, such
as strings. Typing such parameters with string would be misleading
to the user. The enumeration pattern allows to generate a type that
is limited to a specific set of constants, specified as either inlines
or strings literals. See Pattern.EnumInlines and
Pattern.EnumStrings, both of which generate Class values.
Assembly and Namespaces
You have to provide the WIG compiler with a single CodeModel.Assembly
value as described in the "Getting Started" section.
Construct this with the Assembly helper which takes a list of
CodeModel.Namespace values which can be created with the Namespace helper
specifying its name and a list of CodeModel.NamespaceEntity values.
These latter can be type definitions or resource types.
Missing Type Definitions
If you have a type definition which you refer to but does not get included in a namespace in the assembly definition, when building the library, you will get an error.
Resources
You can define WebSharper resource classes using the Resource function.
Pass it to AssemblyWide to make the resource loaded if anything from the
currently created assembly is used.
Namespace "WebSharper.JQuery.Resources" [
Resource "JQuery" "http://code.jquery.com/jquery-1.11.2.min.js"
|> AssemblyWide
]
Use the Requires helper to add a list of defined resources as a dependency to a
type definition or another resource definition.
Use the RequiresExternal to add a list of resource classes from another assembly
to a type definition or a resource definition.