Z – A dynamic functional programming language that compiles to JavaScript

Z is a transpiled language that is for making small applications on the backend*. Z is dynamic and multi-paradigm. However, it leans towards dynamic functional programming. Dynamic functional programming balances the readability of the declarative with the familiarity of the imperative. In Z, you can still cause side effects and create impure functions without dealing with monads or working your way through atoms. However, you can also define your own operators, easily create lambdas, partailly apply functions (and operators), define algebraic data types, employ pattern matching, and create highly functional code. Z leaves how far you go with FP up to you. On top of this, even though Z has it's own runtime, it uses the same data types as JavaScript, allowing for (almost) seamless iterop. In addition, you can leverage the power of Z's (small, but still growing) standard library to further enhance your code. So what are you waiting for? 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.

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:


$ sudo npm install -g @zlanguage/zcomp

Now, in order for the compiler to function, you must also install

globby

:


$ sudo npm install -g globby

Or, with Yarn:


$ sudo yarn global add @zlanguage/zcomp 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 Underscores
  • 0x100 Hexadecimal, Binary, and Octal prefixes!
  • 10bytes Trailing characters
  • 1.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 it's 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

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 iternal value can still change:


const 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 satements 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 it's 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." )

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

Deault 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 ommited:


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 throw 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 implcitly 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 it's "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-seperated 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 certian 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 of 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, comparision, 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.

OperatorAssociativityPrecedenceFunctionLeft OverloadRight Overload
powRightInfinityPerforms exponentiationNA (overload * instead)NA (overload r* instead)
tilLeft555Exclusive rangeprev & succ & <NA
toLeft555Inclusive rangeprev & succ & <NA
byLeft444Used to specify the step of rangesNANA
^Left333Performs exponentiationNA (overload * instead)NA (overload r* instead)
%Left222Performs modulus%r%
/Left222Performs division/r/
*Left222Performs multiplication*r*
+Left111Performs addition+r+
-Left111Performs subtraction-r-
++Left111Performs concatenationconcatNA
>>Left1Left-to-right compositionNANA
<<Left1Right-to-left compositionNANA
|>Left1PipeNANA
<Left-111Less-than<r<
<=Left-111Less-than or Equal-toNA (Define < instead)NA (Define r< instead)
>Left-111Greater-thanNA (Define < instead)NA (Define r< instead)
>=Left-111Greater-than or Equal-toNA (Define < instead)NA (Define r< instead)
=Left-222Compares Structural Equality=r=
andLeft-333And boolean comparisonNANA
orLeft-333Or boolean comparisonNANA

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:


def add: func x! + y! 3 add 4 # ==> 7

At parse time, long chains of operators are transformed back into invocations. To start, operators than do not consist only of symbols cannot have precedence: they are evauluated before any other operators in the chain, and are right associative:


def add: func x! + y! 3 add 4 * 2 # ==> 11, not 14

However, operators that are defined using all symbols 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(+)

Dollar Directives

Dollar directives are Z's form of compile-time reflection. To start, let's talk about compile-time metadata:

Metadata:

Metadata is declared with the meta keyword. It is known only at compile time, not at runtime. It's used to alter the behavior of dollar directives. Example:


meta something: "cool" # Metadata must be strings only meta config: "also cool" # config isn't available at runtime.

Now Introducing: Dollar Directives

First, specify the metadata for ddsdir, the directory where the dollar directives will be coming from:


meta ddsdir: "../dollar-directives"

A dollar directive is fed an AST of the next expression or statement at compile-time, meaning dollar directives must be compiled before hand. For example, the following dollar directive makes a for-of statement (sort of) possible:


export func(forloop) { def assignment: forloop[0] def body: forloop[1] body.zeroth.push(assignment.zeroth) return [ "type": "refinement", "zeroth": assignment.wunth, "wunth": "forEach", "twoth": [ "type": "invocation", "zeroth": "forEach", "wunth": [body] ] ] }

Assuming that dollar directive was in the same directory as the following example, you could use it like:


$for (x: [1, 2, 3]) { log(x) }

While making dollar directives does require advanced knowledge of how the Z Compiler works, using dollar directives is pretty easy, and can make your code more readable.

Dollar directives can take an optional second parameter, all the metadata defined up to the point where the dollar directive was called.

Arrays denoted with () that are followed by a block just add a function containing the block to the end of the array, as seen in the example above.

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 dosen'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 enums. Enums in Z aren't a special new kind of type, they're just a special way to define ceratin 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 it's 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 it's 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 wiil 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 constructor's ust 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 recieve 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 consructor 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 primtive. Otherwise, returns false.

typeOf(val)

Returns the type of val according to the following algorithim:

  1. Is val undefined? If so, return "undefined".
  2. Is val null? If so, return "null".
  3. Does val have a function? Does that function return a string? If so, return the result of calling val's type function.
  4. Is val NaN? If so, return "NaN".
  5. Does Array.isArray return true for val? If so, return "array".
  6. Return the result of calling typeof on val.

typeGeneric(val)

Returns the type of val according to the following algorithim:

  1. Does val define typeGeneric function? Does that function return a string? If so, return the result of calling that function.
  2. Is val an array? If so, return a string in the format "array", where types is equal the result of joining a unique set of calling typeGeneric on all the elements in val with "|"

stone(val)

Returns the result of recursively calling Object.freeze on an object and it's properties. Returns the object, which is now deeply immutable.

throws when passed a circular data structure.

copy(val)

Returns a deepy 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")

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:

MethodJS 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 7 modules, deatiled below. Each module (except the matcher and constructs modules) also has it's 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 (ie. 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. Use match expression insread
  • utf32 - 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.

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(" 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 ustrs 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 recieve 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 recieved 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() implicity 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 in the REPL. For example:


import gr def ln: get gr.line("What's your name?") log(ln ++ " is a nice name.")

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, it's parentheses may be ommited. (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. ] ])

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(); }


from Hacker News https://ift.tt/2Y7fpW4