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
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 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.
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:
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 enum
s. 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:
- Is
val
undefined
? If so, return"undefined"
. - Is
val
null
? If so, return"null"
. - Does
val
have a function? Does that function return astring
? 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 algorithim:
- 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 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:
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 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. Usematch
expression insreadutf32
- 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 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 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
No comments:
Post a Comment
Note: Only a member of this blog may post a comment.