Getting started
To get started with gluon we must first have a way to compile and run Gluon programs. The fastest way to get that is to download one of the pre-built binaries for Linux, Windows, OSX or FreeBSD from https://github.com/gluon-lang/gluon/releases. Alternatively, if you have a Rust compiler and Cargo installed you may install it with cargo install gluon_repl
.
Once installed you can verify that it works by saving the following program into a file named hello_world.glu
and then compile and run it with gluon hello_world.glu
.
let io = import! std.io
io.println "Hello world!"
If everything works the program should have printed Hello world!
to your terminal.
Dissecting Hello World
let io = import! std.io
io.println "Hello world!"
There are a number of things going on in the hello world example so lets break them down one step at a time.
let
Gluon uses the keyword let
to bind values for later use.
import! std.io
import!
is a builtin macro which loads the contents of another module. In the example we use it to get access to the standard library's io
module. Since it appeared on the right side of let io =
we thus bound the std.io
module to the io
binding.
io.println
Here we access the println
function from the io
module we bound earlier which is a function that lets us write strings to stdout.
"Hello world!"
Finally we create a string literal which gets passed to println
to get printed.
Using the REPL
Though it is possible to continue running any programs by saving it to a file and running it with gluon my_script.glu
there is an easier way to go about it when you want to experiment quickly with small programs. By running gluon -i
, gluon starts in "interactive" mode, giving you a REPL where you may evaluate expressions and inspect their results. Try evaluating some simple arithmetic expressions to see that it works.
> 1 + 2
3
> 100 * 3 + 4
304
> 3.14 * 10.0
31.400000000000002
Evaluating only a single expression can get quite unwieldy so if we want to break something up into multiple steps we can use let
to give a name to an expression.
> let pi_2 = 3.14 * 2.0
6.28
> pi_2 * 3.0
18.84
These are the basic parts of the REPL and if you want to you can try writing hello world again by using the features above.
If you still have the hello_world.glu
file around there is another way to run it from inside the REPL by using the special :script
(:s
) command.
> :s hello_world.glu
Hello World!
There are a few other of these special commands as well and you can find them all with :help
(:h
).
> :type 1
Int
> :info std.io.println
std.io.println: String -> IO ()
> :kind std.option.Option
Type -> Type
Finally you may quit the REPL using the :quit
(:q
) command or using <CTRL-D>
.
Anatomy of a gluon program
Let's look at a slightly larger program by writing a guessing game. In this game the player will guess at a random number between 1 and 100 and the program will say whether each guess is to low or to high. If the player guesses correctly the program will congratulate the player and exit.
As a base we can take the hello world example.
let io = import! std.io
io.println "Hello world!"
The first thing we will need is a way to get a number from the user. For this we will use the std.io.read_line : IO String
action. To test that it works we will simply write the line back to the user.
let io @ { ? } = import! std.io
do line = io.read_line
io.println line
There are two new concepts in play here, implicit arguments and do expressions.
do expressions
are similar to let expressions
in that they let us us bind the result of an expression to a name which we can use later. Where they differ is that, rather binding the result of evaluating the expression itself, they expect the right hand side to be a monadic action such as IO
and the value bound is the result of evaluating the action, which in this case is the String
that that were input.
(As was alluded to in the previous paragraph IO
is a Monad
, a rather complex concept which I won't go into here as it is enough for our purposes to only consider the "IO monad" as something that describes how IO
actions are run in sequence.)
do expressions
don't just magically work with IO
actions which is where implicit arguments
come in, it lets us use the compiler to to implicitly insert certain function arguments by looking at the inferred types. This can be thought as a way to get something similar to traits
in Rust but with a bit extra flexibility by requiring a bit of explicitness to let the compiler know what it can use as an implicit argument. Which is why we needed to add the { ? }
record pattern match, the ?
lets the compiler know that it should choose from the fields of the record when looking for an implicit argument. In this case the compiler sees that we use IO
in the do expression
and implicitly inserts an implicit Monad IO
value found in std.io
, letting the do expression
know how to sequence these two actions.
Next we will need to generate a number for the player to guess. For this we will use the std.random
module and specifically the thread_rng.gen_int_range
IO
action. So we will need to run it using do
again. Lets also output the number so that to see how it works, since io.println
takes a String
we will need to convert the number before outputting it which we can do with [show
][].
let io @ { ? } = import! std.io
let random = import! std.random
do target_number = random.thread_rng.gen_int_range 1 101
do line = io.read_line
io.println (show target_number)
The program should now output a random number between 1 and 100 on every run! We aren't checking if that number matches the input from the user yet however so lets do that next.
To check that the input is actually a number and retrieve retrieve it as such we can use the std.int.parse
function. We also need to trim whitespace from the line we read as it contains a trailing newline.
let io @ { ? } = import! std.io
let random = import! std.random
let int = import! std.int
let string = import! std.string
let { Result } = import! std.result
do _ = io.println "Guess a number between 1 and 100!"
do target_number = random.thread_rng.gen_int_range 1 101
do line = io.read_line
match int.parse (string.trim line) with
| Err _ ->
io.println "That is not a number!"
| Ok guess ->
if guess == target_number then
io.println "Correct!"
else
io.println "Wrong!"
If you try and run the program now it should only let you input numbers and give you a chance at the correct number. As it is you only got the one chance now though so lets make it possible to try again if the guess is wrong!
To implement repetition we will need to write a function, specifically a recursive function. As long as the guess is wrong we just say that the guess was wrong and then repeat the process by having the function call itself, if the guess was correct then we exit the program.
let io @ { ? } = import! std.io
let random = import! std.random
let int = import! std.int
let string = import! std.string
let { Result } = import! std.result
do _ = io.println "Guess a number between 1 and 100!"
do target_number = random.thread_rng.gen_int_range 1 101
let guess_number _ : () -> IO () =
do line = io.read_line
match int.parse (string.trim line) with
| Err _ ->
do _ = io.println "That is not a number!"
guess_number ()
| Ok guess ->
if guess == target_number then
io.println "Correct!"
else
do _ = io.println "Wrong!"
guess_number ()
// Start the first guess
guess_number ()
Now there is at least a way to guess again on the same number! It is still a rather tedious game though as the only hint we get is that the number is between 1 and 100 so lets add the last part of letting the program tell whether the guess is to high or to low.
let io @ { ? } = import! std.io
let random = import! std.random
let int = import! std.int
let string = import! std.string
let { Result } = import! std.result
let { Ordering, compare } = import! std.cmp
do _ = io.println "Guess a number between 1 and 100!"
do target_number = random.thread_rng.gen_int_range 1 101
let guess_number _ : () -> IO () =
do line = io.read_line
match int.parse (string.trim line) with
| Err _ ->
do _ = io.println "That is not a number!"
guess_number ()
| Ok guess ->
match compare guess target_number with
| EQ -> io.println "Correct!"
| LT ->
do _ = io.println "Wrong! Your guess is too low!"
guess_number ()
| GT ->
do _ = io.println "Wrong! Your guess is too high!"
guess_number ()
// Start the first guess
guess_number ()
And there we have it, the final program! There are ways to improve this but lets leave that for the next chapter.
Extending the guessing game with effects
In the previous chapter we implemented a simple guessing game. In this chapter we will extend the final program with to keep track of how many guesses the player has done and finally report it at the end.
First, this was the final program from the previous chapter.
let io @ { ? } = import! std.io
let random = import! std.random
let int = import! std.int
let string = import! std.string
let { Result } = import! std.result
let { Ordering, compare } = import! std.cmp
do _ = io.println "Guess a number between 1 and 100!"
do target_number = random.thread_rng.gen_int_range 1 101
let guess_number _ : () -> IO () =
do line = io.read_line
match int.parse (string.trim line) with
| Err _ ->
do _ = io.println "That is not a number!"
guess_number ()
| Ok guess ->
match compare guess target_number with
| EQ -> io.println "Correct!"
| LT ->
do _ = io.println "Wrong! Your guess is too low!"
guess_number ()
| GT ->
do _ = io.println "Wrong! Your guess is too high!"
guess_number ()
// Start the first guess
guess_number ()
To keep track of how many guesses the player has done we need to keep some state as we go through each guess.
There are a few ways of doing this but the one we will look at here is using gluon's extensible effect system.
This can be found under the std.effect
module.
Extensible effects are a way to represent side effects in a composable way. In other words, it is possible to use multiple different effects easily, something which can otherwise be fairly difficult with the monadic representation of side effects that gluon employs.
Rewriting IO into Eff
Before we get into adding more effects though we first need to replace the IO
monad with the Eff
monad used to represent effects.
Normally it is not possible to use a Monad
directly when using effects and instead an entirely new "effect handler" needs to be written for the effect.
However, Eff
allows the use of ONE Monad
instance among the various effects in an effect row with the Lift
effect.
Lets see how we need to modify the code the guessing game to use Eff
instead of IO
, without adding any functionality.
let io @ { ? } = import! std.io
let random = import! std.random
let int = import! std.int
let string = import! std.string
let { Result } = import! std.result
let { Ordering, compare } = import! std.cmp
let { (<|) } = import! std.function
let effect @ { Eff, ? } = import! std.effect
let { Lift, run_lift, lift, ? } = import! std.effect.lift
let start _ : () -> Eff [| lift : Lift IO | r |] () =
do _ = lift <| io.println "Guess a number between 1 and 100!"
do target_number = lift <| random.thread_rng.gen_int_range 1 101
let guess_number _ : () -> Eff [| lift : Lift IO | r |] () =
do line = lift <| io.read_line
match int.parse (string.trim line) with
| Err _ ->
do _ = lift <| io.println "That is not a number!"
guess_number ()
| Ok guess ->
match compare guess target_number with
| EQ -> lift <| io.println "Correct!"
| LT ->
do _ = lift <| io.println "Wrong! Your guess is too low!"
guess_number ()
| GT ->
do _ = lift <| io.println "Wrong! Your guess is too high!"
guess_number ()
// Start the first guess
guess_number ()
run_lift <| start ()
There is quite a bit going down here so lets break it down.
let { (<|) } = import! std.function
let effect @ { Eff, ? } = import! std.effect
let { Lift, run_lift, lift, ? } = import! std.effect.lift
Here we import a few new things. The first just imports the useful <|
operator, it doesn't really do any thing on its own and just serves as a way to let us avoid parentheses in a few places. Essentially f <| a 123 "a"
is the same as f (a 123 "a")
.
Next we import the Eff
type and its implicit instances so we can use do
with it.
Finally we import Lift
and some functions to help us work with it. The lift
function lets us "lift" a monadic action into the Eff
monad. run_lift
does the opposite, it takes the monadic action out of the Eff
monad and lets us evaluate it.
let start _ : () -> Eff [| lift : Lift IO | r |] () =
Here we define our entrypoint and we can see the first use of the Eff
type and a new bit of syntax which describes an "effect row".
Effect rows are described in a similar manner to records, except they use [|
and |]
to delimit instead of {
and }
.
Rather than describing what fields a record holds they instead describe the effects used in this function.
In the same way as records, effect rows also has an optional | r
part which is what makes this "extensible" (see polymorphic records in the reference).
If we defined the row without the | r
part then we would get an error when trying to use the effect in a place that allows more effects.
let eff_closed : Eff [| eff_1 : Eff1 |] () = ...
let eff_open : Eff [| eff_1 : Eff1 | r |] () = ...
let eff : Eff [| eff_1 : Eff1, eff_2 : Eff2 | r |] () =
// Error, type mismatch `[| eff_1 : Eff1 |]` does not contain `eff_2 : Eff2`
do _ = eff_closed
// Ok, because the action is polymorphic/extensible
do _ = eff_open
// etc
Essentially the type variable says that the effect "may have more effects then the ones specified" which lets us only specify the effects that each individual function needs while still being able to use them transparently in a program that uses additional effects.
In this place we only use the lift : Lift IO
effect but we will see how we can use this to specify multiple effects in a little bit.
do _ = lift <| io.println "Guess a number between 1 and 100!"
Here we actually start running an action. Since io.println
returns an IO ()
action and we need an Eff [| lift : Lift IO | r |] ()
action we use lift
to convert into the correct type. Otherwise the code is exactly the same as before!
(The need for lift
is likely to be removed in the future.)
The lines that follow are all the same as before, only they may have received the lift
treatment as well. Except for the last line.
run_lift <| start ()
Here we use run_lift
to go back from Eff [| lift : Lift IO | r |] ()
into IO ()
which the gluon interpreter can execute.
Adding the State effect
Now that we have the program written with Eff
we can use this to easily add some state to track how many guesses the player has done!
We do this using the State
effect which lets us hold a value in the Eff
monad which we can read and write to.
let io @ { ? } = import! std.io
let random = import! std.random
let int = import! std.int
let string = import! std.string
let { Result } = import! std.result
let { Ordering, compare } = import! std.cmp
let { (<|) } = import! std.function
let effect @ { Eff, ? } = import! std.effect
let { Lift, run_lift, lift, ? } = import! std.effect.lift
let { State, eval_state, ? } = import! std.effect.state
let start _ : () -> Eff [| lift : Lift IO, state : State Int | r |] () =
do _ = lift <| io.println "Guess a number between 1 and 100!"
do target_number = lift <| random.thread_rng.gen_int_range 1 101
let guess_number _ : () -> Eff [| lift : Lift IO, state : State Int | r |] () =
do line = lift <| io.read_line
match int.parse (string.trim line) with
| Err _ ->
do _ = lift <| io.println "That is not a number!"
guess_number ()
| Ok guess ->
match compare guess target_number with
| EQ -> lift <| io.println "Correct!"
| LT ->
do _ = lift <| io.println "Wrong! Your guess is too low!"
guess_number ()
| GT ->
do _ = lift <| io.println "Wrong! Your guess is too high!"
guess_number ()
// Start the first guess
guess_number ()
run_lift <| eval_state 0 <| start ()
There are only two new things here, first we add state : State Int
to the effect row to indicate that we want to use a state consisting of a single integer.
The second addition is to add eval_state 0
before the run_lift
call. This initializes the state with 0
and then evaluates the effect.
Notably we haven't actually used the state in any way except initializing it so lets actually use it. To work with the state we have the basic get
, put
and modify
actions.'
let io @ { ? } = import! std.io
let random = import! std.random
let int = import! std.int
let string = import! std.string
let { Result } = import! std.result
let { Ordering, compare } = import! std.cmp
let { (<|) } = import! std.function
let effect @ { Eff, ? } = import! std.effect
let { Lift, run_lift, lift, ? } = import! std.effect.lift
let { State, eval_state, modify, get, ? } = import! std.effect.state
let start _ : () -> Eff [| lift : Lift IO, state : State Int | r |] () =
do _ = lift <| io.println "Guess a number between 1 and 100!"
do target_number = lift <| random.thread_rng.gen_int_range 1 101
let guess_number _ : () -> Eff [| lift : Lift IO, state : State Int | r |] () =
do line = lift <| io.read_line
match int.parse (string.trim line) with
| Err _ ->
do _ = lift <| io.println "That is not a number!"
guess_number ()
| Ok guess ->
// Increment the guess counter by one
do _ = modify ((+) 1)
match compare guess target_number with
| EQ ->
// Retrieve the guess so we can output it
do guesses = get
lift <| io.println ("Correct! You got it in " ++ show guesses ++ " guesses!")
| LT ->
do _ = lift <| io.println "Wrong! Your guess is too low!"
guess_number ()
| GT ->
do _ = lift <| io.println "Wrong! Your guess is too high!"
guess_number ()
// Start the first guess
guess_number ()
run_lift <| eval_state 0 <| start ()
We only needed to add two things here, first we add one on every guess using modify
then we use get
to retrieve the final amount when the player succeeded.
And that is all we had to do to add the guess tracking. The strength of extensible effects is precisely that there is very little work needed to add additional side effects to the program, despite the program as a whole still being entirely pure!
Just to show the strength of effects one last time we add the Error
effect to move the error handling outside of the main logic.
let io @ { ? } = import! std.io
let random = import! std.random
let int = import! std.int
let string = import! std.string
let { Result, map_err } = import! std.result
let { Ordering, compare } = import! std.cmp
let { (<|) } = import! std.function
let effect @ { Eff, ? } = import! std.effect
let { Lift, run_lift, lift, ? } = import! std.effect.lift
let { State, eval_state, modify, get, ? } = import! std.effect.state
let { Error, ok_or_throw, run_error, catch, ? } = import! std.effect.error
type GuessError =
| NotANumber
let start _ : () -> Eff [| lift : Lift IO, state : State Int, error : Error GuessError | r |] () =
do _ = lift <| io.println "Guess a number between 1 and 100!"
do target_number = lift <| random.thread_rng.gen_int_range 1 101
let guess_number _ : () -> Eff [| lift : Lift IO, state : State Int, error : Error GuessError | r |] () =
do line = lift <| io.read_line
do guess =
ok_or_throw
<| map_err (\_ -> NotANumber)
<| int.parse
<| string.trim line
do _ = modify ((+) 1)
match compare guess target_number with
| EQ ->
do guesses = get
lift <| io.println ("Correct! You got it in " ++ show guesses ++ " guesses!")
| LT ->
do _ = lift <| io.println "Wrong! Your guess is too low!"
guess_number ()
| GT ->
do _ = lift <| io.println "Wrong! Your guess is too high!"
guess_number ()
// Start the first guess
catch (guess_number ()) <| \err ->
match err with
| NotANumber ->
do _ = lift <| io.println "That is not a number!"
guess_number ()
run_lift <| run_error <| eval_state 0 <| start ()
Syntax and semantics
Gluon is a functional language at heart, basing its syntax on languages such as F#, OCaml and Haskell. The syntax may thus look strange if you are coming from C-like languages but don't be discouraged! There is actually very little syntax to learn.
If, on the other hand, you are familiar with functional languages you will be right at home. Roughly speaking, Gluon takes the expression syntax from F# and OCaml and uses the type syntax of Haskell.
Identifiers and Literals
The simplest syntactical elements in Gluon are identifiers and literals and none of them should be especially surprising if you are experienced in programming.
Identifiers are a sequence of alphanumeric characters including underscore ("_") which are required to start with either a letter or an underscore. Literals come in four different forms - integer, float, string and character literals.
// An identifier
abc123_
// An integer literal
42
// A float literal
3.14
// A string literal
"Hello world"
// A raw string literal
r"Can contain newlines
world"
r#"With # as delimiters raw strings can also contain quotes without escaping `"` "#
r###" "## "###
// A character literal
'e'
Comments
Comments should be immediately familiar if you are accustomed to C-like languages.
//
starts a line comment which is ended by a newline
/*
starts a block comment which is ended by */
Functions
f x "argument" 3
Being a functional language, functions are everywhere. Because of this, calling functions have an intentionally minimalistic syntax where there is no need to enclose arguments as a parenthesized list of arguments. Instead, arguments are separated by whitespace.
Another way of calling a function is through infix notation since gluon implements all operators as just functions.
1 + 2 // Calls the + function on 1 and 2
(+) 1 2 // Parenthesizing an operator makes it possible to use in a normal function call
It is important to note that function application binds harder than any binary operator.
(+) 0 1 - (+) 2 3 // Equivalent to (0 + 1) - (2 + 3)
Variable bindings
Any language more complex than Hello world is bound to require variable bindings which serve to bind some value to a name allowing it to be used later.
let x = 1 + 2 in x // Returns 3
You may rightly be wondering about the in x
part. gluon takes a strong stance against statements in an effort to keep things consistent. Thus only writing let x = 1 + 2
will be met with a syntax error about a missing in
keyword which is what defines the actual value returned from the let
expression.
Let bindings also allow functions to be defined which is done by listing the arguments between the bound identifier and =
// Defines the `id` function which takes a single argument and returns it.
let id x = x in id 1 // Returns 1
Mutually recursive values can be defined using rec ... in
to enclose the let
bindings.
rec
let f x = g x
let g x = f x
in f 1 // Never returns
This is not limited to functions but works with any value that is capable of recursion (records, variants and functions).
/// An infinite list of `1`
rec let ones = Cons 1 ones
in
/// A recursive set of records
rec
let value1 =
let f x = value2.f x + 1
{ f }
let value2 =
let f x = value1.f x + 2
{ f }
in ()
If expressions
The simplest control flow expression is the if
expression. It evaluates a boolean expression, taking the first branch if the boolean evaluates to True
, and taking the second if it evaluates to False
if True then 1 else 0
Record expressions
To create more complex data types, Gluon has first class records. Records can be used to couple data that is logically grouped into a single type.
{ pi = 3.14, add1 = (+) 1.0 }
To access the fields of a record, .
is used.
let record = { pi = 3.14, add1 = (+) 1.0 }
in record.pi // Returns 3.14
Field assignments can be omitted if there is a variable in scope with the same name as the field.
let id x = x
in { id }
The ..
operator can be used at the end of a record expression to take all fields of one record and fill the constructed record. Explicitly defined fields that also exist in the base record will be in the same order as they are in the base record while all other fields will be prepended in the order that they are written.
let base_record = { x = 1, y = 2, name = "gluon" }
in
// Results in a record with type
// { field : Bool, x : Int, y : Int, name : String }
{
field = True,
..
base_record
}
Array expressions
Arrays can be constructed with array literals.
// Results in an `Array Int`
[1, 2, 3, 4]
Since Gluon is statically typed all values must be of the same type. This allows the Gluon interpreter to avoid tagging each value individually which makes types such as Array Byte
be convertible into Rust's &[u8]
type without any allocations.
// ERROR:
// Types do not match:
// Expected: Int
// Found: String
[1, ""]
Functions to operate on arrays can be found on the array
module.
array.len [1, 2, 3]
Variants
While records are great for grouping related data together, there is often a need to have data which can be one of several variants. Unlike records, variants need to be defined before they can be used.
type MyOption a = | Some a | None
Some 1
Match expressions
To allow variants to be unpacked so their contents can be retrieved, Gluon has the match
expression.
match None with
| Some x -> x
| None -> 0
Here, we write out a pattern for each of the variant's constructors and the value we pass in (None
in this case) is matched to each of these patterns. When a matching pattern is found, the expression on the right of ->
is evaluated with each of the constructor's arguments bound to variables.
match
expressions can also be used to unpack records.
match { x = 1.0, pi = 3.14 } with
| { x = y, pi } -> y + pi
// Patterns can be nested as well
match { x = Some (Some 123) } with
| { x = Some None } -> 0
| { x = Some (Some x) } -> x
| { x = None } -> -1
let
bindings can also match and unpack on data but only with irrefutable patterns. In other words, only with patterns which cannot fail.
// Matching on records will always succeed since they are the only variant
let { x = y, pi } = { x = 1.0, pi = 3.14 }
in y + pi
// These will be rejected however as `let` can only handle one variant (`Some` in this example)
let Some x = None
let Some y = Some 123
x + y
Tuple expressions
Gluon also have tuple expressions for when you don't have sensible names for your fields.
(1, "", 3.14) // (Int, String, 3.14)
Similarily to records they can be unpacked with match
and let
.
match (1, None) with
| (x, Some y) -> x + y
| (x, None) -> x
let (a, b) = (1.0, 2.14)
a + b
Infact, tuples are only syntax sugar over records with fields named after numbers (_0
, _1
, ...) which makes the above equivalent to the following code.
match { _0 = 1, _1 = None } with
| { _0 = x, _1 = Some y } -> x + y
| { _0 = x, _1 = None } -> x
let { _0 = a, _1 = b } = { _0 = 1.0, _1 = 2.14 }
a + b
While that example is obviously less readable the tuple syntax, the important thing to note is that tuples equivalency with records allows one to access the fields of a tuple directly without unpacking.
(0, 3.14)._1 // 3.14
Lambda expressions
While we have seen that functions can be defined in let expressions it is often valuable to define a function without giving it an explicit name.
// \(<identifier)* -> <expr>
\x y -> x + y - 10
// Equivalent to
let f x y = x + y - 10 in f
Type expressions
Gluon allows new types to be defined through the type
expression which, just like let
, requires in <expression>
to be written at the end to ensure it returns a value.
// type <identifier> <identifier>* = <type> in <expression>
type MyOption a = | None | Some a
let divide x y : Int -> Int -> MyOption Int =
if (x / y) * y == x then
Some (x / y)
else
None
in divide 10 4
An important difference from many languages however is that type
only defines aliases. This means that all types in the example below are actually equivalent to each other.
type Type1 = { x: Int }
type Type2 = Type1
type Type3 = { x: Int }
let r1 : Type1 = { x = 0 }
let r2 : Type2 = r1
let r3 : Type3 = r2
in r1
Mutually recursive types can be defined by writing a rec
block.
rec
type SExpr_ = | Atom String | Cons SExpr SExpr
type SExpr = { location: Int, expr: SExpr_ }
in Atom "name"
Do expressions
do
expressions are syntax sugar over the commonly used Monad
type which is used to encapsulate side-effects. By using do
instead of >>=
or flat_map
we can write our code in a sequential manner instead of the closures necessary for sugar free versions. Note do
still requires a flat_map
binding to be in scope with the correct type or else you will get an error during typechecking.
Some 1 >>= (\x -> Some (x + 2))
// or
flat_map (\x -> Some (x + 2)) (Some 1)
// are equivalent to
do x = Some 1
Some (x + 2)
// The binding can also be a (irrefutable) pattern
do { y } = Some { y = "" }
Some y
Sequence expressions
Sequence expressions work just like do
expressions, only they do not have a binding.
let io @ { ? } = import! std.io
seq io.print "Hello"
seq io.print " "
io.println "world!"
The seq
keyword can also be omitted.
let io @ { ? } = import! std.io
io.print "Hello"
io.print " "
io.println "world!"
(In the future one of these ways are likely to be deprecated with only one way remaining, the formatter will be able to update the code in any case).
Indentation
If you have been following along this far, you may be think that the syntax so far is pretty limiting. In particular, you wouldn't be wrong in thinking that the let
and type
syntax are clunky due to their need to be closed by the in
keyword. Luckily, Gluon offers a more convenient way of writing bindings by relying on indentation.
When a token starts on the same column as an unclosed let
or type
expression, the lexer implicitly inserts an in
token which closes the declaration part and makes the following expression into the body.
let add1 x = x + 1
add1 11 // `in` will be inserted automatically since `add1 11` starts on the same line as the opening `let`
If a token starts on the same column as an earlier expression, but there is not an unclosed type
or let
expression, Gluon treats the code as a block expression, meaning each expression is run sequentially, returning the value of the last expression.
do_something1 ()
do_something2 () // `do_something1 ()` is run, then `do_something_2`. The result of `type ...` is the result of the expression
type PrivateType = | Private Int
let x = Private (do_something3 ())
do_something3 ()
match x with
| Private y -> do_something4 x
Indented blocks can be used to limit the scope of some variables.
let module =
let id x = x
type MyInt = Int
{ MyInt, id, pi = 3.14 }
module.id module.pi
Which is equivalent to:
let module =
let id x = x
in
type MyInt = Int
in { MyInt, id, pi = 3.14 }
in
module.id module.pi
Typesystem
In gluon, identifiers starting with an uppercase letter is a type whereas identifiers starting with a lowercase letter are type variables.
Function types
<type> -> <type>
Function types are written using the (->)
operator, which is right associative. This means that the function type Int -> (Int -> Int)
(A function taking one argument of Int and returning a function of Int -> Int
) can be written as Int -> Int -> Int
.
Record type
type_identifier := [A-Z][A-Za-z_0-9]*
variable_identifier := [a-z][A-Za-z_0-9]*
field := <type_identifier> <variable_identifier>* = <type>
| <type_identifier>
| <variable_identifier> : <type>
record_type := { (field,)* }
// Example
{
Float,
BinaryOp = Float -> Float -> Float,
pi : Float,
sin : Float -> Float
}
Records are Gluon's main way of creating associating related data and they should look quite familiar if you are familiar with dynamic languages such as javascript. Looks can be deceiving however as gluon's records are more similar to a struct in Rust or C as the order of the fields are significant, { x : Int, y : String } != { y : String, x : Int }
. Furthermore, records are immutable, meaning fields cannot be added nor removed and the values within cannot be modified.
In addition to storing values, records also have a secondary function of storing types which is Gluon's way of exporting types. If you have used modules in an ML language, this may look rather familiar. Looks can be deceiving however as 'type fields' must match exactly in gluon which means there is no subtyping relationship between records ({ Test = { x : Int } }
is not a subtype of { Test = Float }
). This may change in the future.
{ Test = { x : Int } }
Polymorphic records
Records in gluon can also be polymorphic, that is, just like a function can be polymorphic over it's arguments or return type records can be polymorphic over the fields they contain (see also Row type.
// `f` only requires that the record holds an `x` and `y` field
let f record : { x : Int, y : Int | r } -> Int = record.x + record.y
let z = f { x = 1, y = 2 }
f { x = 1, y = 2, other = "abc" } // The record we pass can hold more fields than the type specifies
Variant type
( | <identifier> (<type>)* )*
| Err e | Ok t
Gluon also has a second way of grouping data which is the enumeration type which allows you to represent a value being one of several variants. In the example above is the representation of Gluon's standard Result
type. It represents either the value having been successfully computed (Ok t
) or that an error occurred (Err e
).
Generalized algebraic data type
Variants also have an alternate syntax which allows Generalized Algebraic Data Type (GADT) to be specified.
type <identifier> (<identifier>)* = ( | <identifier> : (<type>)* )*
type Result e t =
| Err : e -> Result e t
| Ok : t -> Result e t
This encodes the same type as the variant in the previous (with the restriction that the variant must be specified as the top of the type definition). While this simple example doesn't do anything that a normal variant could not define it is possible to encode much more information about what a variant contains through such as the canonical expression example.
type Expr a =
| Int : Int -> Expr Int
| Bool : Bool -> Expr Bool
| Add : Expr Int -> Expr Int -> Expr Int
| If : Expr Bool -> Expr a -> Expr a -> Expr a
let eval e : Expr a -> a =
match e with
| Int x -> x
| Bool x -> x
| Add l r -> eval l + eval r
| If p t f -> if eval p then eval t else eval f
let int : Int = eval (If (Bool True) (Add (Int 1) (Int 2)) (Int 0))
let bool : Bool = eval (Bool True)
()
Through specifying a more specific type in the return type of a GADT variant we enforce that the variant can only contain that specific type in the argument. We can then exploit it when matching to refine the argument.
Alias type
<identifier> (<type>)*
Int
Float
Option Int
Ref String
The last kind of type which Gluon has is the alias type. An alias type explicitly names some underlying type which can either be one of the three types mentioned above or an abstract type which is the case for the Int
, String
and Ref
types. If the underlying type is abstract, then the type is only considered equivalent to itself (ie if you define an abstract type of MyInt
which happens to have the same representation as Int
the typechecker will consider these two types as being distinct).
Higher-kinded types
Higher-kinded types are a fairly abstract concept in Gluon and you may create entire programs without any knowledge about them. Sometimes they are a very valuable tool to have, as they can be used to create very powerful abstractions.
Just as all values such as 0 : Int
, "Hello World!" : String
and Some 4.0 : Option Float
each have a type, these types themselves have their own 'type' or the 'kind' as it is called. For the types of concrete values the Kind
is always Type
so for the earlier examples Int : Type
, String : Type
and Option Float : Type
. That is not very useful on its own but it becomes more interesting when we consider the kind of Option : Type -> Type
. Type -> Type
looks rather like the type of a function such as show_int : Int -> String
but, instead of taking a value, it takes a type and produces a new type. In effect, this lets us abstract over types instead of just over values. This abstraction facility can be seen in the Functor : (Type -> Type) -> Type
type which takes a type with kind Type -> Type
as argument which is exactly the kind of Option
(or List
, Result a
).
Row type
A Row
is defined as a set of (identifier, Type)
pairs and are used to describe the contents of records, variants and effects. A Row
type has it's own special Row
kind (instead of the normal Type
) and has special treatment during typechecking if it is marked as extensible. If two extensible rows are unified (checking if they are equivalent) then they do not need to exactly match, but if one of the rows has more fields than the other (or vice-versa) then the other record is simply constrained to also have those fields.
// Unifying
{ x : Int | r } <=> { y : String | s }
// Results in
{ x : Int, y : String | t }
// The same goes for effects and variants
Rows are currently a bit of an implementation detail and are thus only indirectly exposed to users through records, variants and effects. (In the future direct manipulation and definitions of rows may be added).
Universal quantification
First draft
Universal quantification is what Gluon's "generic types" are called. Consider the identity function in Rust.
fn id<T>(x: T) -> T {
x
}
In Gluon the same function would be written in the following way if it were fully annotated.
let id x : forall a . a -> a = x
// Types can of course be omitted in which the same type as above will be inferred
let id x = x
// Unbound type variables (`a` in this example) are allowed, in which case a `forall` will be
// inserted at the at the "top" of the type (same place as the type above)
let id x : a -> a = x
So in simple case, forall
is no different from declaring type parameters to a function in Rust. But forall
also serves more advanced use cases and is at the center when it comes to making Gluon's records work as modules.
let module =
let id x = x
{ id }
module.id 0
module.id ""
If we were to emulate the above code in Rust we would probably end up with something like this code.
struct Module<T> {
id : Box<Fn(T) -> T>,
}
let module = Module {
id: Box::new(|x| x),
};
(module.id)(0);
(module.id)("");
Alas, this does not work in Rust since module
will be inferred to the type Module<i32>
which makes the second call to id
a type error. In gluon it works as the type of module
is actually { id : forall a . a -> a }
and not forall a . { id : a -> a }
which is the closest analogue to the Rust example.
Intuitively, we can say that since gluon lets forall
be specified inside types we can avoid specializing the type (in this case forall a . a -> a
) which lets us specialize module.id
once for each call to id
instead of specializing the entire module at once.
While all of this looks quite complex, it should for the most part not matter when writing code and common idioms will just work as expected!
Implicit arguments
Sometimes, there is a need to overload a name with multiple differing implementations and let the compiler chose the correct implementation. If you have written any amount of Gluon code so far, you are likely to have already encountered this with numeric operators such as (+)
or comparison operators such as (==)
. If you inspect the types of these functions you will find that the first argument of these functions look a little bit different from normal functions.
(==): : forall a . [std.prelude.Eq a] -> a -> a -> std.types.Bool
(+): forall a . [std.prelude.Num a] -> a -> a -> a
This different looking argument is an implicit argument which means that you do not need to pass a value for this argument, instead, the compiler will try to find a value with a type that matches the type signature. So if you were to call 1 == 2
the compiler will see that the type variable a
has been unified to Int
. Then when the implicit argument is resolved it will look for a value with the type Eq Int
.
Since searching all possible bindings currently in scope would introduce to many ambiguity errors the compiler does not search all bindings when trying to determine an implicit argument. Instead, whether a binding is considered for implicit resolution is controlled by the #[implicit]
attribute. When marking a let
binding as #[implicit]
and this binding is in scope it will be considered as a candidate for all implicit arguments. The #[implicit]
attribute can also be set on a type
binding in which case it applied to all let
bindings which has the type declared by the type
binding.
#[implicit]
type Test = | Test ()
let f y: [a] -> a -> a = y
let i = Test ()
// `i` gets selected as the implicit argument since `#[implicit]` is marked on the type and `i : Test`
()
Since importing each individual binding used as an implicit argument quickly becomes tedious there is a short-hand to bring all implicit bindings from a record into scope.
let { eq, ord } = import! std.int
1 == 1 && 1 < 2
// Also brings in `show`, `num` ...
let { ? } = import! std.int
1 == 1 && 1 < 2
For standard types such as Int
, Float
, String
, Bool
and Option
this gets injected through the implicit prelude that is inserted before all code which lets ==
, <
etc to work out of the box.
Passing implicit arguments explicitly
If you only use implicit functions as explained above then it might just seem like a different name for traits (Rust) or type classes (Haskell). While it is true that the main reason for implicit arguments is to emulate traits/type classes implicit arguments is more powerful than those approaches as it is also possible to override the implicit resolution and instead give the argument explicitly by prefixing the argument with ?
.
let list @ { List } = import! std.list
// Make a custom equality function which returns true regardless of the elements of the list
#[infix(left, 4)]
let (===) = (list.eq ?{ (==) = \x y -> True }).(==)
Cons 1 (Cons 2 Nil) === Cons 3 (Cons 4 Nil)
The inverse also works when defining a function with implicit arguments. By prefixing an argument by ?
an implicit arguments will be given a name inside the function (if ?
is not given in a function definition the argument will only be available for implicit resolution).
let eq ?a : [Eq a] -> Eq (Option a) = {
(==) = \l r ->
match (l, r) with
| (Some l_val, Some r_val) -> a.(==) l_val r_val
| (None, None) -> True
| _ -> False,
}
()
Importing modules
As is often the case, it is convenient to separate code into multiple files which can later be imported and used from multiple other files. To do this, we can use the import!
macro which takes a single string literal as argument and loads and compiles that file at compile time before the importing module is compiled.
For example, say that we need the assert
function from the test
module which can be found at std/test.glu
. We might write something like this:
let { assert } = import! std.test
assert (1 == 1)
Writing modules
Importing standard modules is all well and good but it is also necessary to write your own once a program starts getting too big for a single file. As it turns out, if you have been following along so far, you already know everything about writing a module! Creating and loading a module in gluon entails creating a file containing an expression which is then loaded and evaluated using import!
. import!
is then just the value of the evaluated expression.
// module.glu
type Named a = { name: String, value: a }
let twice f x = f (f x)
{ twice, Named }
//main.glu
let { twice, Named } = import! "module.glu"
let addTwice = twice (\x -> x + 1)
let namedFloat : Named Float = { name = "pi", value = 3.14 }
addTwice 10
Though modules are most commonly a record, this does not have to be the case. If you wanted, you could write a module returning any other value as well.
// pi.glu
3.14
//main.glu
let pi = import! "pi.glu"
2 * pi * 10
Metadata
Sometimes we need a way to associate some extra information to a specific named binding and have it be visible whenever we refer to that name. The most common reason for this is documentation comments. When we write some documentation for a binding we would like this documentation to be visible whenever someone uses that binding.
For this reason gluon runs a "metadata" pass on all code in which "metadata" (such as documentation comments) gets statically propagated throughout the code.
/// Adds one to the argument `x`
let add1 x = x + 1
/// Adds two to the argument `x`
let add2 x = x + 2
add1 // Looking up the metadata of this variable yields the documentation of `add1`
// It can't be statically determined which branch the `if` takes (since constant folding do not
// take place). Thus `addN` do not get any metadata from either `add1` or `add2`
let addN = if True then add1 else add2
addN
Attributes
In addtion to documentation comments gluon also has a special notion of attributes that get propagated in the same manner. These are specified using the following syntax.
Attribute : #[ AttributeContents ]
AttributeContents :
#[ IDENTIFIER ]
| #[ IDENTIFIER ( TOKENS* ) ]
#[infix(..)]
#[infix(<left|right>, <NON-NEGATIVE INTEGER>)]
The #[infix]
attribute is used to specified the fixity and precedence of infix operators. This lets us specify that multiplication binds tighter that addition.
#[infix(left, 6)]
let (+) ?num : [Num a] -> a -> a -> a = num.(+)
#[infix(left, 7)]
let (*) ?num : [Num a] -> a -> a -> a = num.(*)
#[implicit]
#[implicit]
The #[implicit]
attribute is used to mark value bindings or type bindings as usable for implicit resolution. If specified on a value binding then only that specific binding can be used on implicit resolution. If specified on a type binding then all bindings that has that type can be used in implicit resolution.
// Can be used as an implicit argument
#[implicit]
let binding : MyType = ..
#[implicit]
type Eq a = { (==) : a -> a -> Bool }
// Can be used as an implicit argument
let eq_Int : Eq Int = ..
#[derive(..)]
#[derive(IDENTIFIER)]
The #[derive(..)]
attribute can be used on type
bindings to generate implementations for some traits. Currently only Eq
and Show
can be derived and only non-recursive and self-recursive types are supported (mutually recursive types do not work for the moment).
#[derive(Eq, Show)]
type Tree a = | Tip a | Branch (Tree a) (Tree a)
let tree = Branch (Tip 1) (Branch (Tip 2) (Tip 3))
let tree_str = show tree // "Branch (Tip 1) (Branch (Tip 2) (Tip 3))"
tree == Tip 1 // False
#[doc(hidden)]
The #[doc(hidden)]
attribute hides the binding, omitting it from generated documentation.
Modules
Importing modules
As is often the case, it is convenient to separate code into multiple files which can later be imported and used from multiple other files. To do this, we can use the import!
macro which takes a single string literal as argument and loads and compiles that file at compile time before the importing module is compiled.
For example, say that we need the assert
function from the test
module which can be found at std/test.glu
. We might write something like this:
let { assert } = import! std.test
assert (1 == 1)
Writing modules
Importing standard modules is all well and good but it is also necessary to write your own once a program starts getting too big for a single file. As it turns out, if you have been following along so far, you already know everything about writing a module! Creating and loading a module in gluon entails creating a file containing an expression which is then loaded and evaluated using import!
. import!
is then just the value of the evaluated expression.
// module.glu
type Named a = { name: String, value: a }
let twice f x = f (f x)
{ twice, Named }
//main.glu
let { twice, Named } = import! "module.glu"
let addTwice = twice (\x -> x + 1)
let namedFloat : Named Float = { name = "pi", value = 3.14 }
addTwice 10
Though modules are most commonly a record, this does not have to be the case. If you wanted, you could write a module returning any other value as well.
// pi.glu
3.14
//main.glu
let pi = import! "pi.glu"
2 * pi * 10
Embedding API
The API with which the host language interacts with Gluon is very important part of the library. While the complete API can be found in the Rustdoc, this section will explain the most important parts. Please note that the API can change at any point and there are still some public functions which should actually be internal.
Creating a virtual machine
Before you are able to do anything with the library, you will need to create a virtual machine. The virtual machine is responsible for running Gluon programs and can be created with the new_vm function.
Compiling and running gluon code
Once in possession of a RootedThread, you can compile and execute code using the run_expr method on the ThreadExt extension trait.
let vm = new_vm();
let (result, _) = vm
.run_expr::<i32>("example", "1 + 2")
.ok();
assert_eq!(result, Some(3));
Notably, if we were to execute a script with side effects the code above will actually not run the side effects. To make gluon run side effects we need to set the run_io flag on ThreadExt.
let vm = new_vm();
let script = r#"
let io = import! std.io
io.print "123"
"#;
// Returns an action which prints `123` when evaluated
vm.run_expr::<IO<()>>("example", script)
.unwrap();
// Prints `123` to stdout
vm.run_io(true);
vm.run_expr::<IO<()>>(&vm, "example", script)
.unwrap();
Often, it is either inconvenient or inefficient to compile and run code directly from source code. To write the above example in a more efficient way, we could instead load the (+)
function and call it directly.
let vm = new_vm();
// Ensure that the prelude module is loaded before trying to access something from it
vm.run_expr::<OpaqueValue<&Thread, Hole>>("example", r#" import! std.prelude "#)
.unwrap();
let mut add: FunctionRef<fn (i32, i32) -> i32> = vm.get_global("std.prelude.num_Int.(+)")
.unwrap();
let result = add.call(1, 2);
assert_eq!(result, Ok(3));
Calling Rust functions from gluon
Gluon also allows native functions to be called from gluon. To do this we first need to define the function so it is available when running Gluon code.
fn factorial(x: i32) -> i32 {
if x <= 1 {
1
} else {
x * factorial(x - 1)
}
}
fn load_factorial(vm: &Thread) -> vm::Result<vm::ExternModule> {
vm::ExternModule::new(vm, primitive!(1, factorial))
}
let vm = new_vm();
// Introduce a module that can be loaded with `import! factorial`
add_extern_module(&vm, "factorial", load_factorial);
let expr = r#"
let factorial = import! factorial
factorial 5
"#;
let (result, _) = vm.run_expr::<i32>("factorial", expr)
.unwrap();
assert_eq!(result, 120);
add_extern_module can do more than just exposing simple functions. For instance, the primitives module export large parts of Rust's string and float modules directly as records in Gluon under the str
and float
modules respectively.
let vm = new_vm();
let (result, _) = vm.run_expr::<String>("example", " let string = import! \"std/string.glu\" in string.trim \" Hello world \t\" ")
.unwrap();
assert_eq!(result, "Hello world");
Marshalling types
An important part of embedding Gluon is translating non-primitive types from Gluon types to Rust types and vice versa, allowing you to seamlessly implement rich APIs with complex types. This translation is called marshalling.
Required traits
Gluon provides several traits for safely marshalling types to and from Gluon code:
-
VmType provides a mapping between Rust and Gluon types. It specifies the Gluon type the implementing Rust type represents. All types that want to cross the Gluon/Rust boundary must implement this trait.
-
Getable: Types that implement
Getable
can be marshalled from Gluon to Rust. This means you can use these types anywhere you are receiving values from Gluon, for example as parameters for a function implemented on the Rust side or as return type of a Gluon function you want to call from Rust. -
Pushable is the counterpart to
Getable
. It allows implementing types to be marshalled to Gluon. Values of these types can returned from embedded Rust functions and be used as parameters to Gluon functions. -
Userdata allows a Rust type to be marshalled as completely opaque type. The Gluon code will be able to receive and pass values of this type, but cannot inspect it at all. This is useful for passing handle-like values, that will be mostly used by the Rust code.
Pushable
is automatically implemented for all types that implementUserdata
.Getable
is automatically implemented for&T where T: Userdata
when used as argument to a Rust function, for placesOpaqueValue
can be used as a smart pointer around aUserdata
value or theUserdataValue
extractor can be used to clone the value.
Gluon already provides implementations for the primitive and common standard library types.
Implementing the marshalling traits for your types
You can implement all of the above traits by hand, but for most cases you can also use the derive macros in gluon_codegen.
You will also have to register the correct Gluon type. If you are marshalling Userdata
, you
can use Thread::register_type
, otherwise you will need to provide the complete type definition
in Gluon. When using the serialization
feature, you can automatically generate the source
code using the api::typ::make_source
function.
Using derive macros
Add the gluon_codegen
crate to your Cargo.toml
this lets you import and derive the
VmType
, Getable
, Pushable
and Userdata
traits.
VmType
, Getable
and Pushable
can be implemented on any type which only consists of types which in turn implements
these traits whereas Userdata
can be derived for any type as long as it is Debug + Send + Sync
and has a 'static
lifetime.
Sometimes when deriving VmType
you do not want to define a new type. In this case you can use the vm_type
attribute
to point to another, compatible type. See the marshalling example for the complete source for the examples below.
// Using `vm_type` to point to compatible type defined in gluon
#[derive(Debug, PartialEq, VmType, Getable)]
#[gluon(vm_type = "std.list.List")]
enum List<T> {
Nil,
Cons(T, Box<List<T>>),
}
// Defines an opaque type with Userdata
#[derive(Userdata, Trace, Clone, Debug, VmType)]
// Lets gluon know that the value can be cloned which can be needed when transferring the value between threads
#[gluon_userdata(clone)]
// Refers to the `WindowHandle` type registered on the Rust side
#[gluon(vm_type = "WindowHandle")]
struct WindowHandle {
id: Arc<u64>,
metadata: Arc<str>,
}
Implementing by hand
The following examples will all assume a simple struct User<T>
, which is defined in a different
crate (You can find the full code in the marshalling example). To implement the marshalling traits,
we have to create a wrapper and implement the traits for it.
// defined by a different crate
struct User<T> {
name: String,
age: u32,
data: T,
}
VmType
VmType
requires you to specify the Rust type that maps to the correct Gluon type. You can
simply assign Self
. The heart of the trait is the make_type
function. To get the correct
Gluon type, you will have to look it up from the vm, using the fully qualified type name:
let ty = vm.find_type_info("examples.wrapper.User")
.expect("Could not find type")
.into_type();
If you have a non generic type, this is all you need. In our case, we will have to apply the generic type parameters first:
let mut vec = AppVec::new();
vec.push(T::make_type(vm));
Type::app(ty, vec)
You simply push all parameters to the AppVec
in the order of their declaration, and then
use Type::app
to construct the complete type.
Getable
Getable
only has one function you need to implement, from_value
. It supplies a reference
to the vm and the raw data, from which you have to construct your type. Since we are implementing
Getable
for a complex type, we are only interested in the ValueRef::Data
variant.
let data = match data.as_ref() {
ValueRef::Data(data) => data,
_ => panic!("Value is not a complex type"),
};
From data
we can now extract the individual fields, using lookup_field
for named fields or
get_variant
for unnamed fields (like in tuple structs or variants).
// once we have the field's value, we construct the correct type
// using its Getable implementation
let name = String::from_value(vm, data.lookup_field(vm, "name").unwrap();
In this example we used a struct, but if we wanted to construct an enum, we need to find out
what variant we are dealing with first, using the tag
method:
match data.tag() {
0 => // build first variant
1 => // build second variant
// ...
}
Pushable
To implement Pushable
, we need to interact with Gluon's stack directly. The goal is to create
a Value
that represents our Rust value, and push it on the stack. In order to do that, we need to
push the fields of our type first:
self.inner.name.push(vm, ctx)?
self.inner.age.push(vm, ctx)?;
self.inner.data.push(vm, ctx)?;
The ActiveThread
we get passed has a Context
that allows pushing values, but we can do even better and
use the record!
macro:
(record!{
name => self.inner.name,
age => self.inner.age,
data => self.inner.data,
}).push(ctx)
If we were pushing an enum, we would have to use Context::push_new_data
and manually specify the tag
of the pushed variant as well as its number of fields (zero if it's a variant with no attached data).
let val = match an_enum {
Enum::VariantOne => ctx.context().push_new_data(vm, 0, num_fields_in_variant_one),
Enum::VariantTwo => ctx.context().push_new_data(vm, 1, num_fields_in_variant_two),
}?;
Userdata
Implementing Userdata
is straight forward: we can either derive
the trait or use the default
implementation since there are no required methods. However, Userdata
also requires the type to implement
VmType
and Trace
. We can use the minimal VmType
implementation, it already
provides the correct make_type
function for us:
impl<T> VmType for GluonUser<T>
where
T: 'static + Debug + Sync + Send
{
type Type = Self;
}
The Trace
implementation can be automatically derived in most cases as it will just call it's methods on every field
of the type. However, this means that it expects that every field also implements Trace
, if that is not the case you
can opt out of tracing with the #[gluon_trace(skip)]
attribute. This is fine in many cases but can cause reference
cycles if your userdata stores values managed by Gluon's GC. However if it doesn't it is safe to just use skip
.
// Contains no gluon managed values so skipping the trace causes no issues
#[derive(Trace)]
#[gluon_trace(skip)]
struct SimpleType {
name: String,
slot: std::sync::Mutex<i32>,
}
// Here we store a `OpaqueValue` which is managed by gluon's GC. To avoid a reference cycle we must trace
// the field so gluon can find it. `gc::Mutex` is a drop-in replacement for `std::sync::Mutex` which is GC aware.
#[derive(Trace)]
struct Callback(gluon::vm::gc::Mutex<OpaqueValue<RootedThread, fn (i32) -> String>>);
Passing values to and from Gluon
Once your type implements the required traits, you can simply use it in any function you want to expose to Gluon.
If you want to receive or return types with generic type parameters that are instantiated on the Gluon side, you can use the Opaque type together with the marker types in the generic module:
// we define Either with type parameters, just like in Gluon
#[derive(Getable, Pushable, VmType)]
enum Either<L, R> {
Left(L),
Right(R),
}
// the function takes an Either instantiated with the `Opaque` struct,
// which will handle the generic Gluon values for us
use gluon::vm::api::OpaqueValue;
// The `generic` sub-module provides marker types which mimic generic parameters
use gluon::vm::api::generic::{L, R};
fn flip(
either: Either<OpaqueValue<RootedThread, L>, OpaqueValue<RootedThread, R>>,
) -> Either<OpaqueValue<RootedThread, R>, OpaqueValue<RootedThread, L>> {
match either {
Either::Left(val) => Either::Right(val),
Either::Right(val) => Either::Left(val),
}
}
Now we can pass Either
to our Rust function:
// Either is defined as:
// type Either l r = | Left l | Right r
let either: forall r . Either String r = Left "hello rust!"
// we can pass the generic Either to the Rust function without an issue
do _ =
match flip either with
| Left _ -> error "unreachable!"
| Right val -> io.println ("Right is: " <> val)
// using an Int instead also works
let either: forall r . Either Int r = Left 42
match flip either with
| Left _ -> error "also unreachable!"
| Right 42 -> io.println "this is the right answer"
| Right _ -> error "wrong answer!"
Standard types and functions
The API documentation for the standard library can be found here. Some of the modules are only available if Gluon is compiled with the required features:
std.regex
requires theregex
feature (enabled by default)std.random
requires therand
feature (enabled by default)- All
std.json.*
modules require theserialization
feature
TODO
Prelude
When compiling an expression, the compiler automatically inserts a small prelude before the expression itself, which gives automatic access to basic operators such as +
, -
, etc as well as types such as Option
and Result
.
Threads and channels
Gluon has support for cooperative threading and communication between them through the Thread
and Sender
/Receiver
types.
TODO