Skip to content

Language

DeedleFake edited this page Mar 12, 2018 · 20 revisions

Warning

WDTE is under heavy development. Large parts of the language are not implemented yet or are only partially implemented. While most of the stuff on this page probably won't change much, some stuff very well may at any minute and with little warning.

Examples used in this document are designed to run in the WDTE playground.

WDTE is a dynamically-typed, functional-ish, lazily-evaluated scripting language.

Dynamically-Typed
In WDTE, the type follows the value at runtime, rather than the variable at parse-time. There are only a few built-in types, strings, numbers, and arrays, but more can easily be added.
Functional-ish
WDTE takes some minor design inspiration from Haskell and Lisp, but drops the functional scheme where it's too annoying. WDTE was designed primarily as a scripting language, so emphasis on building good, functional abstractions is basically non-existent. If one wants, the language can be treated almost as if it's not functional at all.

That being said, there are a few quirks along those lines which will be covered in greater detail below. In particular, every single value of any kind in the language is a function. Even the built-in types are functions.

Lazily-Evaluated
Functions in WDTE are comprised of recursive chains of unevaluated expressions. These expressions are only evaluated when the Go package's client explicitly calls them. This will be explained in detail below.

Literals

WDTE only has a handful of types of literals:

Strings
String literals can be either single or double quoted. In either case, several escape sequences are available, including `\n` and `\t` Literals may contain newline characters, which will be included by default. If the newline itself is escaped, it will not be included. Any other character, when escaped, is simply included in the string.
Numbers
Number literals may be any positive or negative floating point number. If the number is positive, it can not be preceded by a `+`. `3`, `-3`, `-3.5`, and `3.5` are all valid literals. `+3` is not.
Arrays
Array literals are semicolon seperated lists of expressions contained between square brackets. For example, `[3; 5]`. A semicolon before the closing square bracket is technically required by the grammar, but the scanner will insert it automatically if there isn't one there explicitly. In general, don't include the semicolon for single-line literals, but do include it for multiline literals.

There are a few more built-in types, such as booleans, but they are not usable by default. There is, however, a standard library which includes functions for dealing with them.

Keywords and Symbols

Along with the above literals, there are a number of keywords and symbols, as well as identifiers. Identifiers are differentiated from strings by not being surrounded by quotes. Other than that, they may contain any character that wouldn't cause an ambiguity with the keywords, symbols, or literals. For example, the aforementioned +3 is a valid identifier. Don't use it though. That would be dumb. Identifiers are used for denoting functions, arguments, and imports.

