Where your fantaZies come true
Why create a new programming language?
If you want to know why Z is designed the way it is, go here. But if you want want to know why Z was built in the first place, keep reading. Transpile to JS is pretty popular
these days. JS has become the de facto assembely of the web, because virtually no one codes in "Vanilla JS" these days. A plethora of languages (CoffeeScript, LiveScript, Elm, ClojureScript, TypeScript) have all emerged on the sole
basis of compiling to JS. However, we can seperate the languages that transpile to JS into two categories:
1. Syntatic Sugar
Languages that add new compile-time features to JavaScript, but no knew runtime features. These languages interop with JavaScript nearly seemlessly, but they fail to deliever certain features that require runtime additions, including operator overloading, pattern matching, better concatenation rules, etc.
Examples:
- CoffeeScript
- LiveScript
- ESNext + Babel
- TypeScript
2. JVM Languages (JavaScript Virtual Machine)
These languages add new compile-time and run-time features to JS, and use JavaScript more like assembely code than a target language. While these languages can add any features they want, they often have limited and clunky interop with JS, and are all are limited to the frontend, whereas Syntatic Sugar languages can be used anywhere.
Examples:
- Elm
- Haxe
- ClojureScript
So what are you waiting for? Are you ready to transform your backend experience with amazing features and a cool runtime and standard library? Jump in and learn some Z!
*Z is still in rapid development. There may be bugs that pop up in the Z Compiler as you develop your application. Report them here.
If you still have any problems with Z, or want any new features added, email n8programs@gmail.com.
Getting Started
This tutorial assumes that you have both node and npm installed. If you don't, you can install node here and npm comes with node. If you prefer Yarn (the faster, prettier alternative, you can download it here)
To start, enter a terminal session. Every Z package on npm is namespaced under @zlanguage
. Install the Z compiler as so (make sure to install version 0.4.5, as it is the last stable release):
$ npm install -g @zlanguage/zcomp@0.4.5Now, in order for the compiler to function, you must also install
globby
:
$ npm install -g globby
Or, with Yarn:
$ yarn global add @zlanguage/zcomp@0.4.5 globby
That wasn't too hard! Now, to experiment with Z, let's launch a REPL.
You can launch a Z REPL with:
$ zcomp repl
If all goes well, you should see the following:
zrepl>
Basic Expressions and Math
All expressions defined here are meant to be executed from a REPL.
Let's start by creating a simple math expression:
3 + 2
The REPL will print back 5.
Order of operations works:
3 + 2 * 7
The REPL gives you back 17, not 35.
The following mathematical operators are supported at the following orders of precedence:
^ pow
* / %
+ -
pow
is the same as ^
except that ^
is left-associative while pow
is right-associative.
Number Extensions
Besides typical JavaScript number literals, Z supports the following extensions to them: A number can have:
1_000
Underscores0x100
Hexadecimal, Binary, and Octal prefixes!10bytes
Trailing characters1.somePropertyHere
Trailing refinement
More Expressions
To start, you may have noticed that inputting a raw number without any math is considered an error in the Z REPL. While this may seem peculiar, this is to avoid "useless expressions", like randomly putting a string or a number on some code.
Single line Comments in Z are denoted with #
:
# I'm a single line comment!
Block comments are denoted with /*
and */
Strings in Z are easy enough to reason with:
"Hello" ++ " World" # ===> "Hello World"
Z performs coercion, but its coercion rules make more sense than JavaScript's:
"9" + 2 # ==> 11
"9" ++ 2 # ==> "92"
Booleans also make sense:
true and false # ==> false
false or true # ==> true
Symbol literals are denoted with @, so @x
is the same as Symbol.for("x")
, and @@iterator
is the same as Symbol.iterator
.
Regexps literals are like JavaScript, but they are denoted with `
rather than /
. So `x`g
is the same as /x/g
Now that you have touched the surface of Z, it's time to take it up a notch and start writing files in Z.
Your First File
Now that you've tested Z out, create a directory, call it ztest:
$ mkdir ztest
For each directory you create that will use Z, you must install the Z Standard Library:
$ npm install @zlanguage/zstdlib --save
That really wasn't that much setup.
Now, create a new file, call it hello-world.zlang
:
$ touch hello-world.zlang
Launch your favorite IDE, open helloworld.zlang, and type:
log("Hello World")
Execute the file with:
$ zcomp run helloworld.zlang
It should print out Hello World
to the terminal.
Files can hold more advanced expressions than the REPL, and have statements in them two. From now on, all examples assume that they are being typed in a file. Some examples will contain features that don't work in the REPL.
Variables
Variables in Z can hold any value, they are not constrained to one type of value.
Reassignable variables can be declared with let
:
Note that :
is the assignment operator.
let x: 5 # ==> x is now 5
x: "Hola Mundo" # ==> x has changed types
let y: undefined # ==> You must assign a variable when you initialize it.
y: Math.random() # ==> Put something in y
Constant variables are declared with def
. Constant variables cannot be reassigned, but their internal value can still change:
def x: 5 # ==> This looks familiar.
x: "Hola Mundo" # ==> Runtime error.
Finally, hoisted variables (akin to variables declared with var
in JavaScript) are declared with hoist
:
log(x) # I can see x from all the way up here!
hoist x: 5
So to map Z's assignment statements to their equivalents in JS:
let
-let
def
-const
hoist
-var
Invocations, Arrays, and Objects
You've already seen some invocations in Z (of log
and Math.random
)
As will all built-in data types in Z, Z functions map right to their JavaScript equivalents. Which means calling a function in Z with ()
transpiles to calling a function in JavaScript with ()
:
log("Hola mundo!") # Log acts like console.log.
console.log("Hola mundo!") # Does the same thing.
Math.random() # Let's call another JS function
Date.now() # They all work fine!
Collections in Z
Z supports numerous flexible collection literals to represent objects and arrays, the simplest being brackets:
[1, 2, 3] # Arrays literals are just like JavaScript
let x: 5
[
"hola": "mundo", # Objects are a bit different, object properties in Z are computed, so quotes are required.
x # Expands to "x": 5 if there are other properties in the object
]
[] # Empty Array
[:] # Empty Object
Parentheses can also be used to denote arrays, and brackets can denote arrays or objects. Arrays constructed from parentheses and objects constructed from brackets can be used in destructuring (which will be covered later):
(1, 2, 3) # Array Literal
{1, 2, 3} # Array Literal
{
"x": 3
} # Object Literal
() # Empty Array Literal
{} # Empty Array Literal
Range literals correspond to arrays. They can be written in several different fashions:
1 to 5 # [1, 2, 3, 4, 5]
1...5 # [1, 2, 3, 4, 5]
1 til 5 # [1, 2, 3, 4]
1 to 5 by 2 # [1, 3, 5]
1 til 5 by 2 # [1, 3]
When creating range literals, you can
When invoking a function, you can emulate named parameters with an implicit object (like in ruby):
Point(x: 3, y: 4) # Is like...
Point([
"x": 3,
"y": 4
])
Property Access is akin to JavaScript:
x.y # Alphanumeric property access
x["y"] # Computed property access (any expression can go in the brackets)
x..y # Conditional property access (only works if x isn't falsy, akin to x && x.y)
Control Flow
Z supports very simple control flow. It may seem primitive, but for most imperative constructs, it's all you need. When coding Z procedurally (rather than functionally) these control flow statements will be your best friends.
Z supports typical if
, else if
, and else
statements:
let x: 3
if x = 3 { # Check for equality
log("Three times is the charm!")
} else if x > 0 { # Check for positivity
log("X is positive.")
} else {
log("X is feeling negative today.")
}
Z also has the ternary operator, though its syntax is more readable than most programming languages:
# An easier way to write the above example
let x: 3
log(
if (x = 3) "Three times is the charm!"
else if (x > 0) "X is positive."
else "X is feeling negative today."
)
You can simplify one line if statements by putting the if
at the end of the line (Ruby-style):
log("I'm feeling lucky!") if Math.random() > 0.5
In the looping department, Z supports loop
, which is the equivalent of a while(true)
loop. You exit a loop with break
:
let i: 0
loop {
if i > 9 {
break
}
log(i)
i: i + 1
}
That's it. No fancy potpourri. Just one conditional structure and one type of loop. However, this section only covered Z's imperative control flow structures. You'll see more functional ones soon.
Intro to Functions
Functions in Z are created with the func
keyword. Z supports anonymous functions only, like CoffeeScript. You can name functions by binding a function to a constant variable. Otherwise, parameters and return statements are
rather similar:
def add: func (x, y) {
return x + y
} # Declare a simple function
setTimeout(func () {
log("Around one second has passed!")
}, 1000) # Passing a function as a parameter
def something: func () {
# Return something awesome
}() # IIFE
def boundFunction: func () {
}.bind(someThisValue) # Functions can have properties accessed on them because they are just objects will an internal [[Call]] property
Default parameters are created with the :
operator, and rest parameters are created with the ...
operator.
def add: func (x: 0, y: 0) { # Defaults
return x + y
}
def sum: func (...xs) {
return xs.reduce(func (t, v) { # We'll make this example more concise later.
return t + v
})
}
If a function only consists of one return statement, the curly brackets and return
may be omitted:
def sum: func (...xs) xs.reduce(func (t, v) t + v)
You can mark variables declared within a one-line function (a function with an implicit return statement) to be inferred, by ending them with an exclamation point:
def sum: func (...xs) xs.reduce(func t! + v!) # We'll see how to make this even more concise later.
You can pipe a value through multiple functions via |>
:
3 |> +(1) |> *(2) |> log # Logs 8
You can use >>
and <<
for function composition (as in Elm).
You can partially apply functions with @
def square: Math.pow(@, 2)
def logSquare: log >> square
logSquare(10) # Logs 100
A standalone .
creates an implied function for property access and method invocations. Currently, implied functions and partial application via @
cannot be mixed. For example:
users.map(.name) # Get the name property of users
[1, 2, 3, 4, 5].map(.toString(2)) # Get the binary representation of these numbers (in string form).
That's pretty much all there is to know about basic functions in Z.
Exceptions
The first thing Z clarifies is that Exceptions are not baseballs. For some reason, rather than "raising" an issue, you would "throw" it. That makes no sense at all. And then, to resolve the issue, someone would not "settle" it,
but "catch" it. You can't play a friendly game of catch with exceptions. Z's choice of keywords is more intuitive than throw
and catch
:
try { # Attempt to do something
raise "Something contrived went wrong" # String coerced into an error object
} on err { # When something bad happens
settle err # Explicitly tell Z that the error has been settled/resolved.
}
At this point you are probably asking: why explicitly settle an error? The reason is, explicitly settling an error allows you to put time and thought into how to settle it, and what countermeasures to take. If you forget to settle an error, Z will raise a runtime error. This helps with making Plan Bs when something goes wrong.
A try can only have one on
clause. Handle type checking of the exception in the on
clause.
Z has exception handling for JavaScript interop, but please don't overuse it. Failing to parse an integer should not cause an exception.
Modules
Z's module system is closely related to JavaScript's. A simple example will demonstrate this. Create a file called exporter.zlang
in your test directory, and another file called importer.zlang
in that same directory.
Now, in exporter.zlang
, type:
export 3
In importer.zlang
, type:
import num: "./exporter"
log(num)
Now, to transpile exporter.zlang
, and not immediately run it via the compiler, use the command:
$ zcomp transpile exporter.zlang
And:
$ zcomp transpile importer.zlang
To run the code:
$ node importer.zlang
You should see a 3 printed out.
To further elaborate, each module in Z can export one thing, which is implicitly stoned (Z's version of Object.freeze
) when exported.
Imports in Z are similar to JavaScript ones, except that from
is replaced with :
:
import something: "./somewhere"
import fs # This shorthand becomes:
import fs: "fs"
import ramda.src.identity # Becomes:
import identity: "rambda/src/identity"
In order to export multiple things, you can just export an object:
export [
"something": cool,
"very": cool,
cool,
"some": other.thing
]
As you can see, Z modules are (pretty) easy to work with. We'll see a cool way to import multiple things from a module that exports an object in the next section.
Pattern Matching
Z comes with a default ternary operator:
let happy = true
let mood = if (happy) "good" else "bad" # if (cond) result2 else result2
let moodMessage =
if (mood = "good") "My mood is good."
else if (mood = "bad") "I'm not feeling good today."
else "Unknown mood." # Chaining ternary operators.
However, for advanced conditional checks, this fails to be sufficient. That's where Z's pattern matching comes into play. The match
expression at its simplest can match simplest can match simple values:
let moodMessage = match mood {
"good" => "My mood is good",
"bad" => "My mood is bad",
_ => "Unknown mood" # _ is a catch-all
}
Patten matching is more powerful than this though. It's not limited to matching primitives. You can also match exact values that are arrays and objects:
let whatItIs: match thing {
[1, 2, 3] => "An array of [1, 2, 3]",
["x": 3] => "An object with an x value of 3",
_ => "I don't know what thing is."
}
You can also match types with pattern matching:
let contrived: match someExample {
number! => "A number.",
string! => "A string.",
array! => "An array",
_ => "Something else."
}
If you want to capture the value of a certain type, use !
like an infix operator:
let times2: match thing {
number!n => n * 2,
string!s => s ++ s,
_ => [_, _]
}
Now, to capture elements of arrays, use (
and )
:
def arrResult: match arr {
(number!) => "An array that starts with a number.",
(string!s, string2!s2) => s ++ s2,
(x, ...xs) => xs, # xs represents the rest of the array, which excludes the first element in the array
_ => []
}
Objects can be matched with the {
and }
characters:
def objResult: match obj {
{ x: number!, y: number! } => "A point-like object.", # Match an objects with x and y properties
{
name: string!name,
age: number!age,
car: {
cost: number!,
brand: string!brand
}
} => "A person named " ++ name ++ " that is " ++ age ++ " years old. He/She owns a " ++ brand ++ " type of car.",
_ => "Some other thing"
}
To match a number in between other numbers, use range literals:
def typeOfSquare: match square {
{ size: 1...10 } => "A small square.",
{ size: 11...20 } => "A medium square.",
{ size: number! } => "A big square.",
_ => "Something else."
}
The object and array delimiters in pattern matching work as destructuring too:
def (x, y): [3, 4] # x is 3, y is 4
def {x, y}: [
"x": 3,
"y": 4
] # x is 3, y is 4
You can define blocks
to be associated with different patterns, for example:
match num {
1 => {
log("Num is 1.")
log ("I love the number 1.") # You can put multiple lines in a "block"
return "YAY!" # Blocks are wrapped into functions, so you can return from them.
},
_ => "Not 1 :("
}
You can define your own custom pattern matching conditions with predicates
. To start, define some functions that return a booleans:
def even: func x! % 2 = 0
def odd: func x! % 2 = 1
Then, use the ?
at the end of the function name inside a match
body to use the predicate:
match num {
even? => "An even number.",
odd? => "An odd number.",
number! => "Some other number.",
_ => "Something else."
}
The most advanced form of custom pattern matching is the extractor
. It allows you to not only perform a conditional check on data, but to perform custom matching on it.
Let's start by defining a simple email function:
def Email: func user! ++ "@" ++ domain!
Then, we can defined a extract
method on email
. This extract
method should return an array if there is a pattern to be matched, or undefined
, if there is no match:
Email.extract: func (str) if (str.includes("@")) str.split("@") else undefined
def myEmail: "programmer@cloud.com"
match myEmail {
Email(user, domain) => log(user, domain), # Logs programmer, cloud.com
_ => log("Invalid email.")
}
As you can see extractors
and predicates
add greater flexibility and power to pattern matching.
Runtime Types
Z supports numerous ways to create runtime type checks. Each object in Z can specify its "type" by having a function called type
:
[
"x": 5,
"y": 5,
"type": func "Point"
]
You can find out something's type using the built-in typeOf
function:
typeOf(3) # ==> "number"
typeOf([1, 2, 3]) # ==> "array"
typeOf([
"x": 5,
"y": 5,
"type": func "Point"
]) # ==> "Point"
You can check that a parameter passed to a function is of a certain type at runtime (checking is done behind the scenes with typeOf
):
def add: func (x number!, y number!) { # Note that you can't mix type annotations with default values and rest/spread
def res: x + y
return res
}
!
isn't actually part of the type. It just denotes that a type is present.
You can also add return type annotations:
def add: func (x number!, y number!) number! {
def res: x + y
return res
}
You can also validate that the right-hand side of an assignment is of a certain type:
def add: func (x number!, y number!) number! {
def res number!: x + y
return res
}
This works great for simple functions, however you may need to implement more complex ones. This is made possible by the enter
and exit
statements:
def readPrefs: func (prefs string!) {
enter {
prefs.length < 25
}
def fileHandler: file.getHandler(prefs) # Some imaginary file system.
# Do some stuff
return something
exit {
fileHandler.close() # Clean up the file handler, exit is like finally and must be the last statement in a function.
}
}
enter
is a block of code that contains comma-separated conditions, all of which must be true when the function starts:
def readBytes(bytestream Bytestream!, amount number!) { # fictional type Bytestream
enter {
bytestream.size < amount,
amount < 100,
amount > 0,
}
# Do stuff...
}
exit
pretty much the same as enter, except it is executed at the end of the function, to see if certain conditions have been met. exit
must be the last statement in a function.
A function may only have one
enter
statement and one
exit
statement.
loop
Expressions
loop
expressions are directly inspired by Scala. They are based on Scala's for
expressions, and they may resemble list comprehensions in some languages.
To start, use the operator <-
to map over a list:
def xs: [1, 2, 3]
def result: loop (x <- xs) x * 2 # Result is [2, 4, 6]
You can add predicates using a form of if
:
def xs: [1, 2, 3]
def result: loop (x <- xs, if x % 2 = 0) x * 2 # Result is [2, 6]
You can iterate over multiple lists by inserting multiple <-
s:
# Range literals: 1...5 is [1, 2, 3, 4, 5]
def result: loop (x <- 1...10, y <- 1...10) [x, y] # Matrix of numbers 1 to 10
Using all of this, you could define a flatMap
function:
def flatMap: func (f, xs) {
return loop (
x <- xs,
y <- f(x)
) y
}
Note that you cannot start a line with a loop
expression, as it will be confused with the imperative loop
statement.
The final ability of the loop
expression is that you can place assignments in it. For example:
def strs: ["Hello", "World"]
def res: loop (s <- strs, l: s.length) l * 2 # res is [10, 10]
Operators
You've already seen use of plenty of operators in Z. You've seen addition, subtraction, comparison, equality, and more. But for complete reference, below is a list of operators that come with the Z runtime, and their precedence:
The Left Overload is a method you can define on an object to overload the operator on the left-hand side:
x + y
becomes x.+(y)
if x
defines a +
method.
The Right Overload is a method you can define on an object to overload the operator on the right-hand side:
x + y
becomes y.r+(x)
if y
defines a r+
method.
Operator | Associativity | Precedence | Function | Left Overload | Right Overload |
---|---|---|---|---|---|
pow | Right | Infinity | Performs exponentiation | NA (overload * instead) | NA (overload r* instead) |
til | Left | 555 | Exclusive range | prev & succ & < | NA |
to | Left | 555 | Inclusive range | prev & succ & < | NA |
by | Left | 444 | Used to specify the step of ranges | NA | NA |
^ | Left | 333 | Performs exponentiation | NA (overload * instead) | NA (overload r* instead) |
% | Left | 222 | Performs modulus | % | r% |
/ | Left | 222 | Performs division | / | r/ |
* | Left | 222 | Performs multiplication | * | r* |
+ | Left | 111 | Performs addition | + | r+ |
- | Left | 111 | Performs subtraction | - | r- |
++ | Left | 111 | Performs concatenation | concat | NA |
>> | Left | 1 | Left-to-right composition | NA | NA |
<< | Left | 1 | Right-to-left composition | NA | NA |
|> | Left | 1 | Pipe | NA | NA |
< | Left | -111 | Less-than | < | r< |
<= | Left | -111 | Less-than or Equal-to | NA (Define < instead) | NA (Define r< instead) |
> | Left | -111 | Greater-than | NA (Define < instead) | NA (Define r< instead) |
>= | Left | -111 | Greater-than or Equal-to | NA (Define < instead) | NA (Define r< instead) |
= | Left | -222 | Compares Structural Equality | = | r= |
and | Left | -333 | And boolean comparison | NA | NA |
or | Left | -333 | Or boolean comparison | NA | NA |
The negative precedence and non-consecutive precedence numbers will be explained soon.
First Class Operators
Z has first-class operators, meaning the operators aren't special. They can be created, stored in variables, and in fact, are just ordinary functions.
+
is just defined as an ordinary function! Functions (like +
) can then be called with infix syntax (Note that in Z 0.4.0+, operators MUST HAVE ALL SYMBOL NAMES):
However, operators are left associative and can have custom precedence:
def +': func x! + y!
3 +' 4 * 2 # ==> 11 +' has no precedence, defaults to 1, evaluates after multiplication
You can define a custom precedence for your operators:
# Continuing from the last example:
operator +': 1000 # Give it a high Precedence
3 +' 4 * 2 # ==> 14
Now, all the large precedence numbers should make sense. Operators having large gaps in precedence allows for insertion of operators in between precedence levels.
Since operators are functions, they can be curried. All the built-in operators actually are:
3 |> *(2) |> +(1) |> to(1) # [1, 2, 3, 4, 5, 6, 7]
The following symbol characters are allowed in identifiers: +
, -
, *
, /
, ^
, ?
, <
, >
, =
, !
, \
,
&
, |
, %
, '
Since operators are just functions, you can use them like ordinary functions:
# Add function from before:
def add: +
# Sum an array
[1, 2, 3].reduce(+)
Macros
Note that dollar directives have been removed. The new macro system is much more capable.
Macros are compile-time "functions" that operate on AST nodes. Let's start by looking at a very simple macro:
macro $hello () {
return ~{
"Hello World"
}~
}
There's a lot going on here. First, we define a macro called $hello
with the macro
keyword. All macros in Z start with the $
symbol, because this makes them "pop out" - so you know what's a macro
and what isn't. Then, we return a template, denoted by ~{
and }~
which contains some Z code. This template in this case just contains the string "Hello World"
, meaning that any call to this macro
is immediately replaced with "Hello World"
at compile time. For example:
log($hello)
With the macro above, it will now print "Hello World". Now, macros can also take parameters. This simple $id
macro takes one expression and returns it:
macro $id (~x:expr) {
return ~{
{{~x}}
}~
}
You can see what happens. The tilde denotes a macro parameter, which in this case is of type expr
. You call this parameter x
. Then, when you return a template, you use double braces and the tilde operator again,
to "spread" the value of x
into the template. For example:
log($id [1, 2, 3])
It just logs what you passed to the macro. However, parameters passed to any macro are also available in AST form. For example:
macro $addFour (~arr:expr) {
arr.push(4)
return ~{
{{~arr}}
}~
}
log($addFour [1, 2, 3])
It will log [1, 2, 3, 4]
. However, because AST nodes are passed to macro by reference, you're not allowed to reassign them without loosing the reference. So arr: arr.concat(4)
would make not change the value of
the AST node.
You can also pass blocks to macros - this makes for a natural looking syntax:
macro $while (~cond:expr, ~body:block) {
return ~{
loop {
if not({{~cond}}) {
break
}
{{~body}}
}
}~
}
$while true {
log("MACROS ARE AWESOME!!!!")
}
As one can see, blocks spread into their execution context, so the block passed to $while
becomes part of the loop
block.
Let's say we wanted to make a for-loop macro. It would iterate over a collection, like JavaScript's for-of loop. The parameter list looks like this:
macro $for (~l:expr, of, ~r:expr, ~body:block) {
}
The standalone of
in the parameter list defines a conextual keyword. So of
can act like a keyword in the context of a $for
macro. Now, since we want to iterate over all iterators, we are going to
use Z's loop expression (from the last section): <-
. We are going to define a callback, and for each element of the iterator we're going to call the function for it. Since Z's macros are are non-hygenic, and since we
don't want to corrupt the local environment, we are going to use psuedohygiene for a variable names. This means Z will randomly generate ids for a variable names that keep them readable while leaving only a one in one million chance
of a name conflict with another generated identifier. You accomplish this by using double brackets without the tilde:
macro $for (~l:expr, of, ~r:expr, ~body:block) {
return ~{
def {{callback}}: func ({{~l}}) {
{{~body}}
}
{} ++ loop ({{i}} <- {{~r}}) {{callback}}({{i}})
}~
}
You can use it like this:
$for x of [1, 2, 3] {
log(x)
}
Now, macros can also take keywods as arguments via id parameters:
macro $asgn(~type:id, ~lvalue:expr, =>, ~rvalue:expr)
{
return ~{
{{~type}} {{~lvalue}}: {{~rvalue}}
}~
}
$asgn def x => 3
$asgn let y => 3
Finally: marco varargs capture groups within a certain pattern. You can then use ...(){}
to generate statements for each captured argument:
macro $logEach (...(~toLog:expr)){
return ~{
...(){
log({{~toLog}})
}
}~
}
$logEach (1 2 3 4 5)
This logs each number. A trailing comma can be added to indicate comma seperated values:
macro $logEach (...(~toLog:expr,)){
return ~{
...(){
log({{~toLog}})
}
}~
}
$logEach (1, 2, 3, 4, 5)
Using all of this, you can define a $switch
macro:
macro $switch (~val:expr, ...{case, ~test:expr, ~body:block}){
return ~{
...(){
if {{~test}} = {{~val}} {
{{~body}}
}
}
}~
}
Then you can use it like this:
$switch 1 {
case 1 {
log("1")
}
case 2 {
log("2")
}
case 3 {
log("3")
}
}
Since macros aren't runtime constructs, you can't export them. Instead, in an old-timey fashion, you include
macros into your program. The include
statement copies and pastes the contents of one Z file into another,
during compilation, which allows macros to be "imported":
include "./for.zlang" # A file with the $for macro we defined before
$for x of [1, 2, 3] {
log(x)
}
Macros can also be grabbed from the standard library via includestd
:
includestd "imperative" # Grabs all the macros from the imperative.zlang file stored in the standard library.
Note that macros are still under development. Report any bugs you find here.
Enums
A note: Enums are only available in Z 0.3.1+. A stable, non-buggy implementation of enums is only available in 0.3.5+.
While Z doesn't support classical OOP, Z mixes OOP and FP in Rust-Style enums, which are akin to the algebraic data types of functional languages.
We are going to create a classic cons-list. You may also know this as a linked list.
The general idea of a cons-list is that each "node" of the list could either be Nil
, the empty/end of a list, or a value, and the rest of the cons-list. For example, the cons-list equivalent of
[1, 2, 3]
would be:
Cons(1, Cons(2, Cons(3, Nil())))
To implement this, let's look at enum
s. Enums in Z aren't a special new kind of type, they're just a special way to define certain types of functions. To start, let's make a simple enum representing a color:
enum Color {
Red,
Orange,
Yellow,
Green,
Blue,
Purple
}
We can construct new members of an enum simply by calling its possible states:
Red() # Constructs the "Red" member of the color enum.
Orange() # Constructs the "Orange" member of the color enum.
You can also refer to the enum collectively via its name:
# This is the same as the example above.
Color.Red()
Color.Orange()
The =
operator is automatically defined on each state of Color
. For example:
def col: Red()
col = Red() # true
col = Color.Red() # Also true
col = Orange() # false
Now that you've seen the basics of enums, let's start defining our cons-list enum. We'll call it List
and give it two possible states: Cons
and Nil
:
enum List {
Cons,
Nil
}
However, there's a problem. Cons
needs to store two pieces of data: the first value and the rest of the list. In order to do this, we need fields. Let's look at a simple example of fields with a Point
enum:
enum Point {
Point(x, y)
}
# This could also be written as the following:
enum Point(x, y)
# Because the Point enum has only one constructor with the same name as it
Now, we can construct point objects using Point
, or even Point.Point
. They will have read-only x
and y
properties defined on them:
def myPoint: Point(3, 4)
log(myPoint.x) # 3
log(myPoint.y) # 4
def anotherPoint: Point(x: 3, y: 4) # Named fields can be used to increase readability.
def thirdPoint: Point(y: 4, x: 3) # Named fields can be in any order.
log(myPoint = anotherPoint) # This is true, fields are taken into account in equality too.
We can also use pattern matching to extract fields:
match something {
Point(x, y) => x + y, # Only runs if object is constructed via Point.
_ => "Not a point"
}
Using fields, we can create a working implementation of the cons-list:
enum List {
Cons(val, rest),
Nil
}
Now, we can create cons-lists, and test if they are equal:
def li1: Cons(1, Cons(2, Nil()))
def li2: Cons(1, Cons(3, Cons(4, Nil())))
def li3: Cons(val: 1, rest: Cons(val: 2, rest: Nil()))
li1 = li2 # False
li1 = li3 # True
Now, to easily iterate and apply transformations to cons-lists, let's define a consForEach function that takes a function and a cons-list as a parameter and will pass the function each value in the cons-list:
def consForEach: func (f, list) {
loop {
if list = Nil() {
break
}
f(list.val)
list: list.rest
}
}
However, shouldn't we be able to associate consForEach
with List
itself. Say hello to the where
block. Add the following to your List
definition:
enum List {
Cons(val, rest),
Nil
} where {
forEach(f, list) {
loop {
if list = Nil() {
break
}
f(list.val)
list: list.rest
}
}
}
Now, you can use forEach
like this:
List.forEach(log, Cons(1, Cons(2, Cons(3, Nil()))))
It will print out the elements of the cons-list, one by one.
Now, how can we add types to the fields of Cons
? Let's start by observing types of fields in action:
enum Point(x: number!, y: number!)
Point(3, 4) # All good!
Point("hola", 4) # Error!
enum Line(start: Point!, end: Point!) # Enums can also be used as types
Line(Point(3, 4), Point(3, 4)) # All good!
Line(3, 4) # Error!
Because of this, you'll probably try something like:
enum List {
Cons(val, rest: List!),
Nil
}
However, you'll get an error. Currently, all of an enum constructors must be typed or all must be untyped. There's no in-between. To get around this, you can use the _!
type, which is a work-around for enums:
enum List {
Cons(val: _!, rest: List!),
Nil
}
Now, you'll receive an error (at runtime) when rest
is not of type List
, but val
can be of any type.
Note that _!
only works with enums. In function definitions, you just leave out the type to imply _!
.
While the forEach
functioned defined above is useful, what if we wanted to print out the cons-list as a whole? If you were programming in JavaScript, you might write a custom implementation of the toString
method.
However, enums can derive traits, and unlike in other languages traits/interfaces/protocols in Z are just normal functions given context via the derives
keyword. Let's look at how this works. Start by importing the standard
library's traits module traits
:
importstd traits
Now, extract the Show
trait from traits
:
def {Show}: traits
Now, alert your definition of List
to use the derives
keyword:
enum List {
Cons(val: _!, rest: List!),
Nil
} derives (Show) where {
forEach(f, list) {
loop {
if list = Nil() {
break
}
f(list.val)
list: list.rest
}
}
}
Now, you'll find that any cons-list constructed has a toString
method:
log(Cons(1, Cons(2, Cons(3, Nil()))).toString())
# Logs "Cons(val: 1, rest: Cons(val: 2, rest: Cons(val: 3, rest: Nil())))"
Now let's look at another kind of way to implement a trait: statically. Traits implemented with the static
keyword are automatically applied to each instance of an enum, but to the enum itself. To demonstrate, take the Curry
trait from the trait
module
def {Show, Curry}: traits
Now, add static Curry
to the derives
expression in the definition of list:
enum List {
Cons(val: _!, rest: List!),
Nil
} derives (Show, static Curry) where {
forEach(f, list) {
loop {
if list = Nil() {
break
}
f(list.val)
list: list.rest
}
}
}
Curry
makes all of an objects methods curried, and sicne we derived Curry
on List
, we can do:
def logger: List.forEach(log)
logger(Cons(1, Cons(2, Cons(3, Nil()))))
Below is a list of all the traits defined by the traits
module:
Show
Defines a toString
method on an instance of an enum, which provides a more meaningful string to work with than "[object Object]"
.
Read
Defines a read
method on an enum that attempts to parse a string and return an instance of that enum. However, parsing is limited and only numbers and built-in constants will be converted to their equivalents. Should be
implemented with static
.
Ord
Makes an enum comparable by overloading <
. Starts by comparing constructor order:
enum Color {
Red,
Orange,
Yellow,
Green,
Blue,
Purple
} derives (Ord)
Yellow() < Blue() # True
Orange() > Red() # True
Yellow() <= Orange() # False
If both the left-hand operand and the right-hand operand have the same constructor, it will check to see if the left-hand operand's field is less than the right-hand operand's field:
enum Maybe {
Just(thing),
None
} derives (Ord)
Just(3) < Just(5) # True
It is not recommended to use Ord
on enums that have constructors that have more than one field.
Copy
Copy defines a copy
method on each instance of an enum:
enum Point(x: number!, y: number!) derives (Show, Copy)
log(Point(3, 4).copy(y: 2).toString()) # Logs Point(x: 3, y: 2)
Enum
Enum defines the methods prev
, succ
, and to
on each instance of an enum to allow for creation of ranges and the like:
enum Color {
Red,
Orange,
Yellow,
Green,
Blue,
Purple
} derives (Show, Enum)
log(Red().succ().succ().succ().prev().toString()) # "Yellow()"
log(Red().to(Yellow()).toString()) # "Red(),Orange(),Yellow()"
log(Yellow().to(Red()).toString()) # "Yellow(),Orange(),Red()"
By deriving both Enum
and Ord
you can overload range literals (the to
type, not the ...
type):
enum Color {
Red,
Orange,
Yellow,
Green,
Blue,
Purple
} derives (Show, Ord, Enum)
log((Red() to Yellow()).toString()) # Red(),Orange(),Yellow()
log((Purple() til Red() by 2).toString()) # Purple(),Green(),Orange()
PlusMinus
PlusMinus overloads the +
and -
operators for every instance of an enum. If both operands have the same constructor, and returns a new instance of that constructor with all the fields added. Otherwise, it
adds/subtracts the relative order of the constructors in the enum declaration, and returns an instance of the constructor at that index.
Json
Makes each instance of an enum JSON serializable.
Curry
Curries each method of a certain object. That means if it's implemented with static
it curries the methods of an enum. If it's implemented without static
, it will curry every method on every instance of the
enum.
All the traits above are great, but what if we wanted to build our own trait? We're going to be constructing a Sum
trait that defines a sum
method to add all of a traits fields together.
The implementation of the trait is below:
def Sum: func (obj) {
obj.sum: func () {
let sum: 0
obj.fields.forEach(func (field) {
sum: sum + obj[field]
})
return sum
}
return obj
}
First off, you may notice there's no new trait
keyword. It's just a function. First, the function takes an object representing the newly constructed instance of an enum. It then adds a method to that object: sum. Every instance
of an enum has a read-only fields
property which holds an array containing the string names of every property defined by the enum constructor. By iterating over that array, we can pull out each field name, and then the
value of each field. It adds them, and then returns the object, which now has a sum method. Now, to use this, let's revisit the Point
enum from earlier, and derive Sum
on it:
enum Point(x: number!, y: number!) derives (Sum)
Try using a sum method on a point instance:
log(Point(3, 4).sum())
It should log 7.
When you derive a trait statically, the object passed to the function is the enum itself, for example the Point
object would be passed to the Read
trait if it were derived statically.
To add "reflection" to each enum instance, every instance contains the following metadata:
instance.fields # Array of all fields the constructor defined on the instance
instance.constructor # Reference to the constructor of the instance (which is just a function)
instance.parent # Reference to the overarching enum that the point's constructor belongs to (which is just an object)
parent.order # Array of the order in which the constructors for an enum were defined. Useful for creating traits like "Ord" and "Enum"
Advanced Compiler Commands
There are three commands in the compiler that have not yet been covered: dirt
, watch
, and wdir
:
Directory Recursive Transpilation or DIRT:
The dirt command will transpile an entire directory to an "out" directory, maintaining file structure. So if you have a directory called src
, and you want to transpile everything in it to dist
, use:
$ zcomp dirt src dist
That's it.
Watching Files
If you have a file, say iwillchange.z
, use the watch
command to monitor it for changes, and transpile the file when changed:
$ zcomp watch iwillchange.z ../dist/iwillchange.js
Directory Watching
The wdir
command will watch a directory for changes, and then use dirt
to transpile it when changes occur. This is useful for production, where you have complex nested directories that you need to transpile all
at once. For example:
$ zcomp wdir src dist
In 0.3.8+, the Z REPL has additional capabilities. First off, it will allow you to enter multiline statements when the first statement ends in {
, (
or [
. When you close the block, invocation, or array/object
literal, all that code will be evaluated via the repl.
You can also load and gain access to the functions in a Z file using the :l
command. If you have a file called add.zlang
, which contains a function that adds two numbers, load it via:
zrepl>:l add
And then you can use it as if you had typed it into the REPL yourself.
Runtime Overview
Below is a list of all the built-in non operator functions included in the Z runtime:
isObject(val)
Returns true
if val
is not a scalar primitive. Otherwise, returns false
.
typeOf(val)
Returns the type of val
according to the following algorithm:
- Is
val
undefined
? If so, return"undefined"
. - Is
val
null
? If so, return"null"
. - Does
val
have afunction? Does that function return a string
? If so, return the result of callingval
'stype
function. - Is
val
NaN
? If so, return"NaN"
. - Does
Array.isArray
returntrue
forval
? If so, return"array"
. - Return the result of calling
typeof
onval
.
typeGeneric(val)
Returns the type of val
according to the following algorithm:
- Does
val
definetypeGeneric
function? Does that function return astring
? If so, return the result of calling that function. - Is
val
anarray
? If so, return astring
in the format"array<types>"
, where types is equal the result of joining a unique set of callingtypeGeneric
on all the elements inval
with"|"
stone(val)
Returns the result of recursively calling Object.freeze
on an object and its properties. Returns the object, which is now deeply immutable.
throws
when passed a circular data structure.
copy(val)
Returns a deep copy of val
, except for functions, for which it will return val
itself.
throws
when passed a circular data structure.
log(...vals)
Alias for console.log
.
not(val)
Coerces val
to a boolean
, then returns the negation of that.
both(val1, val2)
Applies the JavaScript &&
to val1 and val2, coerces the result to a boolean
, and then returns that.
either(val1, val2)
Applies the JavaScript ||
to val1 and val2, coerces the result to a boolean
, and then returns that.
m(...vals)
Returns the result of calling vals.join("\n")
chan()
Makes a channel.
send(val, ch)
Sends a value to a channel.
curry(f)
Curries a function (loosely).
JS
The following methods are defined on the global JS
object:
Method | JS Equivalent |
---|---|
JS.new(constructor, ...args) |
new (constructor)(...args) |
JS.typeof(val) |
typeof val |
JS.instanceof(val, class) |
val instanceof class /td>
|
JS.+(x) |
+x |
JS.+(x, y) |
x + y |
JS.-(x) |
-x |
JS.-(x, y) |
x - y |
JS.*(x, y) |
x * y |
JS./(x, y) |
x / y |
JS.**(x, y) |
x ** y |
JS.%(x, y) |
x % y |
JS.==(x, y) |
x == y |
JS.===(x, y) |
x === y |
JS.!=(x, y) |
x != y |
JS.!==(x, y) |
x !== y |
JS.>(x, y) |
x > y |
JS.<(x, y) |
x < y |
JS.<=(x, y) |
x <= y |
JS.>=(x, y) |
x >= y |
JS.&&(x, y) |
x && y |
JS.||(x, y) |
x || y |
JS.!(x) |
!x |
JS.&(x, y) |
x & y |
JS.|(x, y) |
x | y |
JS.^(x, y) |
x ^ y |
JS["~"](x) |
~x |
JS.<<(x, y) |
x << y |
JS.>>(x, y) |
x >> y |
JS.>>>(x, y) |
x >>> y |
Standard Library
Z's standard library is small, but growing. It contains 15 modules, detailed below. Each module (except the matcher
and constructs
modules) also has its own section, after this one.
Modules in Z's Standard Library:
Template
- A module that performs string templating, complete with encoding functions and nested object templating.Tuple
- An implementation of fixed-size immutable collections (i.e. Tuples) in Z.constructs
- A module containing multiple control flow structures implemented as functions.matcher
- The behind-the-scenes implementation of Z's pattern matching. Not for use. Usematch
expression insteadutf32
- An implementation of Unicode in Z, with proper character indexing, Unicode-aware slicing, and more.actors
- A (primitive) recreation of the Actor Model in Z.F
- A functional utility module that is akin to Rambda.gr
- Utility methods for goroutines.traits
- Traits to derive on enums.Decimal
- A big decimal implementation for Z.Rational
- A bigrational number implementation for Z.Complex
- A complex number implementation for Z.taylor
- Taylor series used to approximate irrational numbers.mercen
- A web scraper that asynchronously fetches a list of all discovered Mersenne primes.scrapy
- A web scraper designed to extract a webpage's text content, and then allow you to execute queries on it.gen
- Generators based of Douglas Crockford's generators in How JavaScript Works.objutils
- Tools for managing object references.
Z's standard library also defined the following macro files:
imperative
- Imperative constructs from languages like Java and Ruby
Template
Template is Z's way to perform advanced string interpolation. To start, you use the Template
constructor to make a Template
. Then, you resolve the template by calling resolve
with data:
importstd Template
def nameTemplate: Template("{{person.name:upper}} is a nice name.", [
"upper": func (str) {
return str.toUpperCase()
}
])
log(nameTemplate.resolve([
"person": [
"name": "Joe"
]
])) # ==> "JOE is a nice name."
Tuple
Z's Tuple module allows you to create Tuples not exceeding 4 elements:
importstd Tuple
def red: Tuple(255, 0, 0)
def green: Tuple(0, 255, 0)
def yellow: ++(red, green)
log(yellow._1, yellow._2, yellow._3) # ==> 255 255 0
Unicode Support
Z's utf32
module allows for basic Unicode support. It exports three things:
utf32.quote
utf32
provides a quote
constant that contains the character "
.
utf32.points(...points)
Creates a string
from the code points specified by points
, then passes that string to utf32.string
.
utf32.string(str)
Returns an immutable ustr
, an immutable string capable of accurately representing Unicode characters. Documentation for methods of ustr
is below:
ustr#type()
Returns "ustr"
ustr#toString()
Returns u"str"
, where str
is a string
consisting of the ustr's
code points.
ustr#toJSON()
Returns the result of calling toString
, but without the "u"
.
ustr#at(index)
Returns a new ustr
representing the code point found at index
ustr#codeAt(index)
Returns the code point found at index
.
ustr#points()
Returns a list of the ustr
's code points.
ustr#concat(other)
Returns the result of concatenating the ustr
with other
by joining their code points.
ustr#length(other)
Returns the amount of code points the ustr
has.
ustr#=(other)
Returns true if the ustr
's code points are equal to other's
code points, otherwise returns false. Coerces arrays
and strings
into ustr
s for comparison.
Functional Programming
Z's F
module has plenty of available functional programming constructs: 99 in fact.
There are too many to cover here in detail, but here is the full list of all the functions the F
module exports:
curry
unary
map
filter
reject
reduce
flatMap
>>
<<
|>
|
prop
invoke
reverse
reduceRight
every
some
constant
add
sub
mul
div
mod
neg
append
cat
inc
dec
T
F
N
U
I
NN
id
predArrayTest
all
any
bothTwo
complement
contains
count
zero
one
allButOne
methodInvoke
startsWith
endsWith
indexOf
find
findIndex
eitherTwo
equals
flatten
forEach
fromEntries
entries
has
head
tail
double
triple
indentical
identity
ifElse
init
isNil
join
keys
last
lastIndexOf
length
max
merge
min
pipe
compose
prepend
propEq
range
sort
sortBy
split
sum
take
takeLast
test
toLower
toUpper
trim
toPairs
toString
unique
values
without
takeWhile
dropWhile
zip
zipWith
Concurrency
In recent versions of Z (0.2.20+) the go
keyword is inferred and you do not explicitly have to type it.
Z implements a dynamic and event-loop based form of Go-style concurrency. To start, all asynchronous actions in Z start with a go
function, short for goroutine
, which is capable of using channels
to perform async
actions:
def main: go func () { # Note the "go" keyword
}
main() # Returns a promise, like an async function.
Now, import the gr
module from the standard library:
importstd gr
def main: go func () {
}
main() # Returns a promise, like an async function.
Use destructuring assignment to get the line
function out of gr
:
importstd gr
def {line}: gr
def main: go func () {
}
main()
Use the get
keyword with line
to get a line from process.stdin
:
importstd gr
def {line}: gr
def main: go func () {
def someLine: get line
log(someLine)
}
main()
That wasn't too hard. Now, let's talk about channels, and how they work. To start, construct a channel with the chan()
function:
def channel: chan()
def main: go func () {
}
main()
A channel can send
and receive
values. The send
function sends values to a channel. The get
keyword blocks in a goroutine
until a value is sent to the channel, where
get
will return the sent value. If there are already values in the channel, get
will give you the first. In this way, channels can act like queues:
def channel: chan()
def main: go func () {
log(get channel) # Logs 3
# Don't forget that get is asynchronous
}
send(3, channel) # Send is synchronous
channel.pending() # Number of values still waiting (not received with get) in the channel
main()
This, for example, uses the gr
module to feed a line to a channel:
importstd gr
def channel: chan()
def main: go func () {
log(get channel) # Logs whatever line you entered
}
gr.line(channel) # Send is synchronous
main()
gr
uses readline
behind the scenes to send
to a channel.
Because chan
is just a normal function, you can return a channel from a function and then proceed to use it in a get
expression. So you can do:
importstd gr
def channel: chan()
def main: go func () {
log(get gr.line()) # gr.line() implicitly creates and then returns a new channel, and then, after you have entered a line into stdin, sends the line to that channel, prompting get to return that line.
}
main()
You can design a custom _from
method that returns a promise to overload the get
operator. This is the custom _from
method defined by gr.line
:
To understand this example you should be familiar with the readline
module in node. If not, check it out here.
import readline
line._from: func JS.new(Promise, func (resolve) {
def rl: readline.createInterface([
"input": process.stdin,
"output": process.stdout
])
rl.question("", func (line) {
rl.close()
resolve(line)
})
})
This is the actual line
function defined by gr
:
import readline
def line: func (prompt: "", ch: chan()) {
def rl: readline.createInterface([
"input": process.stdin,
"output": process.stdout
])
rl.question(prompt, func (line) {
rl.close()
send(line, ch)
})
return ch
}
If you're writing a small script, you can omit the go
wrapper function and use get
at the top level. However, if you are using get
at the top level, the export
statement is not allowed.
Also note that top-level get
does not work too well in the REPL. For example:
import gr
def ln: get gr.line("What's your name?")
log(ln ++ " is a nice name.")
To launch a standalone block of code asynchronously, use go
in statement position:
go {
# You can use "get" in here without turning the outside function into a goroutine.
}
You can also handle failure of asynchronous code that sends a gr.gerror
to a channel, with else
after the get
expression:
get myAwesomeChannel else {
log("Something went wrong " ++ err)
}
Here's a list of all the functions defined by gr
:
gr.gerror(err, ch)
Sends a wrapped error containing err
to ch
.
gr.wrapNodeCB(context, f)
Returns a function that takes any number of arguments. The channel is the last argument. If only one argument is provided, the channel is set to chan()
. Then, f
, bound to context
is called
on the new arguments, and a callback function which sends the result (or error) to the channel.
gr.readfile
Equivalent to gr.wrapNodeCB(fs, fs.readFile)
Example:
get gr.readfile("doodad.txt") # Gets the content of doodad.txt
gr.writefile
Equivalent to gr.wrapNodeCB(fs, fs.writeFile)
Example:
get gr.writefile("doodad.txt", "doodad", ch()) # Writes "doodad" to doodad.txt.
gr.json(url, ch: chan())
Gets the json at the specified url
.
Example:
get json("https://yesno.wtf/api") # Gets a JSON object that contains an answer property equal to "yes" or "no".
gr.page(url, ch: chan())
Gets the HTML at the specified url
.
Example:
get page("https://www.google.com/") # Gets the HTML at google.com.
gr.line(prompt: "", ch: chan())
Reads a line from stdin
, using prompt
as input. If called with no arguments, its parentheses may be omitted. (as in get line
)
gr.wrapPromise(prom, ch: chan())
Wraps prom
so that:
get gr.wrapPromise(prom)
Is like (in JS):
await prom
gr.all(chs, ch: chan())
Like Promise.all
, but for goroutines.
gr.race(chs, ch: chan())
Like Promise.race
, but for goroutines.
gr.status(chs, ch: chan())
With get
, it gives back an array of the results of chs. Each result will contain a state
property that is either
"succeeded"
or "failed"
. If it has succeeded, the result will be inside the result
property. If it failed, the error will be inside the error
property.
gr.any(chs, ch: chan())
Will send the first channel in chs
to succeed, or a list of errors if none succeed.
gr.wait(ms, ch: chan())
Waits ms
milliseconds before sending Symbol()
to ch
.
gr.waitUntil(cond, ch: chan())
Waits until cond()
is true before sending Symbol()
to ch
.
gr.give(ch: chan())
Equivalent to gr.wait(10)
. Useful for passing control between goroutines.
gr.select(chs, ch: chan())
For each element
of chs
, will see which element[0]
resolves first, and when it does, executes element[1](what element[0] resolved to)
get gr.select([
[
first,
func (val) log("First " ++ val) # If first channel recieves a value first.
],
[
second,
func (val) log("Second" ++ val) # If second channel recieves a value first.
]
])
More Numbers
Z's standard library has support for a numerous amount of Number types, which all implement the following methods:
+, r+, -, r-, *, r*, /, r/, =, r=, <, r<
Note that Complex
numbers do not implement <
nor r<
This allows you to use these number types in place of everyday IEEE floating point numbers seamlessly. Because all of these number types rely on BigInt
, they'll only work in Node 10+. Let's start with the Big Decimal number
type:
Big Decimals
Big Decimals are contained within the Decimal
module. Here is a sample of their use:
importstd Decimal
def myBigDecimal: Decimal(3) # Decimals can be ordinary numbers
def myBiggerDecimal: Decimal("3.238142194382154234120973481276") # Decimals can be parsed from strings.
log(myBigDecimal + myBiggerDecimal |> .toString()) # Logs "6.238142194382154234120973481276"
log(myBigDecimal + "0.238142194382154234120973481276" = myBiggerDecimal) # Logs "true". Decimal can be compared for equality. Operations with decimals coerce their operands.
Decimals also support all other common operations that you can do with numbers, so they can be added, subtracted, multiplied, divided, and compared. When doing division with decimals, you can specify a precision to divide to:
decimal1 / decimal2 # Default precision of 20 digits
decimal1./(decimal2, -100digits) # One hundred digit precision
The Decimal
object also defines the following constants and methods, which are equivalent to their Math
object counterparts:
E, LN10, LN2, LOG10E, LOG2E, PI, SQRT1_2, SQRT2, abs, ceil, floor, round, sign, random
Big Rationals
Like Big Decimals, Big Rationals support all the common numerical operations. They are represented with a numerator and a denominator, and they can be constructed via the //
operator:
importstd Rational
def { // }: Rational
operator //: 1000
log(3 // 4 |> .toString()) # Logs "3 // 4"
Common operations with Rationals:
importstd Rational
def { // }: Rational
operator //: 1000
log(2 // 6 = 1 // 3) # true
log(1 // 3 + 1 // 2 |> .toString()) # 5 // 6
log(2 // 3 > 5 // 6) # false
log("0.5" = 1 // 2) # true, because strings containing decimal values can be coerced into Rationals.
In addition to supporting common operations, like Decimal
, Rational
defines several constants and functions akin to their Math
counterparts:
E, LN10, LN2, LOG10E, LOG2E, PI, SQRT1_2, SQRT2, abs, sign, random
Complex Numbers
Complex numbers are essential parts of some mathematical applications. For this reason, Z provides minimal complex number support.
Complex numbers support all basic arithmetic operations and equality checking, however, they do not support <
nor r<
. See an example of complex numbers below:
importstd Complex
log(Complex("12 + 7i") + "8 - 2i" |> .toString()) # 20 + 5i
log(Complex(12, 7) = "12 + 7i") # true
Complex trigonometry may be added at some point in the future.
The Complex
object also comes with a static random
method that generates a random Complex number.
Web Scraping
Z's web scraping module, scrapy
is a powerful way to extract text data in sentence form from webpages. Let's get all the sentences from the Wikipedia page about London:
importstd scrapy
def text: get scrapy("https://en.wikipedia.org/wiki/London")
log(text.sentences()) # This will print out an array of all the sentences on the London Wikipedia page
While there's plenty of sentences about London that pop up, there's also sentences that are completely unrelated. Let's find the first 30 sentences with only the string "London" in them:
importstd scrapy
def text: get scrapy("https://en.wikipedia.org/wiki/London")
text.thingsAbout("London").slice(0, 30).forEach(func log("- " ++ x!))
The thingsAbout
method takes a string which it then coerces into a regular expression, and returns an array of all sentences contained within the source text that match that regular expression.
EDU Modules
Some modules in the Z compiler exist for educational purposes only. They are documented here.
Taylor Series
Taylor series are effective ways for approximating rational numbers. The taylor
module comes with all you need to build Taylor series and several built-in ones. To start, you can create an infinite series via taylor.sum
or taylor.product
:
importstd taylor
def factorial: taylor.prod(1, func x!) # This multiplies all the numbers up to the number specified by the approx function.
factorial.approx(5) # 120 (in Decimal form)
Taylor series can also be used to approximate irrational numbers like e
. In fact, taylor
has a prepackaged e
series:
taylor.E.approx(100tries, -40digits).toString() # "2.7182818284590452353602874713526624977552"
If you're interested in how more irrational numbers and functions can be implemented as Taylor series, check out the source code here.
Mersenne Primes
The mercen
module dynamically fetches a list of all known Mersenne primes for you. A Mersenne prime is a prime number that can be represented as two raised to an integer power minus one. For example:
importstd mercen
def primes: get mercen()
log(primes.count()) # Returns the number of Mersenne primes
log(get primes.number(3)) # Get the third Mersenne prime, which is 31
def aBigOne: get primes.biggest() # Get the largest Mersenne prime in the list
log(aBigOne.slice(0, 10)) # Display the first 10 digits of the biggest Mersenne prime
If you wish, you can verify the answers Z gives you on this website.
Imperative Macros
These macros are designed to model the imperative constructs of other languages. Get access to them with:
includestd "imperative"
They function as follows:
$while cond {
body
}
$unless cond {
body
}
$until cond {
body
}
$do {
body
} while cond
$for lvalue of rvalue {
body
}
Examples
This section compares, Z, CoffeeScript, and JavaScript code side by side in common examples.
Hello World:
Z:
log("Hello World")
CS:
console.log "Hello World"
JS:
console.log("Hello World")
Fibbonaci:
Z:
def fib: func (n)
if (n < 2) n
else fib(n - 1) + fib(n - 2)
CS:
fib = (n) -> if n < 2 then n else fib(n - 1) + fib(n - 2)
JS:
function fib(n) {
if (n < 2) return n;
return fib(n - 1) + fib(n - 2);
}
First 25 Squares:
Z:
log(loop(i <- 1...25) i ^ 2)
CS:
console.log([i ** 2 for i in 1..25])
JS:
const squares = [];
for(let i = 0; i < 26; i++) {
squares.push(i ** 2);
}
console.log(squares)
Parameter Type Checking (Runtime):
Z:
def sum: func (init number!, list array!) {
return list.reduce(+, init)
}
CS:
sum = (init, list) ->
throw new Error("Init must be number") if typeof init isnt "number"
throw new Error("List must be array") if not Array.isArray(list)
list.reduce (t, v) ->
t + v
, init
JS:
function sum(init, list) {
if (typeof init !== "number") throw new Error("Init must be number.");
if (!Array.isArray(list)) throw new Error("List must be array.");
return list.reduce((t, v) => t + v, init);
}
Runtime Polymorphic Functions:
Z:
def double: func (val) match val {
{ value: number! } => v * 2,
{ number: number! } => n * 2,
(number!n) => n * 2,
number! => val * 2
string! => val ++ val,
array! => val ++ val,
_ => [_, _]
}
CS:
double = (val) ->
if Array.isArray(val)
if typeof val[0] is "number"
return val[0] * 2
else
return val.concat(val)
if typeof val is "object"
if val.number isnt undefined
return val.number * 2
else if val.value isnt undefined
return val.value * 2
if typeof val is "string"
return val.concat(val)
if typeof val is "number"
return val * 2
return [val, val]
JS:
function double(val) {
if (Array.isArray(val)) {
if (typeof val[0] === "number") {
return val[0] * 2;
} else {
return val.concat(val);
}
}
if (typeof val === "object") {
if (val.number !== undefined) {
return val.number * 2;
} else if (val.value !== undefined) {
return val.value * 2;
}
}
if (typeof val === "string") {
return val.concat(val);
}
if (typeof val === "number") {
return val * 2;
}
return [val, val];
};
Point Objects via Classes/Enums:
Z:
importstd traits
def {Show, PlusMinus}: traits
enum Point(x: number!, y: number!) derives (Show, PlusMinus) where {
dist(p1, p2) {
return Math.sqrt(-(p1.x, p2.x) ^ 2 + -(p1.y, p2.y) ^ 2)
}
}
CS:
class Point
constructor: (x, y) ->
throw new Error("Point.x must be number") if typeof x isnt "number"
throw new Error("Point.y must be number") if typeof y isnt "number"
@x = x
@y = y
equals: (p) ->
p instanceof Point and @x is p.x and @y is p.y
plus: (p) ->
new Point @x + p.x, @y + p.y
minus: (p) ->
new Point @x - p.x, @y - p.y
toString: ->
"Point(x: #{@x}, y: #{@y}"
@dist: (p1, p2) ->
Math.sqrt (p1.x - p2.x) ** 2 + (p1.y - p2.y) ** 2
JS:
class Point {
constructor(x, y) {
if (typeof x !== "number") {
throw new Error("Point.x must be number");
}
if (typeof y !== "number") {
throw new Error("Point.y must be number");
}
this.x = x;
this.y = y;
}
equals(p) {
return p instanceof Point && this.x === p.x && this.y === p.y;
}
plus(p) {
return new Point(this.x + p.x, this.y + p.y);
}
minus(p) {
return new Point(this.x - p.x, this.y - p.y);
}
toString() {
return `Point(x: ${this.x}, y: ${this.y}`;
}
static dist(p1, p2) {
return Math.sqrt((p1.x - p2.x) ** 2 + (p1.y - p2.y) ** 2);
}
}
Concurrently fetching JSON from an API (on node):
Z:
importstd gr
importstd F
def results: F.map(
func result!.answer,
get gr.all(
Array.from(Array(10), func gr.json("https://yesno.wtf/api"))
)
)
log(results)
CS:
https = require 'https'
results = []
getAnswer = ->
https.get 'https://yesno.wtf/api', (res) ->
body = ''
res.on 'data', (chunk) ->
body += chunk
return
res.on 'end', ->
results.push JSON.parse(body).answer
if results.length is 10
console.log results
for i in [1..10]
getAnswer()
JS:
const https = require("https");
const results = [];
function getAnswer() {
https.get("https://yesno.wtf/api", res => {
let body = "";
res.on("data", chunk => {
body += chunk;
});
res.on("end", () => {
results.push(JSON.parse(body).answer);
if (results.length === 10) {
console.log(results);
}
});
})
}
for (let i = 0; i < 10; i++) {
getAnswer();
}
The Why of Z
Why Z? There are already plenty of functional languages out there that transpile to JS. Z provides an alternative if you're looking for something more multi-paradigm. Some languages take the functional paradigm so far that the imperative version is actually more readable in certain simple cases. For example, let's look at a "Hello World" program in Elm:
import Html exposing (text)
main =
text "Hello World"
This is a relatively simple example. However, you can see some things that don't have anything to do with logging "Hello World"
. First off, the import statement is clutter. Why can't the text
function be built-in,
perhaps? And do we have to place our call to the text
function inside of main
?
These questions all have satisfactory answers. However, in a simple program, it makes a lot more sense to drop the clutter. For example, "hello world" in Z is very simple:
log("Hello World")
In more complex cases, there would be obvious reason to have modules, and a main function. But in simple cases, an imperative script would be easier. You'll encounter the this situation with many different constructs: there are times when loops make more sense than their recursive counterpart and when references are easier than pass-by-value.
Z is about letting you choose between functional and imperative, taking whichever one suits you current needs. If you look through the Z standard library, you'll see that it's predominantly functional on a line-by-line basis, but uses imperative constructs now and then.
Z is about balance. About equilibrium. About letting you create modules just for the fun of it. About letting you choose the paradigm best for the situation.
Contribute
If you want to contribute to Z, you should check out the following links: