This document is very much a work in progress. It is not an official specification and may not reflect the current state of the language. (svn revision: 6:7M )
IndtroductionPrism is an unusual programming language. It is an experiment in "computational storytelling" — writing stories down in way that how they are told depends on the outcome of an algorithm. This is not a new idea, but an underdeveloped one. From interactive fiction (IF), to video games, to choose-your-own-adventure books, the form is a growing one, but one that has only existed for a few decades. It is a new and bold medium; one in which all classics have yet to be written.
Prism is a proposal, an investigation, an exploration into how these stories can be told without resorting to complex, proprietary solutions. What the form lacks is a common language. Without one, every author writes his stories in isolation, separated from one another. Through a common language, one that can be read by anyone, a criticism will emerge. We will read each others stories, learn, grow, create tradition.
One of the original solutions to this problem was the choose your own adventure books I used to read as a kid. I did not realize until I was older, that their solution was in fact a programming language of sorts. When a branch was encountered, the reader was presented with something like this:
It eventually occurred to me, that what this really meant was:
It almost reads like a computer program in BASIC. It seems that a programming language is such a natural solution to this problem, that a group of non-programmers came up with one to solve it.
What follows is a proposal for a new language, one that attempts to be powerful and adaptable enough to solve this problem in a more general sense, and yet remain accessible to those with very little programming experience. We make no claim that this is the final solution. only that one exists, and should be discussed.
First StepsEvery project of any magnitude must begin with clear a definition of the goals. It is easy romanticize about new forms, but when without direction, it is just fantasy.
Prism aims to a general solution, which means it provides a means for more specific solutions to be built on top of it. This means that it is a complete high level programming language, not a data language for very specific goals. We want to create a language where anyone can write scripts for any specific format and still have them be recognizable.
Prism stands for Primitive Interactive Storytelling Mechanism, and it aims to be just that: primitive. By primitive, we mean do not mean incomplete, we mean uncomplex. We mean a small, consistent syntax. There are a few other design goals we have:
- The language must be concise.
- The language must be versatile.
- The language must be extensible.
- The language must not have a bias towards a particular platform.
If there is one single maxim that I strive to follow while writing and designing Prism it is something once written by Antoine de Saint-Exupèry: "In anything at all, perfection is finally attained not when there is no longer anything to add, but when there is no longer anything to take away."
Simple is beautiful.
SyntaxPrism borrows from a handful of languages, most notably ML, C, Perl, Python, and Lua, with bits and pieces taken from here and there. Hopefully it will look familiar to those with previous experience programming experience. The remainder of this introduction will be dedicated to presenting Prism's syntax.
Here are a few snippets to get you started. First, let's start with the famous "hello world" program. The first variation:
The square brackets and asterisks will be explained in the next section. There is another, very special, variation in Prism:
Notice the backticks. These are called Magic Strings in Prism. They are a very important feature, but for now you can just thing of it as a string that prints itself when it is evaluated.
Now we can look at something interesting. That is, the "Hello World" of functional languages: a recursive factorial function.
Here's another classic: Euclid's Algorithm for greatest common divisor.
Don't worry if aren't familiar with functional languages, recursion, factorials, Euclid, algorithms, or greatest common divisors; the point here is to just give you a taste of Prism's syntax. For many purposes, Prism can be quite useful using only a small subset of the full language. As the language matures, tutorials on using Prism for very specific tasks will be provided. Don't believe the propaganda: liberal arts majors can write code too.
Everything is a CommentMost computer languages have "comments" -- sections of input that the compiler/interpreter is told to ignore. Unless otherwise specified, all input to the Prism compiler is ignored. Everything is a comment, in other words. Prism works backwards from nearly every other language in that the compiler in told whats sections of the input to pay attention to, rather than what parts to ignore.
Prism scripts are meant to be read by humans. They are plain or formatted text with source code inserts. They are documents, in much the same way as scripts and screenplays, that are meant to be read: by actors, artists, designers, and others working on the project.
To delimit a section of code, use the brackets that we've seen in earlier examples. When we are in code mode, we can delimit comments "(*" and "*)". This way, comment and code deliminators can be nested as much as you like.
Note the lack of semi-colons after lines in that last example. Expressions are separated with newlines in Prism. Also, instead of any sort of brackets, sequences of expressions are grouped simply with indention level. All the expressions that are indented the same are considered part of the same sequence. This is a concept Python users are familiar with, but is a bit alien to most other languages.
There are a couple trade-offs that go along with this. Making whitespace have significance means you can't cram a bunch of expressions on one line (not without clever tricks, anyway). The upside is that there is less syntax to get in the way. Simple is beautiful.
Everything is an ExpressionBesides comments, everything in Prism is an expression. That is, there are no parts of the program that do something but don't evaluate to anything. Even a sequence of expressions is an expression itself which evaluates to the result of the last expression. In fact, an entire Prism program can be thought of as one expression.
Recall that indentation is used to group a sequence of expressions. Now that we know that sequence evaluates to the result of the last expression, we can make good use of this. We can "jump up" a level anywhere it Prism code to perform some sort of calculation.
While this may not be the most practical example, it gets the point across. The indented section, "jumps out" a level. It is akin to pushing and popping a stack. A new frame is pushed, the sequence is evaluated, and then we pop back to expression we were evaluating. This while push-evaluate-pop operation counts as one expression.
This seems silly, but this sort of generalization is important. We try to create a small number of general rules rather than a large number of special rules. Now we can define conditionals simply as: "if <expression> then <expression>", and each expression can do an unlimited number of things.
AtomsNow that we have explained a couple necessary concepts, we can begin a more careful examination of the language. We start off with types. Prism is dynamically and weakly typed. There can be various types of objects, similar to what you would see in Lisp. An atom is one of the most important and intrinsic.
An atom is either a number, a string, or a boolean value (or "nil", which is really the same as false). Prism does its best to convert freely between these, but it occasionally needs you help. There is no need to explicitly declare objects, simply initialize them.
The types are fairly self explanatory. You will see more of how these interact later on.
The VoidA want to very briefly mention one special identifier in Prism, which we call "the void". The void is simply an underscore than can be assigned to freely, but that assignment doesn't hold. It always evaluated to nil. In many ways it is analogous to "/dev/null" on the Unix operating system.
This may seem silly (and melodramatic), but this will come in handy later in cases where we need an identifier, but don't care about what happens to it. Mainly this is useful for anonymous functions, which we will get to in a bit.
OperatorsPrism has a handful of built in binary operators which are mostly familiar.
Operator | Type | Description |
---|---|---|
<- | any | assignment |
+ | number | addition |
- | number | subtraction |
* | number | multiplication |
/ | number | division |
^ | number | exponentiation |
mod | number | modulo |
= | any | equal |
< | number | less than |
<= | number | less than or equal |
> | number | greater than |
>= | number | greater than or equal |
<> | number | not equal |
and | boolean | logical and |
or | boolean | logical or |
, | any | listing operator |
=> | any | pairing operator |
It's important to note that binary operators are simply syntactic shortcuts for actual functions. In that sense, Prism doesn't really have any "unary operators" so much as it has some built in functions. Here are a few of them, though there are many more.
Function | Type | Description |
---|---|---|
- | number | negate |
not | boolean | logical not |
any | print to stdout |
Currently, there are two conditional constructs in Prism: "if" and "while".
An 'if' expression follows the form if <expression1> then <expression2> else <expression3>. The first expression is evaluated as a boolean. If that boolean is true, expression2 is evaluated, otherwise expression3" is.
The else is optional. It it is omitted, and the condition evaluates to false, the if expression then evaluates to nil.
As we saw in the last section, a sequence of indented expressions still counts as an expression. So we can of course nest if expressions.
Using "while" works much the same way, with the exception that <expression2> in executed repeatedly until <expression1> evaluates to false.
Two things to be aware of: "do" is just an alias for "then". It is syntactic sugar. Also, while expressions can have else clauses. This may seem a little odd, but it is consistent. The "else" expression is evaluated in the case that the loop condition is never true.
Both "if" and "while" expressions evaluate to a value. Keeping that in mind, we can write an interesting variation of our coin flipping program.
The "while" expression evaluates to the last evaluation of the body expression. In this case the while expression evaluates to the value of d.
Ubiquitous TablesPrism has only one type of compound type, but it is a very versatile one: associative arrays, or tables. This is an idea that came via Lua, which was in turn influenced by the ubiquitous lists in Lisp.
In Prism, tables take the place of arrays, lists, structures, classes, and other data structures that are built into other languages. First, let's see how we create a simple numerically indexed array.
Numerically indexed lists are simply comma separated lists of expressions. Elements can then be accessed using the common bracket notation. Let's see a more interesting example: a table indexed by strings:
Here keys are associated with values using the pairing operator ("=>"). The table is indexed here just as it was in the previous example.
Tables can also be enclosed in curly brackets, but these are optional unless the table is empty or only contains one element.
One syntactic shortcut that can be used with tables is the dot operator. Tables with strings as the keys can be indexed by a dot followed by the string without quotes. This only works for strings that follow the rules of prism identifiers.
Here you can see how tables in Prism can take the place of structures in other languages.
FunctionsDeclarations of functions is quite simple. It consists simply of the name of the function followed by the name of the parameters it takes, followed by a colon and the body of the function. Functions "return" the last expression evaluated.
The function repeats a loop, improving the guess until it is within a certain range a accuracy. When the loop ends, we simply evaluate "guess". The function call then evaluates to the value of guess.
If you have a good understanding of how things are evaluated in Prism, you'll realize that we don't even need to stick "guess" at the end of this function, because the while loop also evaluate the the value of guess, since an assignment to guess was the last expression executed.
Calling functions is equally straightforward.
We have already seen examples of recursive functions, so I will simply point out that recursive function do not require any special syntax. They work just the way you expect them to
One special case I would like examine is functions with no parameters. Technically, Prism functions must have at least one parameter, but there are ways around having to declare functions with a parameter and ignore it.
Suppose we have a function like this:
This is indeed a perfectly valid function declaration. But how do we call it?
Since functions in Prism always must take a parameter, this sort of function declaration is shorthand for a function that takes one parameter that is ignored. So to call it, we can simply pass any one parameter to it. It is a matter of convention though that we pass '()' (i.e. nil) to it.
Functions in Prism are first class objects. They can be passed as parameters, assigned, stuck in tables, returned from functions, and anything else you care to do with them.
Let's look at a simple example of a function that returns a function.
This is an example of a closure. Here the function "adder" is defined in terms of "n", the parameter passed into "make_adder".
Function declarations in Prism not only declare function with a given name, they also evaluate to that function. Hence is this last example, a function called "adder" is declared in the scope of "make_adder" and then evaluates to itself, which is then returned by "make_adder". Remember that everything is an expression, that includes function declarations.
This may seem confusing. Let's see an example of an anonymous function: a function directly from its declaration.
The expression in this examples evaluates to the square of 7 (that is, 49). Notice though, that it has the side effect of declaring a function called "square" that still exists after the function is evaluated. This may be undesirable, especially for a so called "anonymous" function.
The remedy is to make use of the void identifier ominously mentioned earlier on. Recall that this is a special identifier that never retains a value, and always evaluates to nil. Using it in place of "square", we can rewrite our last example as:
Now we have called a function declaration in place, but effectively no function is declared. It simply evaluates to that function, without binding it to an identifier, which was our goal for anonymous functions.
Lets see one more example of functions as first class objects.
Try to figure out what this example does on your own!
String Substitution(TODO)
Magic StringsMagic strings — delimited by backticks — are string literals with side effects. They perform some function on the string enclosed in quotes. By default that operation is to simply print the string to standard output.
The real power of this lies in redefining the function that the backticks perform. The function called on the string is called the "bang" function and is denoted by an exclamation point: '!'. It takes two arguments, the string itself, and the scope where it was called. Often, you won't need to worry about the scope parameter.
We can overload the bang operator to do something special for our particular program. Say we want the backticks to work like backticks and Perl and execute some shell command. (Although Perl backticks evaluate to the output of the command, which we won't do.)
Magic strings have some very important uses that we are about to see.
Writing Linear Character Dialogue in PrismHere we get the punch line of sorts; the answer to the question "how can I use a semi-functional, dynamically typed programming language to write stories?" The answer does not involve any special syntax, it instead uses the language's general rules in a special way. Here you get to see some justification for some of the seemingly dubious design decisions in Prism. Let's see an example of some linear dialogue, and then we can begin discussing nonlinear stories.
Let's take a minute an examine this. It looks a little like the language we've been using so far, but it also does not look too different from the actual printed play. First, notice that the characters just regular tables. The lines of dialogue also are just regular functions. Even though they are anonymous functions declared in the scope of HAMLET or POLONIUS. However, we use these basic language features in very special ways.
We see that the actual lines are contained in backticks. So we know that the bang operator is being called on them when the functions are executed. So, they are printed by default. But we can do a bit better than that. We can make the bang operator print the scope, which in this case is HAMLET or POLONIUS.
Great! Now the bang operator will print the character name, a newline, then what he says. We are off to a good start. But now we must decide how these function actually get executed. The trick is: that the interpreter creates a list of functions declared in a given scope, in the order they were declared. This list is called "__funcs__". So all we must do, is call each function in __funcs__ in sequence, and we have a program that recited Hamlet. Sure, it may not be impressive yet, but we are merely laying groundwork.
That was easy. "Table.iterate", is a function that takes to arguments, a function, and a table. Then it iterates over each item in the list and performs the function on it. In our "recite" function, we simply call each function in the table.
Writing Nonlinear Character Dialogue in PrismThe goal of using a programming language is so we can decide, computationally, how the story is told. Otherwise, we may as well just print a listing of a text file.
Here's the plan: a function will return either nil if the next function in line is to be called, or it can return another function to be called next instead. In a sense, it acts like a goto or jump in another language (that supports such barbarisms).
This requires some modifications to our recite function:
This is a bit more complicated, and it isn't necessary that you fully understand it since it is actually built-in. We provide it explicitly here just so its behavior isn't mysterious. The best way to understand it though, is to see an example.
Here we have an example of a story that can not be told by conventional means for one simple reason: it is infinite. It gets stuck in an intentional infinite loop. Notice the third line of dialogue. It actually has a name rather than being declared with the void identifier. In the function after that, the last thing evaluated is "Villian.A", meaning the lines will be said back and forth until the end of time (or the user presses ctrl-c).
This is a trivial example, but it is still something magical: a story that is not finite. There are limitless possibilities when we think about writing computational stories. We are free to write recursive stories, branching stories, (pseudo) random stories, and interactive stories. This is not some misguided attempt to think of literature as computer code, it is a misguided attempt to think of computer code as literature.
With that aside, let's look at some other examples. Suppose we want to let the user decide what to path to take in the story, similar to choose your own adventure books.
In the second function, a function is returned, but which one is determined by a function called "user_choose". We won't go over implementing the "user_choose" function, since our intention is to demonstrate the basic technique, and not specifics. You can now at least see how we can use this language to very concisely write interactive stories. We could also implement a function similar to "user_choice" which randomly selects a function from a list. We can use that to write the literary equivalent of John Cage's "chance music" compositions, which relied on coin tosses, or the I Ching, so the performance is different each time it is played.
ConclusionThe introduction is perhaps vague, in part to preserve brevity, and in part because the language is still developing. The specifics may change through the development, by the idea remains constant.