Automatic node generationIt is possible to automatically generate so called ``type node generators'' using the camlp4 extension.Automatic generation with ``with seditable''Structure editor for a calculatorConsider a language for a calculator which can add and subtract. To create a structure editor for such a calculator, simply add ``with seditable'' at the end of the declaration, like so:
(* example location: getting-started/code/1-calculator *)
type calc =
| Plus of calc * calc
| Minus of calc * calc
| Number of int
with seditable
This simply creates a function ``seditable_of_calc'' which is of type let value_of_type_calc = SEditable.input (seditable_of_calc)If you wanted to display something of type calc, you would write let _ =
let foo = Number 5 in
let n =
SEditable.create_node seditable_of_calc
in
SEditable.load n foo;
SEditable.show n
The function interpret takes a term of type calc and returns the result of the calculation:
let rec interpret =
function
Plus (a, b) ->
interpret a + interpret b
| Minus (a, b) ->
interpret a - interpret b
| Number x -> x
You can now implement a full blown calculator:
let _ =
let inp =
SEditable.input (seditable_of_calc)
in
let n =
SEditable.seditable_of_int ()
in
SEditable.load n (interpret inp);
SEditable.show n
A video of the result can be viewed at http://www.youtube.com/watch?v=QqJj2c9Xxo8.
Moving around and inputtingTo move around your structure editor, use the arrow keys to change the current location. You can type things in the boxes. If choices are presented to you, press F<number> to select one of the choices. Often times you can press the delete key to clear the current node. Control-s will submit whatever you've written if you have done. Control-c is for copy and Control-v is for paste. when a node is selected, press the ``r'' key to retract it into three dots. Pressing the ``e'' key will expand the current node or all the descendants by one level. If you want to expand all of it, press ``a''. Pressing escape toggles the suggestion window. Insert moves between the source and the menu. You can also use the mouse to select a node or a suggestion. The following table :
Nodes and type nodesAn element of type node can be displayed using the functionOsetGUI.run_with_node. Once displayed, the user is free to edit the node using the keyboard and mouse. A type node generator is an automatically generated function of type unit -> OsetNode.type_node. To extract a value from a type node generator you can use the Seditable.input function. If a type node generator is generated from a type ``foo'' then it will be bound to the name seditable_of_foo. For example, nn the previous example the type node seditable_of_calc was generated for type ``calc''. Type nodes are objects and have the following useful methods:
Furthermore, they have the following types:
the method
Automatic generation with ``grammar''A prettier calculatorThe previous calculator works, but it's not very pretty. We can use the new grammar construct to create an equivalent calculator:
(* example location: getting-started/code/2-prettier-calculator *)
grammar calc :
Plus : calc, "*", calc
| Minus : calc, "*", calc
| Number : "(",int, ")"
This does two things
to_string to create a parseable string. We will soon see how to handle tokens. A video of the result can be viewed at http://www.youtube.com/watch?v=k6N6WcBUVmo.
Tokens and choiceA formSay you want to create a form where the user must input his name, email, sexe and city. An email is defined by a regular expression and a city is defined by a list of (possibly thousands) of strings. To deal with regular expressions we can us tokens and to deal with lists of string we can usechoice_of_strings function like so:
(* example location: getting-started/code/3-form *)
token email = ".+@.+\\..+"
type city = string
let seditable_of_city =
SEditable.choice_of_strings ["Montreal";"New York";"Paris";"London"]
Once done, you can write the rest of the form like so:
type sexe = Male | Female with seditable
type form =
{
name : string;
city : city;
email: email;
sexe: sexe;
} with seditable
You would then, predictably, prompt the user for a term like so:
let _ =
let inp =
SEditable.input (seditable_of_form)
in
print_endline inp.email
Using token and grammar you can model many already defined context free grammars. A video of the result can be viewed at http://www.youtube.com/watch?v=ZAM0lvxJpyw.
Parametric type node generatorsIf a type is parametric it will generate a parametric type node generator. Parametric type node generators take type nodes as their arguments. For example the list type's type node generatorSEditable.seditable_of_list has type (unit -> type_node) -> unit -> type_node so that SEditable.seditable_of_list SEditable.seditable_of_int is a type node for int list. Another parametric type node generator of the same type is SEditable.seditable_of_star which was generated for type star, which is the same as type list. Other examples of parametric type node generators are question and plus. Using star, plus and question we can write grammars which are very similar to EBNF grammars.
SQLYou can generate parts of the structure editor dynamically. The following is an example of a structure editor where the tables names and row names are generated by functions.
(* example location: getting-started/code/4-sql *)
let get_table_names () = ["table1";"table2"]
let get_row_names () = ["age";"address"]
type table_name = string
let seditable_of_table_name =
SEditable.choice_of_strings (get_table_names ())
type row_name = string
let seditable_of_row_name =
SEditable.choice_of_strings (get_row_names ())
grammar select :
Select : "SELECT" , (row_name list) , "FROM", (table_name list)
Then you can call the structure editor like so:
let _ =
let res = SEditable.input (select_of_select)
in
match res with
Select (tables,rows) ->
print_endline
("SELECT " ^ (String.concat " , " tables) ^
" FROM " ^ (String.concat " , " rows))
Generating structure for already defined types
XMLA video of the result can be viewed at http://www.youtube.com/watch?v=KD7kxhz3DD4. If you want to generate a structure editor for an already generated type, you would do the following:(* example location: getting-started/code/6-prettier-xml *)
open Xml
grammar xml_p (= xml) :
PCData : "PCData", string
| Element : "Element", (string * (string * string) list * (xml_p list))
let _ =
SEditable.input (seditable_of_xml_p)
OSET will generate a new type xml_p like so:
type xml_p = xmland will then generate the corresponding seditable_of_xml_p function. Please note that the module in which the constructors are located MUST be opened.
Prettier XMLThe previous example works, but it doesn't correspond to the XML grammar we're used to. We can get around this by using the piggyback function.First thing to do is to define the grammar:open SEditable
grammar xml_grammar :
Element : "<", string, attribute star, ">" ,
xml_grammar star,
"</", string, ">"
| CharData : string
and attribute:
Attribute : string,"=",string
Notice the star type is used; this type is defined in the module SEditable. The second thing to notice is that you can build mutually recursive types with the ``and'' construct. Now that we have a structure editor, we would like to interface it with the xml-light library. We do this by defining two functions: from_xml and to_xml:
let rec to_xml =
function
Element (str,atts,grams,_) ->
Xml.Element
(str,
List.map (function Attribute (x,y) -> (x,y)) atts,
List.map to_xml grams)
| CharData str ->
Xml.PCData str
let rec from_xml =
function
Xml.Element (str,atts,xmls) ->
Element
(str,
List.map (fun (x,y) -> Attribute (x,y)) atts,
List.map from_xml xmls,
str)
| Xml.PCData str ->
CharData str
Using these two functions, we can construct a structure editor for Xml.xml like so:
type xml_p = Xml.xml let seditable_of_xml_p = SEditable.piggyback seditable_of_xml_grammar from_xml to_xmlwe can then use the structure editor as a pretty printer for xml documents.
let pretty_print_xml xml =
let n = SEditable.create_node seditable_of_xml_p in
SEditable.load n xml;
SEditable.to_string n
A video of the result can be viewed at http://www.youtube.com/watch?v=63xkE406tFU.
How to define the star typeThe star type itself is built from a piggyback using the epsilon type. The epsilon type corresponds to the empty string.
grammar 'a star_p:
More : 'a, 'a star
| Done : epsilon
type 'a star = 'a list
let rec to_star =
function
More (a,b) ->
a :: to_star b
| Done _ -> []
let rec from_star =
function
a :: b -> More (a,from_star b)
| [] -> Done ()
let seditable_of_star x = piggyback (seditable_of_star_p x) from_star to_star
Writing custom structure editorsWe will now investigate how to write your own structure editors from scratch. Structure editors are represented by the abstract class Node, found in the module OsetNode.Hello WorldThehello_world class inherits the simplest_node_implementation virtual class and which simply overrides the render method so that it displays Hello World. The simplest_node_implementation class simply implements most of node's virtual methods as simply as possible. Here is the code:
(* example location: getting-started/code/hello-world *)
open OsetWidgets
class hello_world =
object
inherit OsetNode.simplest_node_implementation
method render =
box (label "Hello World")
end
let _ = OsetGUI.run_with_node (new hello_world)
XMLOur first object language will be XML. We will be building this structure editor with the help of the simplest_node_implementation class. Before we start, it is essential to open the module OsetWidgets, a module which includes functions which dictate how a node is drawn on the screen
open OsetWidgets We will define three mutually recursive functions: iter, pcdata and element. Our first function iter will take an XML.xml and return a node.
(* example location: getting-started/code/custom-xml *)
let rec iter x =
match x with
Xml.PCData str ->
pcdata str
| Xml.Element (x,y,z) ->
element (x,y,z)
then we define our second function, pcdata. We will inherit from the ``simplest_node_implementation'' class, which contains trivial definitions for all the methods of a node except the render method. The render method must return something which is a boxed_widget. There are several ways to create a boxed widget, but the easiest is to call the ``box'' function on a widget, such as the
and pcdata str =
(object (self)
inherit OsetNode.simplest_node_implementation
method render =
box
(label str)
end)
Our third and most interesting function is element. This function introduces the ``information_bar'' method to our reader. This method displays text on the top of the screen. In this case we will simply display the element attributes. The function ``node'' takes a node and returns a widget which represents that node. Finally, the left_spacing widget is presented, which simply offsets its child from the left. Here's the result:
and element (x,elems,unprocessed_children) =
let children = List.map (fun a -> (iter a)) unprocessed_children in
(object(self)
inherit OsetNode.simplest_node_implementation
method information_bar =
let attributes = List.map (fun (x,y) -> x ^ " = " ^ y) elems in
if attributes = []
then [],[]
else
("attributes:" :: attributes,[])
method render =
box
(vbox
(label x ::
(List.map
(fun a -> left_spacing 30 (node (a:>OsetNode.node) ))
(children:>OsetNode.node list))))
end)
Finally, we include a function make which calls iter
let make x = iter (Xml.parse_file x) to run a node, call ``OsetGUI.run_with_node'' like so:
let _ =
let t = ExampleXML.make "elite.xml" in
OsetGUI.run_with_node (t:>OsetNode.node)
OsetWidgets
The return type of the render method is a
SuggestionA suggestion is a term of typestring ->
(string *
(unit -> [`Stay | `Move_to of 'a | `Search | `Search_from of 'a ])) option
A suggestion takes a string and either returns None if the string doesn't match the suggestion or Some (s,f) where s is the string displayed to the user and f is a function which returns one of the following:
OsetSuggestionThe OsetSuggestion module provides many functions to automatically create suggestions. For example:
Examples of boxed widgetbox (label "Hello World!")
box (hbox [label "foo";label "bar"])
box
(hbox
[label "some vbox"; vbox [label "some node";node some_node]])
box ~unselected_color:Sdlvideo.grey (label "foo")
suggestions
str_state
[int_suggestion (fun i () -> print_endline i;`Stay)]
Adding a menuWhen you automatically generate a type node generator, create a type node and passe it toOsetGUI.run_with_node you are presented with a graphical user interface with a menu box on the top left corner. Clicking on the top left corner (or pressing insert) presents the menu. It is possible to present a custom menu by overriding the get_node_specific_menu method, which has type menu option where
type menu =
Environment of
string *
(string ->
(string *
(unit ->
[`Switch_to_menu of menu
| `Switch_to_node of node
| `Focus_source
| `Focus_source_and_search
| `Focus_menu]))
option)
The string in the pair is the text presented to the right of the menu box. The second element of the pair is very much like a suggestion. Like a suggestion it returns None if the string doesn't match and Some(s,f). Unlike a suggestion, f is one of the following:
You can use
Adding a global menu
It is possible to add a global menu for all nodes by using
Key bindingsKeybindings are added to nodes using theget_node_specific_keybindings method which has type
([ `Unicode of int
| `Keysym of Sdlkey.t * Sdlkey.mod_state] *
(unit ->
[ `Switch_to_menu of node menu_param
| `Switch_to_node of node
| `Focus_source
| `Focus_source_and_search
| `Focus_menu])) list
You would typically use `Unicode except for control characters such as backspace and delete.
Miscellaneous examplesBinary treeThe binary tree example is another good example of using suggestions. The code is in thecustom-binarytree directory. A never_ending_binarytree is either an internal node with two children or a leaf which is a suggestion box. The suggestion boxes have only one suggestion. The suggestion either accepts any string and creates an internal node whose name matches the string inputted by the user. If the string inputted by the user is empty, it names the internal node ``unnamed''. Notice that the `Leaf constructor takes a string reference. This is so that the suggestion box keeps its contents after a redraw. If you had created a new reference for every call to ``suggestions'', the box would be cleared after every redraw.
Changing fontsTo change the font simply set theOSET_FONT_LOCATION environment variable. In bash you would write something like export OSET_FONT_LOCATION=/foo/bar/baz.ttf
Simply typed lambda calculusThis chapter describes the structure editors located in the simply-typed-lambda-calculus subdirectory. The underlying datatype we wish to focus on is the following:(* example location: getting-started/code/simply-typed-lambda-calculus *)
type expression =
Application of expression * expression
| Lambda of string * expression
| Number of int
| Boolean of bool
| String of string
| Atom of string
type function_definition =
string * expression
type program =
function_definition list
The reason that we will be building the structure editor from scratch is that we can restrict the user to only inputting correct programs. Another advantage to building it from scratch is that we can decide how to display the nodes. By correct we mean that all variables and function names are bound where they are used. For this purpose, we need a context which propagates throughout the nodes. Our context will have type
A first attemptTo understand this chapter, please look at the code infirst-attempt. There are three mutually recursive classes. The first is expression which can be one of 6 things:
val mutable body :
[
| `Unset of string ref
| `List of expression_list
| `Atom of (string,string ref) either
| `Number of (int,string ref) either
| `String of (string,string ref) either
] = `Unset (ref "")
renderingThere are several cases for rendering an expression.| `Unset str ->Is when an expression is waiting for user input. It gives the user the option between switching the node to a atom, number, string or list. | `List lst ->renders the list with a margin of 30 pixels. lst has type expression_list
| `Atom (Right str) ->represents an unintialized atom. It asks the user to input the name of an atom. The name cannot be an empty string. The other cases are similar.
programAprogram represents an expandable list of function definitions. It stores the function definitions already added and a tail node which adds more function definitions.
tailThetail node keeps prompting the user to add more nodes until the user selects ``done''. When a tail node is done it becomes a small light grey box. Notice that when a node is added the status is reinitialized to `Adding (ref ""). This simply clears the suggestion box.
Adding types, unification, context and error highlightingThis section describes the source found in the directorysecond-attempt.
Adding typesFirst we model types with a simple datatype:type typ = | Arrow of typ * typ | Int | Bool | Variable of (typ option) refThen we define a function to convert types to string
let name_of_reference_map : (typ option ref * string) list ref = ref []
let rec string_of_typ =
function
Arrow (t,t') ->
"(" ^ string_of_typ t ^ "->" ^ string_of_typ t' ^ ")"
| Int -> "int"
| Bool -> "bool"
| Variable {contents = Some t} ->
string_of_typ t
| Variable v ->
try
List.assoc v !name_of_reference_map
with
_ ->
let name =
"x" ^ string_of_int (List.length !name_of_reference_map)
in
name_of_reference_map := (v,name) :: !name_of_reference_map;
name
UnificationWe implement a straightforward reference based unification algorithm.exception Ununifiable
let rec unify =
function
| (Variable {contents=Some t}, b) ->
unify (t,b)
| (Variable a,Variable {contents=Some t}) ->
unify (Variable a,t)
| (Variable a,b) ->
a := Some b
| (a,Variable b) ->
b := Some a
| (Arrow (t,t'),Arrow (u,u')) ->
unify (t,u);
unify (t',u')
| (Int,Int) -> ()
| (Bool,Bool) -> ()
| _ -> raise Ununifiable
ContextContext is simply a map from names to types, defined like so:type context = (string * typ) list Adding to and using the contextGiven a contextcon, we are interested in seeing if an expression is well typed in that context. In the class expression there is a method check which takes a context and an expected type and checks if the expression does indeed have the expected type. If anything goes wrong, the error mutable variable is updated. In the class function_definition there is a method add_to_context which takes a context and checks its underlying expression in that context. If the function_definition has a name, that name is added to the context. In this manner, the context is incrementally expanded and the function definitions are sequentially checked. All this is done in the render method in the program class. The information_bar method in expression displays the current type of the expression if it hadn't encountered an error when checking; otherwise it prints the error. An expression which has an error will have a red box around it, which was achieved by using the ~unselected_color label with box
Final touchesThis section describes the source found in the directoryfinal-attempt.
Smarter suggestionsWe want the structure editor to suggest only possibilities which would result in a well typed program. We do this by unifying all the possible choices.Checking unifiabilityIf an expression is expecting a typet, say Arrow(Int,Variable (ref None)), then the possible suggestions will be those whose type unify with that type. We cannot simply call unify on the type of every element in the context because unify might instantiate the reference in t. The solution to this is to first copy the two types and then unify the copies making sure that equal references in the original types are mapped to equal references in the result, like so:
let copy_type t =
let rec iter sofar =
function
Variable {contents=Some t} -> iter sofar t
| Variable x ->
(try
(sofar,List.assq x sofar)
with
_ ->
let new_var = Variable (ref None) in
((x,new_var)::sofar,new_var))
| Arrow (t,t') ->
let (sofar',u) = iter sofar t in
let (sofar'',u') = iter sofar' t' in
(sofar'',Arrow(u,u'))
| (Int | Bool | String) as x -> (sofar,x)
in
snd (iter [] t)
let are_unifiable t1 t2 =
let t1' = copy_type t1 in
let t2' = copy_type t2 in
try
ignore(unify (t1',t2'));
true
with
_ -> false
Once we have the are_unifiable function, we can change the render method in expression at the `Atom (Right str) and `Unset _ branches. We simply have to see if the current expression satisfies the predicate are_unifiable current_typ t where t is the type we're expecting.
Adding a menuWe will define a menu to save the current program. First we define the abstract datatype like so:type exp =
Application of exp * exp
| Lambda of string * exp
| Number of int
| Boolean of bool
| StringLit of string
| Atom of string
type fundef =
string * exp
type prog =
fundef list
Then we must define a method trans for the class program whose return value is prog. Once that's done, we can define a menu like so:
let program = new program
let menu _ =
let save_suggestion =
OsetSuggestion.string_and_action_to_suggestion
"save"
(fun _ ->
let out = open_out "save_file" in
(try
output_value out (SEditable.trans program)
with
_ -> ());
close_out out;
`Focus_source)
in
(OsetNode.Environment
("menu",
[save_suggestion;load_suggestion
]))
let _ = OsetGUI.run_with_node ~top_menu_func:menu program
Notice how menu takes an argument, being the local menu of the node. Since the nodes do not have a local menu (defined by the get_node_specific_menu method), the argument is ignored. We leave it up to the reader to finish writing this menu by adding a load suggestion; this will require the reader to add a method to the program class.
Adding deletionDeletion is added by overriding theget_node_specific_keybindings method in a class. In our implementation we override the method in the program class, like so:
method get_node_specific_keybindings =
[
(`Keysym (Sdlkey.KEY_DELETE,Sdlkey.kmod_none),
fun ()->
body <- [];
`Focus_source)
]
We leave it up up to the reader to override the get_node_specific_keybindings in the expression and function_definition classes. |