Someone asked me recently, what was the motivation behind LispE. Here is an example of the kind of functionalities that LispE proposes.
Adventures games were very popular at the beginning of the PC revolution back at the beginning of the 80s. Implementing a game is not very difficult in itself, but it is far from being a trivial task.
However, function programming concepts such as data types and pattern matching can prove incredibly handy to implement such a game.
For instance, you can transform each of your different actions into a Data Type:
(data Command
[Move atom_]
[Break atom_ atom_]
[Open atom_ atom_]
[Kill atom_ atom_]
[Pick atom_ atom_]
[Take atom_]
[Drop atom_]
)
(Note that [..] are equivalent to (..), we can freely choose one or the other for more code clarity)
A Data Type provides some very interesting way of controlling how your input is handled in your code. For instance, in each of these definitions, we define the number of arguments and their basic type. In this case, each should be an atom.
However, a data type should not be confused with a struct as in languages such as C++ or Java. It does not record any data structures in memory. Functional Programming tries to keep away from side-effect, while struct in imperative languages wouldn't care less.
If we take the following C struct:
struct {
int i;
char c;
} foo;
foo.i = 10;
foo.c = 'a';
Nothing prevents us from modifying the internal values of that struct, which of course might provoke some side-effect.
So what's the point?
These structures shine when used in collaboration with pattern functions.
(defpat action ( [Pick 'up x] ) ...)
(defpat action ( [Take x] ) ...)
(defpat action ( [Drop x] ... )
(defpat action ( [Move direction] ...)
The function action in this context is polymorphic and can be triggered with different data types.
(action [Move 'left])
(action [Pick 'up 'stone])
Each of these actions will then be matched against the corresponding function definitions above.
If you have a look on: minizork_en.lisp, you'll see that we take a string as input and then apply a few transformations:
(setq dialog "GET THE STONE")
; car and cdr have also been repurposed for strings
; GET THE STONE is transformed into: Get the stone
(setq dialog (+ (upper . car dialog) . lower . cdr dialog))
; We replace synonyms with their official value: Get --> Take
; We transform each of our strings into atoms.
; Note that select returns the first non null value
; We remove stopwords (replaced with "~")
; We eventually obtain: (Take stone)
(setq commands
(filterlist '(neq '~) ; we remove stopwords
(maplist ; we replace words with their synonyms or remove stopwords
(λ(x) (atom . select (key filterwords x) x))
(split dialog " "))))
Once this transformation has been completed, we can try our new command:
(maybe
(action command) ; (action [Take stone])
(println "Wrong command")
)
Basically, if there is an error, we catch it and execute the last line.
The use of pattern functions with data types simplify quite a lot the whole code.
Ive been working on an Elisp text adventure engine you may find interesting:
https://www.github.com/progfolio/spiel
Not written in as functional a style, but pattern matching (elisp's
pcase
macro) is at the heart of parsing the input.