#Script Lisp
#Script
is designed as a small, expressive and wrist-friendly dynamic scripting language that for maximum familiarity
is modelled after the world's most popular and ubiquitous scripting Language, JavaScript. Its minimal syntax was inspired
by other small but powerful languages which heavily utilizes functions instead of adopting a larger language grammar
defining different bespoke syntax for language constructs.
Small Languages like Smalltalk, despite being one of the most influential languages in history, is famous for its minimal syntax that fits on a post card. A language with arguably better power to size ratio is Lisp which the inventor of Smalltalk, Alan Kay has credited it as being the greatest single programming language ever designed after realizing:
“the half page of code on the bottom of page 13… was Lisp in itself. These were “Maxwell’s Equations of Software!”
Lisp's unprecedented elegance and simplicity spawned a myriad of dialects, some noteworthy implementations illustrating the beauty of its small size and expressive power is lispy by by Peter Norvig (Director of Google Research) that implements a Lisp interpreter in just 117 lines of Python 3 code (inc. a REPL).
Another compact dialect is Zick Standard Lisp which @zick has implemented in 42 different languages including a recursive Lisp evaluator in Lisp implemented in only 66 lines of code.
A more complete Lisp implementation in C# is the elegant Nukata Lisp
by SUZUKI Hisao which is a
Common Lisp-like Lisp-1 dialect with tail call optimization and partially hygienic macros, although
has some notable limitations including a small standard library,
only uses the double
numeric type and doesn't contain .NET Scripting support.
Script Lisp Overview
ScriptLisp is an
enhanced version of Nukata Lisp with a number of new features
that reuses #Script
existing scripting capabilities to provide seamless integration with both the rest of #Script
(see Language Blocks an Expressions)
and .NET including Scripting of .NET Types, support for all .NET numeric types and access to its comprehensive
library of over 1000+ Script Methods - optimally designed for accessing .NET functionality from a dynamic language.
To improve compatibility with existing Common Lisp source code it also implements most of the Simplified Common Lisp Reference as well as all missing functions required to implement C# LINQ 101 Examples in Lisp:
To improve readability and familiarity it also adopts a number of Clojure syntax for defining a
data list and map literals,
anonymous functions,
syntax in Java Interop for .NET Interop,
keyword syntax for indexing collections and accessing index accessors
and Clojure's popular shorter aliases for fn
, def
, defn
- improving source-code compatibility with Clojure.
Lisp REPL
In addition to being a 1st class language option in #Script
, Lisp's dynamism and extensibility makes it particularly
well suited for explanatory programming whose access via a REPL is now built into the latest
x and app dotnet tools
which can be quickly installed in any Windows, macOS or Linux OS (with .NET Core) with:
$ dotnet tool install -g x
Or if you have a previous version installed, update to the latest version with:
$ dotnet tool update -g x
Where you'll then be able to bring up an instant Lisp REPL with:
$ x lisp
The quick demo below shows the kind of exploratory programming available where you can query the scriptMethods
available,
query an objects props
, query the Lisp interpreter's global symbols
table containing all its global state including all
defined lisp functions, macros and variables:
Annotated REPL Walk through
Here's an annotated version of the demo below which explains what each of the different expressions is doing.
Just like Sharp Scripts and Sharp Apps the Lisp REPL runs within the #Script Pages ScriptContext sandbox that when run from a Sharp App folder, starts a .NET Core App Server that simulates a fully configured .NET Core App. In this case it's running in the redis Sharp App directory where it was able to access its static web assets as well as its redis-server connection configured in its app.settings.
; quick lisp test!
(+ 1 2 3)
; List of ScriptMethodInfo that the ScriptContext running this Lisp Interpreter has access to
scriptMethods
; first script method
(:0 scriptMethods)
; show public properties of ScriptMethodInfo
(props (:0 scriptMethods))
; show 1 property per line
(joinln (props (:0 scriptMethods)))
; show both Property Type and Name
(joinln (propTypes (:0 scriptMethods)))
; view the Names of all avaialble script methods
(joinln (map .Name scriptMethods))
; view all script methods starting with 'a'
(globln "a*" (map .Name scriptMethods))
; view all script methods starting with 'env'
(globln "env*" (map .Name scriptMethods))
; print environment info about this machine seperated by spaces
(printlns envOSVersion envMachineName envFrameworkDescription envLogicalDrives)
; expand logical drives
(printlns envOSVersion envMachineName envFrameworkDescription "- drives:" (join envLogicalDrives " "))
; view all current global symbols defined in this Lisp interpreter
symbols
; view all symbols starting with 'c'
(globln "c*" symbols)
; see how many symbols are defined in this interpreter
(count symbols)
; see how many script methods there are available
(count scriptMethods)
; view the method signature for all script methods starting with 'all'
(globln "all*" (map .Signature scriptMethods))
; count all files accessible from the configured ScriptContext
(count allFiles)
; view the public properties of the first IVirtualFile
(props (:0 allFiles))
; display the VirtualPath of all available files
(joinln (map .VirtualPath allFiles))
; display the method signature for all script methods starting with 'findFiles'
(globln "findFiles*" (map .Signature scriptMethods))
; see how many .html files are available to this App
(count (findFiles "*.html"))
; see how many .js files are available to this App
(count (findFiles "*.js"))
; show the VirtualPath of all .html files
(joinln (map .VirtualPath (findFiles "*.html")))
; view the VirtualPath's of the 1st and 2nd .html files
(:0 (map .VirtualPath (findFiles "*.html")))
(:1 (map .VirtualPath (findFiles "*.html")))
; view the text file contents of the 1st and 2nd .html files
(fileTextContents (:0 (map .VirtualPath (findFiles "*.html"))))
(fileTextContents (:1 (map .VirtualPath (findFiles "*.html"))))
; display the method signatures of all script methods starting with 'redis'
(globln "redis*" (map .Signature scriptMethods))
; search for all Redis Keys starting with 'urn:' in the redis-server instances this App is configured with
(redisSearchKeys "urn:*")
; display the first redis search entry
(:0 (redisSearchKeys "urn:*"))
; display the key names of all redis keys starting with 'urn:'
(joinln (map :id (redisSearchKeys "urn:*")))
; find out the redis-server data type of the 'urn:tags' key
(redisCall "TYPE urn:tags")
; view all tags in the 'urn:tags' sorted set
(redisCall "ZRANGE urn:tags 0 -1")
; view the string contents of the 'urn:question:1' key
(redisCall "GET urn:question:1")
; parse the json contents of question 1 and display its tag names
(:Tags (parseJson (redisCall "GET urn:question:1")))
; extract the 2nd tag of question 1
(:1 (:Tags (parseJson (redisCall "GET urn:question:1"))))
; clear the Console screen
clear
; exit the Lisp REPL
quit
Enable features and access resources with app.settings
You can configure the Lisp REPL with any of the resources and features that Sharp Apps and
Gist Desktop Apps have access to, by creating a plain text app.settings
file with all the
features and resources you want the Lisp REPL to have access to, e.g. this Pure Cloud App app.settings
allows the Lisp REPL to use Database Scripts against a AWS PostgreSQL RDS server and query remote
S3 Virtual Files using Virtual File System APIs:
# Note: values prefixed with '$' are resolved from Environment Variables
name AWS S3 PostgreSQL Web App
db postgres
db.connection $AWS_RDS_POSTGRES
files s3
files.config {AccessKey:$AWS_S3_ACCESS_KEY,SecretKey:$AWS_S3_SECRET_KEY,Region:us-east-1,Bucket:rockwind}
See the plugins app.settings for examples of how to load and configure ServiceStack Plugins.
Lisp REPL TCP Server
In addition to launching a Lisp REPL from the Console above, you can also open a Lisp REPL into any ServiceStack App
configured with the LispReplTcpServer
ServiceStack plugin. This effectively opens a "programmable gateway" into any
ServiceStack App where it's able to perform live queries, access IOC dependencies, invoke internal Server functions and query
the state of a running Server which like the Debug Inspector
can provide invaluable insight when diagnosing issues on a remote server.
To see it in action we'll enable it one of our production Apps techstacks.io which as it's a
Vuetify SPA App is only configured with an empty SharpPagesFeature
as it doesn't use any server-side scripting features.
We'll enable it in DebugMode
where we can enable by setting DebugMode
in our App's appsettings.Production.json
which will launch a TCP Socket Server which by default is configured to listen to the loopback IP on port 5005
.
if (Config.DebugMode)
{
Plugins.Add(new LispReplTcpServer {
ScriptMethods = {
new DbScripts()
},
ScriptNamespaces = {
nameof(TechStacks),
$"{nameof(TechStacks)}.{nameof(ServiceInterface)}",
$"{nameof(TechStacks)}.{nameof(ServiceModel)}",
},
});
}
ScriptNamespaces behaves like C#'s
using Namespace;
statement letting you reference Types byName
instead of its fully-qualified Namespace.
Whilst you can now connect to it with basic telnet
, it's a much nicer experience to use it with the rlwrap
readline wrap utility which provides an enhanced experience with line editing, persistent history and completion.
$ sudo apt-get install rlwrap
Then you can open a TCP Connection to connect to a new Lisp REPL with:
$ rlwrap telnet localhost 5005
Where you now have full scriptability of the running server as allowed by #Script Pages SharpPagesFeature
which
allows scripting of all .NET Types by default.
TechStacks TCP Lisp REPL Demo
In this demo we'll explore some of the possibilities of scripting the live techstacks.io Server where we can
resolve
IOC dependencies to send out tweets using its registered ITwitterUpdates
dependency, view the source and load a remote
parse-rss lisp function into the new Lisp interpreter attached to the TCP connection,
use it to parse Hacker News RSS Feed into a .NET Collection where it can be more easily queried using its built-in functions
which is used to construct an email body with HN's current Top 5 links.
It then uses DB Scripts to explore its configured AWS RDS PostgreSQL RDBMS, listing its DB tables and viewing its
column names and definitions before retrieving the Email addresses of all Admin users, sending them each an email with HN's Top 5 Links by
publishing 5x SendEmail
Request DTOs using the publishMessage ServiceStack Script to where
they're processed in the background by its configured MQ Server that uses it to execute the
SendEmail
ServiceStack Service where it uses its configured AWS SES SMTP Server to finally send out the Emails:
Password Protection
Since TCP Server effectively opens your remote Server up to being scripted you'll want to ensure the TCP Server is only accessible within a trusted network, effectively treating it the same as Redis Security Model.
A secure approach would be to leave the default of only binding to IPAddress.Loopback
so only trusted users with SSH access will
be able to access it, which they'll still be able to access remotely via Local PC > ssh > telnet 127.0.0.1 5005
.
Just like Redis AUTH you can also add password protection for an additional layer of Security:
Plugins.Add(new LispReplTcpServer {
RequireAuthSecret = true,
...
});
Which will only allow access to users with the configured AuthSecret:
SetConfig(new HostConfig {
AdminAuthSecret = "secretz"
});
Annotated Lisp TCP REPL Transcript
; resolve `ITwitterUpdates` IOC dependency and assign it to `twitter`
(def twitter (resolve "ITwitterUpdates"))
; view its concrete Type Name
(typeName twitter)
; view its method names
(joinln (methods twitter))
; view its method signatures
(joinln (methodTypes twitter))
; use it to send tweet from its @webstacks account
(.Tweet twitter "Who's using #Script Lisp? https://sharpscript.net/lisp")
; view all available scripts in #Script Lisp Library Index gist.github.com/3624b0373904cfb2fc7bb3c2cb9dc1a3
(gistindex)
; view the source code of the `parse-rss` library
(load-src "index:parse-rss")
; assign the XML contents of HN's RSS feed to `xml`
(def xml (urlContents "https://news.ycombinator.com/rss"))
; preview its first 1000 chars
(subString xml 0 1000)
; use `parse-rss` to parse the RSS feed into a .NET Collection and assign it to `rss`
(def rss (parse-rss xml))
; view the `title`, `description` and the first `item` in the RSS feed:
(:title rss)
(:description rss)
(:0 (:items rss))
; view the links of all RSS feed items
(joinln (map :link (:items rss)))
; view the links and titles of the top 5 news items
(joinln (map :link (take 5 (:items rss))))
(joinln (map :title (take 5 (:items rss))))
; construct a plain-text numbered list of the top 5 HN Links and assign it to `body`
(joinln (map-index #(str %2 (:title %1)) (take 5 (:items rss))))
(joinln (map-index #(str (padLeft (1+ %2) 2) ". " (:title %1)) (take 5 (:items rss))))
(def body (joinln
(map-index #(str (padLeft (1+ %2) 2) ". " (:title %1) "\n" (:link %1) "\n") (take 5 (:items rss)))))
; view all TechStacks PostgreSQL AWS RDS tables
(dbTableNames)
(joinln dbTableNames)
; view the column names and definitions of the `technology` table
(joinln (dbColumnNames "technology"))
(joinln (dbColumns "technology"))
; search for all `user` tables
(globln "*user*" (dbTableNames))
; view how many Admin Users with Emails there are
(dbScalar "select count(email) from custom_user_auth where roles like '%Admin%'")
; assign the Admin Users email to the `emails` list
(def emails (map :email (dbSelect "select email from custom_user_auth where roles like '%Admin%'")))
; search for all `operation` script methods
(globln "*operation*" scriptMethods)
; search for all `email` Request DTOs
(globln "*email*" metaAllOperationNames)
; view the properties available on the `SendEmail` Request DTO
(props (SendEmail.))
; search for all `publish` script methods that can publish messages
(globln "publish*" scriptMethods)
; create and publish 5x `SendEmail` Request DTOs for processing by TechStacks configured MQ Server
(doseq (to emails) (publishMessage "SendEmail" { :To to :Subject "Top 5 HN Links" :Body body }))
Run and watch Lisp Scripts
The same Sharp Scripts functionality for #Script
is also available to Lisp scripts where you can use the x
and app
dotnet tools to run and watch stand-alone Lisp scripts with the .l
file extension, e.g:
$ x run lisp.l
$ x watch lisp.l
To clarify the behavioural differences between the Lisp REPL's above which uses the same Lisp interpreter to maintain state changes across each command,
the watch
Script is run with a new Lisp Interpreter which starts with a fresh copy of the Global symbols table so any state changes after each
Ctrl+S
save point is discarded.
Watch lisp
scripts
This quick demo illustrates the same functionality in Sharp Scripts is also available in lisp
scripts
where it provides instant feedback whilst you develop in real-time:
Annotated Lisp watch script
;<!--
; db sqlite
; db.connection northwind.sqlite
; files s3
; files.config {AccessKey:$AWS_S3_ACCESS_KEY,SecretKey:$AWS_S3_SECRET_KEY,Region:us-east-1,Bucket:rockwind}
;-->
; delete remove.txt file
(sh (str (if isWin "del" "rm") " remove.txt"))
; View all `northwind.sqlite` RDBMS Tables
(textDump (dbTableNames) { :caption "Northwind" } )
; Display first `customer` row in Single Row View showing all Table Columns
(textDump (dbSelect "select * from customer limit 1"))
; Display all Customers in London
(def city "London")
(textDump (dbSelect "select Id, CompanyName, ContactName from customer where city = @city" { :city city } ))
; View all root files and folders in configured S3 Virtual File Provider
(joinln (map #(str (.Name %) "/") (allRootDirectories vfsContent)))
(joinln (map .Name (allRootFiles vfsContent)))
; Show first 10 *.png files in S3 VFS Provider
(def pattern (or (first ARGV) "*.png"))
(joinln (map .VirtualPath (take 10 (findFiles vfsContent pattern))))
Page Arguments
You can also use the same syntax for declaring any app.settings
page arguments used in #Script
and code
Scripts:
<!--
db sqlite
db.connection northwind.sqlite
-->
But for compatibility with any Lisp syntax highlighters and code editors they can also be prefixed with a ;
line comment as seen above.
Executing Lisp in .NET
Lisp like all #Script
languages are executed within a ScriptContext
that defines all functionality available to them, i.e:
var context = new ScriptContext {
Args = { ... }, // Global Arguments available to all Scripts, Pages, Partials, etc
ScriptMethods = { ... }, // Additional Methods
ScriptBlocks = { ... }, // Additional Script Blocks
FilterTransformers = { .. }, // Additional Stream Transformers
PageFormats = { ... }, // Additional Text Document Formats
Plugins = { ... }, // Encapsulated Features e.g. Markdown, Protected or ServiceStack Features
ScanTypes = { ... }, // Auto register Methods, Blocks and Code Page Types
ScanAssemblies = { ... }, // Auto register all Methods, Blocks and Code Page Types in Assembly
}.Init();
Where you can customize the pure sandboxed ScriptContext your Script is executed within by extending it with:
To render lisp
you'll first need to register the Lisp Language with the ScriptContext
you're using:
var context = new ScriptContext {
ScriptLanguages = { ScriptLisp.Language }
}.Init();
Then use RenderLisp
(i.e. instead of RenderScript
) to render Lisp code, e.g:
// render lisp
var output = context.RenderLisp("(dateFormat now \"HH:mm:ss\")");
// async
var output = await context.RenderLispAsync("(dateFormat now \"HH:mm:ss\")");
These APIs match the high-level APIs for rendering normal #Script
:
var output = context.RenderScript("{{ now |> dateFormat('HH:mm:ss') }}");
var output = await context.RenderScriptAsync("{{ now |> dateFormat('HH:mm:ss') }}");
Finer grained control
The high-level APIs above wraps the finer-grained functionality below which works by rendering a SharpPage
configured with the lisp
language in a PageResult
that all languages use:
var context = new ScriptContext {
ScriptLanguages = { ScriptLisp.Language }
}.Init();
var dynamicPage = context.LispSharpPage("(dateFormat now \"HH:mm:ss\")"); // render lisp
//var dynamicPage = context.SharpScriptPage("{{ now |> dateFormat('HH:mm:ss') }}"); // render #Script
var output = new PageResult(dynamicPage).RenderScript();
//async
var output = await new PageResult(dynamicPage).RenderScriptAsync();
If you need the return value instead you can access it from:
var result = new PageResult(dynamicPage).EvaluateResult(out var returnValue)
? ScriptLanguage.UnwrapValue(returnValue)
: null;
If your script source code doesn't change you can re-use dynamicPage
which lets you re-evaluate your source code's cached AST.
Evaluating Lisp Script Results
If you instead wanted to access return values instead of its rendered output, use the EvaluateLisp()
APIs:
var result = context.EvaluateLisp("(return (+ 1 1))"); //= 2
The generic overloads below utilizes ServiceStack's Auto Mapping utils to convert the return value into your preferred type, e.g:
double result = context.EvaluateLisp<double>("(return (+ 1 1))"); //= 2.0
string result = context.EvaluateLisp<string>("(return (+ 1 1))"); //= "2"
Which can also be used for more powerful conversions like converting an Object Dictionary into your preferred POCO:
var result = context.EvaluateLisp<Customer>(
"(return (dbSingle \"select * from customer where id=@id\" { :id id }))",
new ObjectDictionary {
["id"] = 1
});
.NET Interop
The syntax for .NET Interop is inspired directly from Clojure's syntax used for Java Interop. See Scripting .NET Type Resolution for how to configure Types and imported Namespaces you want your Lisp scripts to have access to.
Member Access
The '.'
prefix if for accessing an instance members which can be used for retrieving a properties public properties, fields and
invoking instance methods, e.g:
- (.Property instance)
- (.Field instance)
- (.Method instance ...args)
Indexer Access
Use ':'
prefix for accessing a Types indexer or for indexing collections, e.g:
- (:key indexer)
- (:"string key" dictionary)
- (:n list)
- (:n array)
- (:n enumerable)
- (:n indexer)
It can also be used to access an instance public Properties and Fields:
- (:Property instance)
- (:Field instance)
However for readability we recommend using '.'
prefix above to convey instance member access.
Constructor Access
Use '.'
suffix for creating instances of Types:
- (Type. ...args)
- (Namespace.Type. ...args)
You can also create instances using the new script method, which as it accepts a
string
Type Name can be used to create generic classes with multiple generic args, e.g:
- (new "Type" ...args)
- (new "Type<T1,T2,T3>" ...args)
Static Member Access
Use the '/'
separator to access a Type's static members or to invoke its static methods, e.g:
- (StaticType/Property)
- (StaticType/Field)
- (StaticType/Const)
- (StaticType/Method ...args)
Use '.'
dot notation for specifying the fully-qualified Type name or to reference its Inner classes, e.g:
- (Namespace.StaticType/Member)
- (Namespace.StaticType.InnerType/Member)
Script Methods
Use '/'
prefix to reference any Script Method registered in your ScriptContext
:
- (/scriptMethod ...args)
Script Methods without arguments can be referenced as an argument binding that when referenced as an argument (i.e. without brackets) are implicitly evaluated, in-effect making them a calculated property:
- /methodAsBinding
While a '/'
prefix indicates a reference to a script method, for readability it can be excluded as when
there's no existing symbol defined in the Lisp interpreter's symbol table it will fallback to referencing a script method:
- (scriptMethod ...args)
- methodAsBinding
This does mean that when there exists a symbol
of the same name defined you will need to use the '/'
prefix to reference a script method.
Generic Types
All references above support referencing generic types and methods with a single generic Type argument, e.g:
- (StaticType/Method<T>)
- (GenericStaticType<T>/Member)
- (GenericStaticType<T>/Method<T>)
- (GenericType<T>.)
As ','
is one of Lisp's few syntax tokens (unquoting) it prevents them
from being able to use them to specify multiple generic arguments.
Instead you'll need to use the Constructor function for referencing constructors with multiple generic arguments where you'll also need to specify the types of the exact constructor you want to call, e.g:
- (/C "Tuple<String,int>(String,int)")
The difference between the /C
script method constructor function and Lisp's C
function is that the script method only returns a reference
to the constructor which you'll need to invoke with arguments to create an instance:
- ((/C "Tuple<String,int>(String,int)") "A" 1)
Whilst Lisp's C
function will auto-invoke the constructor function with the supplied arguments in a single expression:
- (C "Tuple<String,int>(String,int)" "A" 1)
Likewise when needing to invoke generic methods with multiple generic args you'll need to use Function:
- ((/F "Tuple.Create<String,int>(String,int)") "A" 1)
Or Script Lisp's F
function for invoking a function reference in a single expression:
- (F "Tuple.Create<String,int>(String,int)" "A" 1)
For more examples and information see Scripting .NET Types.
Property Setters
You can populate multiple properties on an instance using the set script method, e.g:
- (set instance { :Prop arg ... })
Alternatively properties can be set individually with:
- (.Prop instance arg)
Lisp Lists vs .NET Collections
A potential source of friction when interoperating with .NET is that Lisp Lists are Cons Cells so that a code or data list in Lisp, i.e:
'(1 2 3)
[1 2 3]
Is implemented as a Linked List of Cons cells:
(1 . (2 . (3 . null)))
Which is what Lisp's core functions expect to operate on, namely:
car cdr caar cadr cdar cddr caaar caadr cadar caddr cdaar cdadr cddar cdddr append mapcar consp cons?
listp list? memq member assq assoc nreverse last nconc dolist dotimes mapcan mapc nthcdr nbutlast
These core Lisp functions can't be used against .NET collections directly, instead you can use
(to-cons collection)
to convert a .NET IEnumerable
collection into a cons list, e.g:
(cdr (to-cons netEnumerable))
Should you need to do the inverse you can use (to-list cons-list)
to convert a cons list to a .NET List, e.g:
(to-list (range 10))
We've made Script Lisp's cons Cell
an IEnumerable
so that all other built-in Lisp functions can operate on both
cons cells and .NET Collections where instead of iterating a list with (do-list)
you can use (do-seq)
to iterate
both .NET Collections and cons cells, e.g:
(do-seq (x collection) (println x) )
Annotated .NET Interop Example
To see what this looks like in action here's an annotated simple real-world example that heavily utilizes .NET interop:
; define function and assign to `parse-rss` value in Lisp interpreters symbols table
(defn parse-rss [xml]
; define local variables used within this scope
(let ( (to) (doc) (channel) (items) (el) )
; use XDocument.Parse() to parse xml string argument containing xml and assign to `doc`
(def doc (System.Xml.Linq.XDocument/Parse xml))
; create empty ObjectDictionary (wrapper for Dictionary<string,object>) and assign to `to`
(def to (ObjectDictionary.))
; create empty List of ObjectDictionary and assign to `items`
(def items (List<ObjectDictionary>.))
; descend into first <channel> XML element and assign to `channel`
(def channel (first (.Descendants doc "channel")))
; use `XLinqExtensions.FirstElement()` extension method to assign channels first XML element to `el`
(def el (XLinqExtensions/FirstElement channel))
; iterate through all elements up to the first <item> and add them as top-level entries in `to`
(while (not= (.LocalName (.Name el)) "item")
; add current XML element name and value entry to `to`
(.Add to (.LocalName (.Name el)) (.Value el))
; move to next element using `XLinqExtensions.NextElement()` extension method
(def el (XLinqExtensions/NextElement el)))
; add all rss <item>'s to `items` list
; iterate through all `channel` child <item> XML elements
(doseq (elItem (.Descendants channel "item"))
; create empty ObjectDictionary and assign to `item`
(def item (ObjectDictionary.))
; use `XLinqExtensions.FirstElement()` to assign <item> first XML element to `el`
(def el (XLinqExtensions/FirstElement elItem))
(while el
; add current XML element name and value entry to `item`
(.Add item (.LocalName (.Name el)) (.Value el))
; move to next element using `XLinqExtensions.NextElement()` extension method
(def el (XLinqExtensions/NextElement el)))
; add `item` ObjectDictionary to `items` List
(.Add items item))
; add `items` ObjectDictionary List to `to` at key `items`
(.Add to "items" items)
; return `to` ObjectDictionary
to
)
)
For comparison, this would be the equivalent implementation in C#:
public static ObjectDictionary ParseRss(string xml)
{
var to = new ObjectDictionary();
var items = new List<ObjectDictionary>();
var doc = XDocument.Parse(xml);
var channel = doc.Descendants("channel").First();
var el = channel.FirstElement();
while (el.Name != "item")
{
to[el.Name.LocalName] = el.Value;
el = el.NextElement();
}
var elItems = channel.Descendants("item");
foreach (var elItem in elItems)
{
var item = new ObjectDictionary();
el = elItem.FirstElement();
while (el != null)
{
item[el.Name.LocalName] = el.Value;
el = el.NextElement();
}
items.Add(item);
}
to["items"] = items;
return to;
}
Importing Global Scripts
Importing scripts in Lisp is essentially a 2-stage process of parsing Lisp source code into an SExpression, (basically Lisp's AST of tokenized elements captured in a 2-field Cons Cell) then evaluating it in a Lisp interpreter where any defined symbols are captured in its Symbols table.
Lisp Script captures its "standard library" in a Global Interpreter which serves as the starting template for all other Lisp Interpreters
which starts with a copy of the Global symbols table which you can further populate with your own common functions using Lisp.Import()
, e.g:
Lisp.Import(@"
(defun fib (n)
(if (< n 2)
1
(+ (fib (- n 1))
(fib (- n 2)) )))");
Loading Scripts
Loading scripts within a Lisp script works similarly except they're only loaded into that Lisp interpreters symbol table, a new one
of which is created in each new PageResult
.
Scripts loaded locally are loaded from the ScriptContext
configured Virtual Files Provider
which for #Script Pages SharpPagesFeature
is configured to use the App's cascading virtual file sources.
A new ScriptContext
starts with an empty MemoryVirtualFiles
which you can write files to with:
var context = new ScriptContext {
ScriptLanguages = { ScriptLisp.Language },
ScriptMethods = { new ProtectedScripts() },
};
context.VirtualFiles.WriteFile("lib1.l", "(defn lib-calc [a b] (+ a b))");
context.VirtualFiles.WriteFile("/dir/lib2.l", "(defn lib-calc [a b] (* a b))");
context.Init();
You can load these scripts by symbol name where it assumes a .l
extension, by quoting the argument so
Lisp doesn't try to evaluate it as an argument, e.g:
(load 'lib1)
(lib-calc 4 5) ;= 9
Alternatively you can specify the virtual path to the script. You can load multiple scripts with the same definitions, in Lisp this just updates the value assigned to the symbol name with the latest definition, e.g:
(load "lib1.l")
(lib-calc 4 5) ;= 9
(load "/dir/lib2.l")
(lib-calc 4 5) ;= 20
Import Scripts from URLs
Inspired by Deno you can also import remote scripts from URLs, e.g:
(load "https://example.org/lib.l")
Locally Cached
Like Deno all remote resources are cached after first use so after it's loaded once it only loads the locally cached
version of the script (where it will still work in an airplane without an internet connection). This cache is maintained
under a .lisp
folder at your configured Virtual Files provider (that can be deleted to clear any caches).
For Sharp Scripts or Apps using the web
or app
dotnet tools it's stored in its own cache folder that can be cleared with:
$ x --clean
Import Scripts from Gists
There's also first-class support for gists which you can reference with gist:<gist-id>
, e.g:
(load "gist:2f14d629ba1852ee55865607f1fa2c3e")
This will load all gist files in gist order, if you only to load a single file from a gist you can specify it with:
(load "gist:2f14d629ba1852ee55865607f1fa2c3e/lib1.l")
Script Lisp Library Index
To provide human readable names to remote Lisp Scripts and a discoverable catalog where anyone can share their own scripts, you reference gists by name listed in the #Script Lisp Library Index which is itself a self-documenting machine and human readable gist of named links to external gists maintained by their respective authors.
Index library references can be loaded using the format index:<name>
, e.g:
(load "index:lib-calc")
Which also support being able to reference individual gist files:
(load "index:lib-calc/lib1.l")
If you'd like to share your own Lisp Scripts with everyone and publish your library to the index, just add a link to your gist with your preferred name in the Gist Index Comments.
Viewing Script Source Code
You can view the source code of any load script references with load-src
, e.g:
(load-src 'lib)
(load-src "/dir/lib2.l")
(load-src "https://example.org/lib.l")
(load-src "gist:2f14d629ba1852ee55865607f1fa2c3e/lib1.l")
(load-src "index:lib-calc")
Disable Remote Imports
Should you wish, you can prevent anyone from loading remote scripts with:
Lisp.AllowLoadingRemoteScripts = false;
#Script Pages Integration
Whilst Lisp is a small, powerfully expressive functional dynamic language it's not great for use as a templating language.
Whilst there have been several attempts to create a HTML DSL in Lisp, nothing is better
than having no syntax which is the default Template mode for #Script
where it will emit everything that's not in a
Template or Language Expression as verbatim text.
A nice USP of Script Lisp is that you're never forced into going "full Lisp", you can utilize #Script
template expressions and
Script Blocks handlebars syntax that provides the ideal DSL for usage in a Template Language for generating HTML
and utilize your preferred Lisp
or Code Script Languages for any computational logic you want included in your page
using Language Blocks and Expressions.
Implementation
Despite being implemented in different languages a #Script
page containing multiple languages, e.g:
Still only produces a single page AST, where when first loaded #Script
parses the page contents as a contiguous
ReadOnlyMemory<char>
where page slices of any Language Blocks and Expressions
on the page are delegated to the ScriptContext
registered ScriptLanguages
for parsing which returns a fragment which is
added to the pages AST:
When executing the page, each language is responsible for rendering its own fragments which all write directly to the pages OutputStream
to generate the pages output.
The multi-languages support in #Script
is designed to be extensible where everything about the language is encapsulated within its
ScriptLanguage
implementation so that if you omit its registration:
var context = new ScriptContext {
// ScriptLanguages = { ScriptLisp.Language }
}.Init();
Any language expressions and language blocks referencing it become inert and its source code emitted as plain-text.
Lisp Argument Scopes
One differentiator between Lisp and Code languages is that code
utilizes the containing page current scope
for all its argument references where as Lisp stores all its definitions within the Lisp interpreter symbol table
attached to the PageResult
, so whilst Lisp scripts can access arguments within the pages scope, in order for the
outer page to access any Lisp symbols they need to be exported, e.g:
Exporting Lisp Functions
Lisp functions can also be exported for usage in the rest of the page by calling (to-delegate lispFn)
to convert it
into a .NET Delegate, e.g:
Although an easier way to define functions in Lisp is to use the defn Script Block which wraps this in a convenient syntax:
Controlling Lisp output
One of Lisp's famous traits is everything is an expression which is typically desired within a language, but may not
be what you want when generating output. E.g traditionally Lisp uses setq
to set a variable which also returns its value that #Script
will emit as it automatically emits all statement return values.
You could use def
which is an alias for setq
which returns null
, other options include wrapping all statements within an empty let
expression where only the last expression is returned, or you could use a Language Block Modifier
to ignore the entire lisp
block output and only export the result you want to be able to control precisely where to emit it:
can use either 'q', 'quiet' or 'mute' block modifier to ignore output
Another way to generate output from Lisp is to use its built-in print functions below:
- (print ...args) - write all arguments to the
OutputStream
- (println ...args) - write all arguments to the
OutputStream
followed by a new line - (printlns ...args) - write all arguments to the
OutputStream
with a' '
space delimiter followed by a new line - (pr ...args) - same as
print
but HTML encode all arguments - (prn ...args) - same as
println
but HTML encode all arguments
dorun
dorun can be used for executing a "statement block" (sequence of expressions)
with side-effects that you want to discard the return type (akin to void
return type):
(dorun println (map fizzbuzz (range 1 100)))
Or you can use do
to return the last expression in the statement block:
(do (+ 1 1) (+ 2 2)) ; 4
(do (+ 1 1) (+ 2 2) nil)) ; null
(do ()) ; null
Learn #Script Lisp
A great resource for learning Script Lisp is seeing it in action by seeing how to implement C#'s 101 LINQ Examples in Lisp:
Explore APIs in real-time
We can also take advantage of Lisp's dynamism and interactivity to explore APIs in real-time, a great way to do this is via
a watched Lisp script on the side where it provides instant feedback after each Ctrl+S
save point
or a active Lisp REPL.
- symbols - List all symbols in Lisp interpreter - most symbols are named after standard Lisp or clojure functions
- (symbol-type symbol) - Display the Symbol's Value Type
- scriptMethods - List all available Script Method Names registered in
ScriptContext
- scriptMethodTypes - List all available Script Method Type information
- (joinln collection) - Display the string output of each item in the collection on a separate line
- (globln pattern collection) - Only display lines matching the glob pattern
- (typeName instance) - View the instance Type Name
- (props instance) - Display the Property names of an Instance public properties
- (fields instance) - Display the Field names of an Instance public fields
- (methods instance) - Display the Method names of an Instance public methods
- (propTypes instance) - Get the PropertyInfo[] of an Instance public properties
- (fieldTypes instance) - Get the FieldInfo[] of an Instance public fields
- (methodTypes instance) - Get the Script Method Infos of an Instance public methods
- (staticProps instance) - Display the Property names of an Instance public static properties
- (staticFields instance) - Display the Field names of an Instance public static fields
- (staticMethods instance) - Display the Method names of an Instance public static methods
- (staticPropTypes instance) - Get the PropertyInfo[] of an Instance public static properties
- (staticFieldTypes instance) - Get the FieldInfo[] of an Instance public static fields
- (staticMethodTypes instance) - Get the Script Method Infos of an Instance public static methods
You can view the Scripts API Reference and Scripts Documentation on this website to interactively explore the available APIs, we'll work on providing further interactive documentation for the built-in Lisp functions, in the mean-time the best resource are their implementation.
For reference, here's are a quick list of all built-in Lisp symbols:
- % * *gensym-counter* *version* / /= _append _nreverse + < <= = > >= 1- 1+ 1st 2nd 3rd abs all? and any?
append apply assoc assoc-key assoc-value assq atom atom? average butlast C caaar caadr caar cadar caddr
cadr car cdaar cdadr cdar cddar cdddr cddr cdr ceiling cons cons? consp cos count debug dec decf def
defmacro defn defun dispose do dolist dorun doseq doseq-while dotimes dump dump-inline elt empty? end?
endp enumerator enumeratorCurrent enumeratorNext eq eql equal error even? every every? exit exp expt F f++
false filter filter-index first flatmap flatten floor gensym glob globln group-by htmldump identity if
inc incf instance? intern isqrt it last length let letrec list list? listp load load-src logand logior
logxor lower-case make-symbol map mapc mapcan mapcar map-index map-where max member memq min mod nbutlast
nconc new new-map next not not= nreverse nth nthcdr null number? odd? or order-by pop pr prin1 princ
print println printlns prn prs push push-end random range reduce remove remove-if rest return reverse
round rplaca rplacd second seq? setcar setcdr set-difference sets sin skip skip-while some some? sort
sort-by sqrt str string string? string-downcase string-upcase subseq sum symbol-name symbol-type t take
take-while tan terpri textdump third to-array to-cons to-delegate to-dictionary to-list true truncate
union unless upper-case when where where-index while zero? zerop zip zip-where
Common Lisp by convention uses a *p
suffix for predicate functions but we prefer Clojure's (and Ruby's)
more readable *?
suffix convention, for source-code compatibility we include both for
core Lisp predicts and just *?
for others.