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 )

Indtroduction

Prism 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:

If you decide to start back home, turn to page 4. If you decide to wait, turn to page 5.

It eventually occurred to me, that what this really meant was:

IF <you decide to start back home> THEN GOTO 4 ELSE IF <you decide to wait> THEN GOTO 5

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 Steps

Every 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:

  1. The language must be concise.
  2. The language must be versatile.
  3. The language must be extensible.
  4. 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.

Syntax

Prism 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:

[* print "Hello World!" *]

The square brackets and asterisks will be explained in the next section. There is another, very special, variation in Prism:

[* `Hello World!` *]

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.

[* fact n: if n <= 1 then 1 else n * fact (n-1) *]

Here's another classic: Euclid's Algorithm for greatest common divisor.

[* gcd a b: if b = 0 then a else gcd b (a mod b) *]

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 Comment

Most 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.

Without any code deliminators, all of this is simply ignored by the compiler, meaning you can write whatever you want.

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.

Here is a countdown function. [* countdown_recursive n: if n >= 0 then print n countdown (n-1) (* return '()', which is the nil value in Prism *) else () *] That was fun, but some may prefer an imperative version using loops and counters; [* countdown_imperative n: while n >= 0 do print n n <- n - 1 () (* again, the function evaluates to nil *) *]
Whitespace Matters

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 Expression

Besides 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.

Let's set two variables, and then swap their values; [* a <- "foo" b <- "bar" a <- tmp <- b b <- a tmp *]

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.

Atoms

Now 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.

Initializing some atoms. [* (* strings *) str1 <- "foo" (* a string *) str2 <- 'bar' (* a string without substitution *) (* numbers *) num1 <- 3.141592654 num2 <- 42 num3 <- 0xdeadbeef (* booleans *) bool1 <- true bool2 <- false (* special -- nil, which = false *) nil <- () *]

The types are fairly self explanatory. You will see more of how these interact later on.

The Void

A 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.

"The Void" [* _ <- 42 print _ (* prints 'nil' *) *]

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.

Operators

Prism 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
print any print to stdout

Control

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.

[* min <- if a < b then a else b *]

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.

We assume that a, b, and c have already been initialized. [* min <- if a < b then if a < c then a else c else if b < c then b else c *]

Using "while" works much the same way, with the exception that <expression2> in executed repeatedly until <expression1> evaluates to false.

Flip coins until a heads comes up and print the probability of it taking that many flips. [* p <- 1/2 while int (rand 0 1) = 0 do p <- p / 2 print p *]

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.

Same as before, but congratulate the lucky. [* p <- 1/2 while int (rand 0 1) = 0 do p <- p / 2 else print "First flip! Congrats!" print p *]

Both "if" and "while" expressions evaluate to a value. Keeping that in mind, we can write an interesting variation of our coin flipping program.

[* d <- 2 print (1 / while int (rand 0 1) = 0 do d <- d * 2) *]

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 Tables

Prism 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.

[* primes <- 2, 3, 5, 7, 11, 13, 17, 19 thirtyfive <- primes[2] * primes[3] *]

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:

[* table <- "key1" => value1, "key2" => value2, "key3" => value3 secondvalue <- table["key2"] *]

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.

[* regulartable <- { "A", "B", "C" } (* or *) regulartable <- "A", "B", "C" (* here we need curly brackets to avoid ambiguity *) singulartable <- { "A" } emptytable <- {} *]

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.

[* v <- "x" => 5.5, "y" => -2.0, "z" => 0.0 w <- "x" => 6.5, "y" => 12.2, "z" => -1.0 (* calculate the dot product *) dot <- (v.x * w.x) + (v.y * w.y) + (v.z * w.z) *]

Here you can see how tables in Prism can take the place of structures in other languages.

Functions

Declarations 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.

A function to calculate square roots, using Newton's method. [* sqrt x: guess <- 1 while abs (quess^2 - x) > 0.001 do guess <- (x/guess + guess) / 2 guess *]

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.

