-
Notifications
You must be signed in to change notification settings - Fork 81
Language
As of v8.0.0, the curly braces can be used to create (anonymous) hashmaps, as in JSON.
New syntax uses curly braces:
> (def h {a:3 b:5 })
{a:3 b:5}
>
The old style syntax for anonymous hash maps still works; the two are equivalent:
> (def h (hash a:3 b:5))
{a:3 b:5}
>
The json2 function is new too. It prints hashes as JSON objects (commas inserted). The original json function returns raw bytes instead of string, and includes some meta-data that is not in the original JSON source.
> (def h (hash a:3 b:["pretty" "nice"]))
{a:3 b:["pretty" "nice"]}
zygo> (println (json2 h))
{"a":3, "b":["pretty", "nice"]}
With these changes, one can now (source "tests/sample.json") and have the JSON file read into zygo hash maps. Note that the default keys in zygo are symbols. The default keys in JSON are strings, and we do not (at the moment) convert them to symbols -- principally so that printing them out again via (json2) will keep the keys as strings, and thus remain parsable JSON. Hence access to members must use the h["element"] syntax instead of h.element:
> j=(source "tests/sample.json")
{"glossary":{"title":"example glossary" "GlossDiv":{"title":"S" "GlossList":{"GlossEntry":{"ID":"SGML" "SortAs":"SGML" "GlossTerm":"Standard Generalized Markup Language" "Acronym":"SGML" "Abbrev":"ISO 8879:1986" "GlossDef":{"para":"A meta-markup language, used to create markup languages such as DocBook." "GlossSeeAlso":["GML" "XML"]} "GlossSee":"markup"}}}}}
zyg> j.glossary // querying for the symbol %glossary won't find it.
error in __main:3: hash has no field 'glossary' [err 1]
zygo> j["glossary"] // the string is the key in traditional JSON.
{"title":"example glossary" "GlossDiv":{"title":"S" "GlossList":{"GlossEntry":{"ID":"SGML" "SortAs":"SGML" "GlossTerm":"Standard Generalized Markup Language" "Acronym":"SGML" "Abbrev":"ISO 8879:1986" "GlossDef":{"para":"A meta-markup language, used to create markup languages such as DocBook." "GlossSeeAlso":["GML" "XML"]} "GlossSee":"markup"}}}}
zygo> j["glossary"]["title"]
"example glossary"
zygo> (pretty true)
zygo> j
{
"glossary":{
"title":"example glossary"
"GlossDiv":{
"title":"S"
"GlossList":{
"GlossEntry":{
"ID":"SGML"
"SortAs":"SGML"
"GlossTerm":"Standard Generalized Markup Language"
"Acronym":"SGML"
"Abbrev":"ISO 8879:1986"
"GlossDef":{
"para":"A meta-markup language, used to create markup languages such as DocBook."
"GlossSeeAlso":[
"GML"
"XML"
]
}
"GlossSee":"markup"
}
}
}
}
}
zygo>
Zygo was already a great configuration language, as it allows comments and supports NaN, +Inf, -Inf in numbers. With these changes to be visually similar to JSON, zygo syntax provides a great (and familiar looking) configuration file experience.
zygomys has seven types of Atoms: ints (which are int64 in Go), floats
(which are float64), strings, chars (runes), bools,
and symbols. There is also an atom for raw bytes. Raw bytes corresponds
to a []byte
in Go, and is constructed with (raw content
). Raw bytes
are mostly used to manipulate packets or byte buffers received from
external parties.
The following are different kinds of literal syntax for these atoms.
3 ; an int
-21 ; a negative int
nil ; the nil value; the empty list () is also represented by nil.
0x41 ; int in hexadecimal
0o755 ; int in octal
0b1110 ; int in binary
4.1 ; a float
-2.3 ; a negative float
1.3e20 ; a float in scientific notation
'c' ; the character 'c'
'\n' ; the newline character
"asdfsd" ; a string
asdfsd ; a symbol
true ; the "true" boolean
false ; the "false" boolean
`a raw string literal` ; like Go raw strings
hello: ; the same symbol as %hello. The colon quotes and ends the symbol.
Comments are Go style, with /* block comments */
and //
for comment-until-end-of-line. The syntax for symbols is the same as that for Go identifiers. Symbols name program entities such as variables and types. Symbols are a sequence of one or more letters and digits. The first character in an identifier must be a letter.
To avoid conflicts between user functions and future features, symbols that begin with an _
underscore are reserved for system functions and extensions.
Assignment is done using either the (def) or the (set) operator.
(def x 10)
will always define a new variable x
in the current scope. If there is already an x
in the current scope, it will be updated. No scope up the stack will ever be effected. def
should be your choice for most operations. Notice that a for
loop does create a new scope, hence (def)
expressions inside a for
loop cannot modify variables established before the loop, and a set
is needed.
While def
is the workhorse, sometimes set
is needed. set
is more powerful and thus more dangerous; it can modify non-local variables. Like def expression (set x 10)
will update the value of x
with 10 if x
is already defined in the current scope, and define a new binding if there is no x
anywhere on the stack. However, if x
is not found in the current scope, we will search up the scope stack for an earlier binding to x
. If x
is indeed found up the stack, the value of x
in that higher scope will be updated to 10. If no binding is found, a local one is created. The non-local update of 'set' is essential is some cases, but should be used with care.
Using the infix notation within curly-braces, set may be expressed as in Go:
{a = 10}
(mdef a b c (list 1 2 3))
will assign value 1 to symbol a
, value 2 to symbol b
, and value 3 to symbol c
. The last element of an mdef must be a list. If the list has fewer elements than the target symbols that are provided, it is an error. However it is perfectly acceptible to assign to fewer symbols than the list holds. mdef
is often used in conjunction with hpair
to fetch a key and its corresponding value. For example,
(mdef key val (hpair myhash 0))
will fetch the first (because it was given the 0
argument) key and value from myhash
.
To support Go-like multiple assignment, the =
assignment operator now supports multiple values on either side of the =
sign:
(a, b, c = true, false, true)
For loops in zygomys are patterned after loops in C. There are three parts to the loop expression: the initializer, the predicate, and the advance. These three parts are written in an array before the body expressions. Optionally a label can be attached to the for loop (the label is a quoted symbol, typically quoted using a trailing colon). The unlabeled for loop looks like:
(for [(initializer) (test) (advance)]
body)
In contrast, the labeled for loop looks like the following, where outerLoop:
is the label.
(for outerLoop: [(initializer) (test) (advance)]
body)
For example, here is counting to 0..9, and summing as we go:
(def sum 0)
(for [(def i 0) (< i 10) (def i (+ 1 i))]
(println i)
(set sum (+ sum i)))
Notice how set
use is required here to update the sum
variable which is before the loop, as for-loops create a new nested-scope for the pre-amble and body. The new scope avoids clobbering a parent loop varaible by mistake -- the child loop does not have to try to guess what the parent called its variables. This is especially valuable when sourcing unknown other code from within a loop.
Here is an example using a for loop to go though a hash. While it is easier to use the range
loop (below) for this purpose, the example illustrates just how much code the range
macro saves you.
> (def h (hash a:3 b:5))
(hash a 3 b 5)
> (def k (keys h))
[a b]
> (for [(def i 0) (< i (len k)) (def i (+ 1 i))]
(printf "my hash value %v for key %v\n"
(hget h (aget k i))
(str (aget k i))))
my hash value 3 for key a
my hash value 5 for key b
2
>
Last but not least, here is an example of a nested for loop using a labeled break and a labeled continue. We
make use of the ++
macro which is easy to read, and simply increments its argument by 1 using set
.
(def isum 0)
(def jsum 0)
(for outerLoop: [(def i 1) (< i 5) (++ i)]
(set isum (+ isum i))
(for innerLoop: [(def j 1) (< j 5) (++ j)]
(set jsum (+ jsum j))
(cond (> j 2) (continue outerLoop:)
(and (> i 2) (> j 3)) (break outerLoop:)
null)
(set jsum (+ jsum 1000))
)
)
(printf "isum is %d\n" isum)
(printf "jsum is %d\n" jsum)
(assert (== isum 10))
(assert (== jsum 8024))
The range
macro acts like a for-range loop in Go, assigning successive
keys and values to the variables you name. In the example below, k
gets
each key in turn, and v
gets each value from hash h
. You can use
range
to iterate through arrays too.
> (def h (hash a:44 b:55 c:77 d:99))
> (def s "")
> (range k v h (set s (concat s " " (str k) "-maps->" (str v))))
> s
" a-maps->44 b-maps->55 c-maps->77 d-maps->99"
>
> //
> (def sum 0) (range i value [10 50 40] (+= sum value)); range over array demo
> sum
100
> // i took on 0, 1, 2 in the above, although it wasn't used in this example.
Lists are just cons-cell lists like in other LISP dialects and are delimited by parentheses
(aFunction arg1 arg2
(anotherFunction arg1 arg2))
You can also just describe plain pairs (cons-cells in which the tail is not necessarily a list) using the \ character. The backslash \ replaces the dot of traditional lisp syntax, because dot is re-purposed for dot-symbol/element selection.
(a \ b)
Arrays correspond to Go slices and are delimited by square braces.
[1 2 3 4]
zygomys has mutable hashmaps which use Go maps internally. The literal syntax uses the (hash ) form.
(hash %a 3
%b 2)
Using the :
alternative quoting mechanism, the same map can be written more readably as
(hash a:3 b:2)
The colon does not become a part of the symbols a
and b
in the above example. It simply acts like a quote %
operation, but on the right-hand-side of the symbol.
The hash above maps a
to 3 and b
to 2. Hash keys can only be integers, strings, chars, or symbols.
The quote symbol %
indicates that the following expression should be
interpreted literally. This is useful for declaring symbols and lists.
Note that this is deliberately different from the tradition in Lisp of
using the single quote '
, since we prefer to be compatible with Go's
syntax, which uses the single quote for rune literals.
%(1 2 3 4)
%aSymbol
An anonymous function can be declared in zygomys like so
(fn [a b] (+ a b))
A function can be declared with a name using defn
.
(defn add3 [a] (+ a 3))
Note that like in Clojure, the argument list is given in an array instead of a list.
A binding can be added in the current scope using def
. You can also create
a new scope and declare bindings in it using let
or letseq
(letseq
replaces let*
).
(def a 3)
(let [a 3
b 4]
(* a b))
; returns 12
(letseq [a 2
b (+ a 1)]
(+ a b))
; returns 5
The difference between let
and letseq
is that let
creates bindings all at
once, so you will not be able to access earlier bindings in later bindings.
The letseq
form creates bindings one by one, sequentially, so each binding can access
bindings declared before it.
Functions can be called in the regular way.
(defn add3 [a] (+ a 3))
(add3 2) ; returns 5
They can also be called indirectly using apply.
(apply + [1 2 3]) ; returns 6
(apply + %(1 2 3)) ; same as above
This works exactly the same with anonymous functions
((fn [a b] a) 2 3) ; returns 2
(apply (fn [a b] a) [2 3]) ; same as above
zygomys by default uses the universal conditional statement, cond
.
(If using the infix syntax, 'if' and 'else' are also now implemented.)
The syntax of cond is as follows.
(cond
firstCondition firstExpression
secondCondition secondExpression
...
defaultExpression)
The cond
statement will check the conditions in order. If the condition is
true, it will return the result of the corresponding expression. If not, it
will move on the next condition. If none of the conditions are true, it will
return the result of the default expression. The default expression is the
only required portion of this statement. The way to think of this is that the
first condition/expression pair is an if
statement, the second is an
else if
statement, and the default is the else
statement.
zygomys also provides the short-circuit boolean operators and
and or
.
The and
expression will return the first "falsy" sub-expression or, if all
sub-expressions are "truthy", the last sub-expression is returned. The or
expression is the opposite, returning the first "truthy" expression or the
last expression.
The boolean false
, the null value (empty list), the integer 0, and the null
character are considered "falsy". All other values are considered "truthy".
The begin
statement is used to sequence expressions. It will run all
sub-expressions and return the result of the final expression. The top-level,
function bodies, and let-statement bodies have implicit begin
statements.
The following builtin functions are provided by the language runtime.
-
source
run code from named path
-
for
loop; as in C -
range
loops over hashes; as in Go.
-
defmac
defines a macro. Macros use function syntax, with arguments in [] an array. - vargs after
&
support, for example therange
macro is defined as:
(defmac range [key value myHash & body]
^(let [n (len ~myHash)]
(for [(def i 0) (< i n) (def i (+ i 1))]
(begin
(mdef (quote ~key) (quote ~value) (hpair ~myHash i))
~@body))))
-
^
is the syntax-quote shortcut (rather than backtick). -
~
is unquote. Use(quote ~sym)
to capture the actual symbol name used in the argument (as above in therange
example). -
~@
is splicing-unquote. See the body vararg in therange
example. -
macexpand
given an expression, returns the quoted expansion without running it. Useful for debugging macros.
-
sll
(shift-left logical) -
sra
(shift-right arithmetic) -
srl
(shift-right logical)
bitAnd
bitOr
bitXor
-
bitNot
(one's complement)
-
and
(short circuits) -
or
(short circuits) not
+
-
*
/
-
mod
(modulo) -
**
exponentiation -
+=
-=
update with set -
++
--
update with set
<
>
<=
>=
-
==
(equality check) !=
-
type?
(returns the type as a string) -
zero?
(returns true only for 0, 0.0, and null character) -
null?
(returns true only for ()) -
empty?
(returns true only for (), [], and {}) list?
array?
-
number?
(int, char, or float) int?
float?
char?
string?
symbol?
hash?
println
print
printf
-
str
stringifies its argument -
json
/unjson
for JSON encoding/decoding of records -
json2
for simpler string JSON encoding of anonymous hashes; includes no meta-data like (json) provides.
str2sym
sym2str
gensym
-
json
/unjson
-
msgpack
/unmsgpack
- a native Go struct can be associated with each record
-
togo
to reify a Go struct that matches the record
-
slurpf
read newline delimited file into an array of strings -
(writef val "path")
function write val to path -
owritef
same aswritef
but will clobber any existing path -
system
shell out and return the combined output as a string -
flatten
turn nested lists of strings into one flat array -
defined?
checks if the symbol is in scope in the current environment
The array
function can be used to construct an array. It is identical to
the square brace literal syntax.
The makeArray
function creates an array of a given length. By default, the
items in the array are intialized to null.
(makeArray 3) ; => [() () ()]
(makeArray 3 0) ; => [0 0 0]
The aget
function indexes into an array.
(aget [0 1 2] 1) ; returns 1
aget
accepts a 3rd default value in case the access is out of bounds
(assert (== %outOfBoundsValue (aget [0 1 2] 99 %outOfBoundsValue)))
The :
macro can be used in place of aget
for more compact notation
(assert (== %33 (:0 [33 44 55])))
(assert (== %44 (:1 [33 44 55])))
(assert (== %55 (:2 [33 44 55])))
(assert (== %outOfVoundsValue (:99 [33 44 55] %outOfBoundsValue)))
The aset
function modifies the value in the array at the given index
(def arr [0 1 2])
(aset arr 1 3)
; arr should now be [0 3 2]
Arrays are mutable in zygomys.
The list
, cons
, first
, and rest
functions operate the same as in
other LISP dialects. Note, however, that first
and rest
can also work on
arrays. The second
function on lists and arrays gets the second element;
this different from rest
which returns a sequence after the first
.
The flatten
function will convert a nested list of strings and symbols into a flattened array of strings.
The sget
function is similar to the aget
function, except it operates on
characters of a string instead of elements of a list.
The hget
function retrieves a value from the hashmap by key.
(def h (hash %a 2 %b 3))
(hget h %a) ; => 2
You can give the hget
function a third argument, which is the default value that will be returned if the key is not found. If no default is given and the key is not found, a runtime error will occur.
(hget (hash %a 3) %b 0) ; => 0
The hset
function works similar to aset
but using a key instead of an index.
(def h (hash %a 3))
(hset h %a 2) ; h is now (hash %a 2)
The hdel
function takes a hash and a key and deletes the given key from the hash.
(def h (hash %a 3 %b 2))
(hdel h %a) ; h is now (hash %b 2)
The (defmap)
macro creates a named record type. The underlying hashmap can be accessed with the same tools as above. The ':' and '->' operators can also be deployed.
> (defmap castle)
> (defmap tower)
> (def disney (castle name:"Cinderella" attraction: (tower rooms:4 view:"spectacular")))
> disney
(castle name:"Cinderella" attraction: (tower rooms:4 view:"spectacular"))
>
> (-> disney attraction: rooms:)
4
> (-> disney attraction: view:)
"spectacular"
> (-> disney name:)
"Cinderella"
> (:name disney)
"Cinderella"
> (:attraction disney)
(tower rooms:4 view:"spectacular")
> (:rooms (:attraction disney))
4
>
> (hset (:attraction disney) rooms: 34)
> (:attraction disney)
(tower rooms:34 view:"spectacular")
> disney
(castle name:"Cinderella" attraction: (tower rooms:34 view:"spectacular"))
>
The append
function can append an expression to the end of an array or
a character onto the end of a string.
(append [0 1] 2) ; => [0 1 2]
(append "ab" 'c') ; => "abc"
(append [0 1] [2 3]) ; => [0 1 [2 3]]
The appendslice
will join two arrays together without creating an
array within an array, much like calling Go's append(a, b...).
(appendslice [0 1] [2 3]) ; => [0 1 2 3]
The concat
function overlaps with append
and appendslice
. It will concatenate
any number of arrays, strings, or lists.
(concat [0 1] [2 3] [4 5]) ; => [0 1 2 3 4 5]
(concat "ab" "cd" "ef") ; => "abcdef"
(concat %(1 2) %(3 4) %(5 6)) ; => (1 2 3 4 5 6)
The len
function returns the number of elements in an array, keys in a hash,
or number of characters in a string.
-
(newScope ...)
introduces a new scope. The statements in the ... will not leak bindings or change variables outside the scope -- that is, unless (set) is used to do so deliberately.
Mostly for system development, several REPL-only special commands are recognized that let one see exactly what the interpreter is doing as it generates and executes expressions.
-
.debug
an.undebug
turn on and off the system tracing facility -- warning, very low level! :-) -
.dump
will show the current environment's functions instruction and stack -
.dump function
will dump a specific function by name -
.gls
(global listing) will display the symbol table, all bindings. -
.ls
will show the internal stacks. Placing(_ls)
in your code will produce a stack dump at that point in execution, and continue executing. -
(infixExpand {})
will show the converted infix expression in its s-expression form.
The sys
macro calls the system
function; this issues a shell command and returns the stdout and stderr as a string. The sys
must have a space after it. For example,
zygo> (sys pwd)
`/Users/jaten/go/src/github.com/glycerine/zygomys.wiki`
zygo>
zygo> (sys ls -1) // not all symbols come through the macro cleanly
error in system:-1: Error calling 'system': flatten on '[]zygo.Sexp{zygo.SexpPair{Head:zygo.SexpSymbol{name:"ls", number:157, isDot:false, ztype:""}, Tail:zygo.SexpPair{Head:-1, Tail:0}}}' failed with error 'arguments to system must be strings; instead we have zygo.SexpInt / val = '-1''
in __main:2
zygo> (sys `ls -1`) // so, simply: pass a string to avoid the -1 parsing as a number token
`Go API.md
Home.md
Language.md`
zygo>
zygo> // illustrate extracting a string from system output
zygo>
zygo> (nsplit (sys `ls -1`)) // split on newlines into an array
["Go API.md" "Home.md" "Language.md"]
zygo> (:2 (nsplit (sys `ls -1`))) // pick array index 2 (0-based always)
`Language.md`
zygo>
Statements placed within the '{' and '}' curly braces are treated as infix, and parsed accordingly. For example, 'if' and 'else', comparisons, and arithmetic work as expected in infix mode. Function calls always use prefix syntax.
zygo> a = 10
10
zygo> if a * 10 + 3 > 100 + 2 { (println "over the one-oh-two-mark") } else { (println "nope")}
over the one-oh-two-mark
zygo> if a * 10 + 3 > 100 + 5 { (println "over the one-oh-five-mark") } else { (println "nope")}
nope
zygo>
Notice this works because the repl is implicitly wrapped in an infix block. This allows the repl to work as a calculator easily.
Mirroring Go, you can partition code into packages. Packages use the same visibility rules as Go: lower-case identifiers are private, upper-case identifiers are publically accessible.
zygo> stuff = (package stuff (def b "i am a private string") (def B "I am a Public string"))
(package stuff
elem 0 of : global
(global scope - omitting content for brevity)
elem 1 of : scope Name: 'stuff'
B -> "I am a Public string"
b -> "i am a private string"
)
zygo> stuff.B
"I am a Public string"
zygo> stuff.b
error in __main:9: Cannot access private member 'b' of package 'stuff'
zygo>
Notice that the private (lower-case) variable was not available outside the package.
If you store a package in a file, then -- just as in Go -- you can use the import statement
to import the package from the path to the file. Here we'll use the example package
helloKit from the tests directory, giving it the alias pk
.
zygo> (sys `pwd`)
"/Users/jaten/go/src/github.com/glycerine/zygomys"
zygo> (import pk "tests/prepackage")
(package helloKit
elem 0 of : global
(global scope - omitting content for brevity)
elem 1 of : scope Name: 'helloKit'
Funky -> (defn Funky [a] (concat a " rov" Rov " chases " Kit))
Kit -> "cat"
Rov -> "erDog"
UsePriv -> (defn UsePriv [] (concat "my number is " (str privetLane)))
privetLane -> 42
)
zygo> pk.Rov
"erDog"
zygo>
The map
function takes a function as the first argument. It applies that function to the second argument, which can be either a list or an array.
zygo> (map (fn [x] (** x 3)) %(1 2 3 4 5))
(1 8 27 64 125)
zygo> (map (fn [x] (** x 3)) [1 2 3 4 5])
[1 8 27 64 125]
zygo> (map (fn [x] (+ x 1)) [1 2 3 4 5])
[2 3 4 5 6]
zygo>