Here's a list of all of the keywords, including symbols:

  • .
  • ->
  • --
  • {
  • }
  • [
  • ]
  • (
  • )
  • =>
  • ;
  • :
  • (@
  • switch
  • default
  • memo
  • let
  • import

Comments begin with a # and run until the end of the line.

Example

Now that the initial definitions are out of the way, how about an example with a line-by-line walkthrough?

Gist

01 let s => import 'stream';
02
03 # fib returns the nth Fibonacci number.
04 let memo fib n => switch n {
05	== 0 => 0; == 1 => 1;
06	default => + (fib (- n 1)) (fib (- n 2));
07 };
08
09 print (fib 5);
10
11 s.new [5; 3; 8]
12	-> s.map (+ 2)
13	-> s.collect
14	-> print
15	;

TODO: Fix everything below this line. It hasn't been updated since #58 was finished.

Before getting to the line-by-line walkthrough, there are a couple of things to note about this example. The first is that if you parse the above code with the library and try to run a function, it will crash. WDTE defaults to having almost no functions defined for it at all. This includes basic functions such as +, print, and others. There is a 'standard library', composed of several standard modules, but WDTE also doesn't know how to import modules without being told, including the standard library.

The second thing to note is the overall structure. A WDTE script, or module, is, at its core, a semicolon-seperated list of import statements and function declarations, in any order. In the above example, there is only one import statement, on line 1. An import statemnt begins with a string literal, which can be either single or double quoted, is followed by an assignment operator (=>), and an identifier. When the script is parsed, the client can tell the library how it want to handle imports. For more information, see the library overview.

Function Declarations

Now lets walk through the function definitions. Lines 3 through 6 contain a definition for a simple, recursive Fibonacci number calculator. A function declaration is zero or more modifiers, an identifier, by zero or more argument declarations, which are also identifiers, followed by the assignment operator, followed by an expression.

In this case, fib has been declared as a 'memo', a special type of function that checks if the same input has been passed to it twice and returns a result cached from the first time around. Memos have a couple of quirks, but they're a good fit for something like a recursive Fibonacci function. Without declaring it as a memo, passing it a high argument, such as fib 100, will use a lot of system resources, and probably take quite a while. A memo, on the other hand, can calculate it in less than a second.

Memos are useful, but one must take care when declaring them. For one thing, there is some overhead in looking values up in the cache. It's a good tradeoff for something that uses heavy amounts of branching recursion, such as fib, or something that you'll be calling a lot which takes a while to calculate, but it's something to keep in mind. Another thing to keep in mind is that a memo will only ever run once for a given set of arguments. This means that it is generally a bad idea to memoize a function that calls Go code, as the Go code might do something different when called a second time with the same arguments. Finally, memos also have an odd effect on their arguments. Specifically, they evaluate them when they are called, rather than later the way other functions do.

Expressions

An expression is a 'single', which will be explained in a second, followed by zero or more arguments, also singles, optionally followed by a 'chain'. A single is a type of simple expression. It's any expression which contains no arguments, essentially. In other words, it's a single function reference, a literal, a compound, or a switch.

Besides literals, function references are the simplest type of single. A function reference is an identifier optionally followed by a . and another idenentifier. If the second part is absent, the first identifier is assumed to refer to an argument to the current function or, if none match, then to another function declared elsewhere in the same module. If the second part is present, then the first is assumed to be a reference to an import, and the second is a function defined in the imported module. The first is a namespace, in other words. An important thing to note is that only functions declared in imported modules can be accessed from the current module, not modules imported by those other modules.

Switches

Switches are WDTE's only conditional expression. A switch starts with the keyword switch, followed by an expression, followed by zero or more semicolon-seperated cases in squigly braces. A case is an expression, the assignment operator, and another expression. Cases are evaluated in the order that they appear.

Cases work a bit oddly. Essentially, the left-hand side of a case is evaluated. Then, the initial condition is passed to the output of the left-hand side. If the result of that is a boolean true, then the case is picked. If it's any other value, then the case is rejected. If a case is picked, then the right-hand side is evaluated and returned.

In place of an expression on the left-hand side, the keyword default may be used. If a default case is encountered while checking cases, it is always returned. Once a case is returned, no more cases will be checked. Note that the last case must be followed by a semicolon, unlike with arrays.

Compounds

On the right-hand side of the default case on line 5 can be found several compounds. A compound is denoted by parantheses, which makes them look a tad lisp-like, but there are a few differences. A compound is a type of expression that contains a semicolon-seperated list of other expressions. Unlike arrays, however, compounds are not a type in and of themselves. When evaluated, a compound evaluates each expression in itself in turn, and returns the result of the last expression. Because of this, a compound containing only a single element, as on line 6, is effectively a subexpression. This allows function calls, complete with arguments, to be passed as arguments to other functions. In this case, the two recursive function calls necessary for a Fibonacci caluclation are being added together.

Moving to the main function on lines 10 through 17, things get a little more interesting. The first thing to note is the use of the compound to make main essentially a series of expressions, rather than a simple single-expression form. In this form, a compound is used for bypassing the functional, expression-based syntax that the language defaults to. When main is evaluated, each of the two sub-expressions will be called in the order given. Note as well the trailing semicolon on the second expression; the rules regarding the last semicolon are the same for compounds as they are for arrays.

Chains

The first expression of the compound isn't of much interest. It doesn't do anything that wasn't demonstrated in the Fibonacci example. The second, however, demonstrates the previously mentioned chain feature, which is where WDTE's treatement of every single value as a function becomes very useful. A chain can be applied to any non-single expression. It is denoted by the chain operator (->) followed by another expression, which itself can contain a chain. The way a chain works can be slightly odd at first, but it's actually fairly simple to work with once you get the hang of it.

Chains are based on the UNIX shell's well known pipe system. Unlike the UNIX shell, however, functions have no concept of stdin and stdout. Worse, they don't even run in parallel, making such a system impractical. Functions do have input and output, however, and this is how the chain system works. In a chain, the first function is evaluated. Then the second is evaluated. The output of the first function is then passed to the output of the second. Then the third is evaluated. The output of the previous section is then passed to the output of the third. And so on.

The oddity here is the calling order. In a two element chain, both elements are called with all of their arguments before they ever see each other. Then, because everything is a function, the output of the second can be called again, and it is to this call that the output of the first is passed as an argument.

As a simpler example than the above, consider the previous call to print. Rather than using a compound, the expression could have been fib 5 -> print. This does require the definition of print to be set up to handle the usage, but this is as simple as having print return itself when given no arguments. If it does, then it will simply be called again with the expression fib 5 as an argument the second time.

Variable Numbers of Arguments

Before finishing explaining expression evaluation, there is one other detail to note. Taking a page from Haskell, functions defined in the script itself may be called with less arguments than they were defined with. When this is done, another function is returned which takes the remaining arguments. This is, in fact, how passing functions around is done at all, as simply referencing the function is the same as calling it with no arguments. Functions defined in Go are not subject to this rule, but may be set up to work like this if they want to. Or they can do something else entirely.

Errors

All of the above is well and good, but sometimes things break. Functions don't do what someone expects, there's some bad input, a Go function panicked... In any of these situations, WDTE generates a value of a special error type and returns it. The error type has a number of unique properties. For example, if any of the elements of a compound return one of these error types, the compound stops early and returns the error. Switches do much the same thing. Attempting to call a function that doesn't exist also generates and returns an error. Go functions may handle errors in any way that they want to, but if a function panics and the panicked value is an error, then a new WDTE error is generated from that and returned.

With properly defined functions, an error can be propogated all the way back up to the original calling Go code, which can then handle it properly. Or the client could write some functions to handle it before it gets there. Like almost everything in WDTE, it's up to the client.

Conclusion

Now that you've finished the language overview, it would be prudent to read the library overview as well. The language itself is, after all, as mentioned above, completely non-functional without the library.

Get it?