A slightly more concise version. [* sqrt x: guess <- 1 while abs (quess^2 - x) > 0.001 do guess <- (x/guess + guess) / 2 *]

Calling functions is equally straightforward.

We can then call our new square root function as we see fit. [* a <- 2 b <- 4 (* functions have higher precedence than binary operators. * We need parenthesis to evaluate the expression properly. * Otherwise, sqrt will be called just on a. *) c <- sqrt ( a^2 + b^2 ) *]

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:

A parameter-less function. [* hello: print "Hello World!" *]

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.

Calling hello. [* hello () *]

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.

A function to make functions that add n to x. [* make_adder n: adder x: x + n *]

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.

[* (square x: x^2) 7 *]

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:

[* (_ x: x^2) 7 *]

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.

[* make_adder k: _ x: k + x make_adder_list n: L <- {} while n >= 0 do n <- n - 1 L[n] <- make_adder n L twenty_adders = make_adder_list 20 (* add 7 to 4 *) print (twenty_adders[7] 4) *]

Try to figure out what this example does on your own!

String Substitution

(TODO)

Magic Strings

Magic 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.

[* `Hello World!` *]

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.)

Emulating Perl style backticks with Prism style backticks. [* (* First, overload the bang operator. *) ! str scope: Sys.exec str (* Now we can use it to commit terrible deeds. *) `rm -rf *` *]

Magic strings have some very important uses that we are about to see.

Writing Linear Character Dialogue in Prism

Here 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.

[* (* Dramatis Personae *) HAMLET <- {} POLONIUS <- {} (* Act II, Scene 2 *) POLONIUS._: `Though this be madness, yet there is method in't. -- Will you walk out of the air, my lord?` HAMLET._: `Into my grave?` POLONIUS._: `Indeed, that's out of the air. (Aside) How pregnant sometime his replies are! A happiness that often madness hits on, which reason and sanity could not so prosperously be delivered of. I will leave him and suddenly contrive the means of meeting between him and my daughter. -- My honourable lord, I will most humbly take my leave of you.` HAMLET._: `You cannot, sir, take from me anything that I will not more willingly part withal -- except my life, except my life, except my life.` *]

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.

Overloading the bang operator. [* ! str scope: print scope.__name__ print "\n" print str () *]

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.

Call all the lines of dialogue. [* recite lines: Table.iterate (_ n: n ()) lines (* now recite all the lines *) recite __funcs__ *]

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 Prism

The 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:

A more sophisticated recite. [* recite lines: indexes <- Table.invert lines i <- 0 while not (lines[i] = ()) do r <- lines[i] () i <- if r <=> () then indexes[r] else i + 1 *]

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.

High Noon: A Spaghetti Code Western [* (* Dramatis Personae *) Villian <- {} Hero <- {} (* ACT I *) Villian._: `You! I saw you die with my own eyes. We left you, spitting up blood in the El Paso desert.` Hero._: `Next time, aim for the heart you lowdown, no-good, bastard.` Villian.A: `Call me that once more, and you'll be sorry!` Hero._: `I said: you're a no-good bastard!` Villian.A *]

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.

High Noon: A Spaghetti Code Western [* (* Dramatis Personae *) HERO <- {} GIRL <- {} (* ACT II *) HERO._: `Forget it lady, you want nothin to do with me.` GIRL._: `No. I can see it in your eyes. There's good in you, you're just afraid to let it out.` user_choose (HERO.good, HERO.bad, HERO.ugly) HERO.good: `Maybe you're right. Maybe theres been enough blood. I suppose sometimes it takes more guts to not kill.` HERO.bad: `Any good in me died when I gunned down Bob Jenkins in the street. I started the killing, and by God, I'll see it to its end.` HERO.ugly: `I didn't realize you were so sweet for me. I reckon I could kill all these mooks by sun down, and ride back to have my way with you...` *]

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.

Conclusion

The 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.

Hosted by:
SourceForge.net Logo