Kal
Kal is a highly readable, easy-to-use language that compiles to JavaScript. It's designed to be asynchronous and can run both on node.js and in the browser. Kal makes asynchronous programming easy and clean by allowing functions to pause and wait for I/O, replacing an awkward callback syntax with a clean, simple syntax.
For an overview, see the Github page or check out the examples.
Kal is expressive and offers many useful synonyms and constructs to make code readable in almost plain English.
Kal is designed with a unique philosophy:
- Eliminate the yucky parts of JavaScript, but keep the good stuff including the compatibility, and the great server and client runtime support.
- Make code as readable as possible and make writing code straightforward. Eliminate the urge (and the need) to be terse and complicated.
- Provide an alternative to callbacks (which look weird) and promises (which are weird) while providing excellent, easy-to-use asynchronous support.
Check out the examples for some sample use cases.
Installation Using npm
This is the preferred method for installing Kal. Make sure you have installed node.js. Kal works with versions 0.6, 0.8, and 0.10. It might work with other versions as well. Install the latest "stable" release of Kal using npm:
sudo npm install -g kal
sudo
may not be required depending on how you installed node
.
Syntax Highlighting
A TextMate bundle for TextMate and Sublime Text is available with limited but very useful support for Kal's syntax.
Vim support is also available thanks to @bcho.
Help and Support
Visit the Google group to ask questions and interact.
File an issue on Github or send a pull request if you have something to add!
Installing from the Repository
If you need the latest and greatest (possibly unstable/broken) build, you can build Kal manually. Most users can skip this section and just use the latest npm
version.
Kal is written in Kal, so you need a prebuilt version of the compiler available to do the initial build:
sudo npm install -g kal
Then you can clone the repo, install the developer dependencies, and build the compiler:
git clone https://github.com/rzimmerman/kal kal
cd kal
npm install
npm run-script make
Run the tests to make sure everything is going well:
npm test
If you're extra serious, you can use your new build to rebuild itself in case there were any notable changes to the compiler between the npm release and the latest commit. This will also run the tests.
npm run-script bootstrap
Now install your latest version using npm:
npm pack
Assuming the tests pass, this will make an archive file that you can install (the filename depends on the version):
sudo npm install -g kal-0.x.x.tgz
Alternatively you can just run the scripts/kal
file if you don't want to install it globally.
Usage
If you installed Kal globally (using the -g
option), you can run the interactive shell by running kal
with no arguments.
$ kal
kal> 'hello' + ' ' + 'world'
'hello world'
You can use the kal utility to run or compile files. Run kal -h
for the full option set. If you installed kal locally (didn't use the -g option), you will need to specify the path to the kal executable, usually located at node_modules/kal/scripts/kal
.
kal path/to/file.kal --runs the specified file
kal -o path/for/output path/to/file1.kal path/to/file2.kal ... --compiles all files/directories listed to javascript
and writes the output into the folder specified by -o
Using the -j
or --javascript
switches will show the output of the compiler.
If you import Kal in your Javascript code, it installs a compile hook that allows you to directly import .kal files:
require('kal');
require('./mykalfile'); //refers to mykalfile.kal
Literate Kal
Literate Kal offer an exciting way to write well-documented, readable code. The idea is based on Literate CoffeeScript. The compiler will treat any file with a .litkal
or .md
extension as a Markdown document. Code blocks, denoted by four spaces of indentation, are treated as Kal code while anything that is not indented is treated as a comment. See this example of a Literate Kal file. New in 0.5.2.
As of 0.5.4, Kal is written in Literate Kal.
Whitespace and Indentation
In Kal, spaces for indentation are significant and tabs are not valid. Indents are required for function definitions and blocks of code inside of if
statements, try
/catch
blocks, and loops.
You should use two spaces to denote an indent. You can technically use any multiple of two spaces, but two is recommended as a style guideline. Any whitespace on blank lines is ignored. Semicolons at the end of statements are not required nor are they valid.
In general single statements cannot contain line breaks. Notable exceptions are list and object definitions. For example:
a = [1, 2,
3,
4]
b = {a:1
c:2}
Will work, however:
a = 1 +
1
Is invalid. Future versions may include better support for line breaks within statements.
Comments
Comments are preceeded by a #
sign. Anything after the #
on the line will be ignored.
print 5 #this is a comment
Multiline comments are enclosed by ###
:
###
A multiline
comment
###
Functions and Tasks
Functions are defined with an optional name and a list of arguments.
function my_function(arg1, arg2)
return arg1 + arg2
and
my_function = function (arg1, arg2)
return arg1 + arg2
Both define a variable my_function
that takes two arguments and returns their sum. CoffeeScript syntax is also valid:
my_function = (arg1, arg2) ->
return arg1 + arg2
But is generally discouraged unless it significantly helps readability. It was originally included to ease porting of the Kal compiler from CoffeeScript to Kal. Coffee-style functions must contain a line break after the ->
. =>
is not supported.
Functions can have default arguments. These will be used if the specified argument is null
or undefined
:
function default_args(x,y=2)
return x + y
print default_args(1) # prints 3
Functions are called using parentheses.
my_function(1, 2)
Will return 3
. Parentheses are optional if the function has at least one argument:
my_function 1, 2
Is also valid. Function calls can be chained this way as well, so any of the following
print(my_function(1,2))
print my_function 1, 2
print my_function(1, 2)
will all print 3
. When calling a function with no arguments, parentheses are required.
Tasks are similar to functions, except that they are intended to be called asynchronously (usually using a wait for
statement).
task my_task(arg)
return arg * 2
or
my_task = task (arg)
return arg * 2
Tasks should not be called synchronously. If a task is called synchronously, it will return with no value. When called asynchronously.
print my_task 1
Is valid syntax, but will print undefined
.
wait for x from my_task(1)
print x
Will print 2
as expected. See the wait for
section for more details on asynchronous calls.
Objects and Arrays
Objects and arrays are defined similarly to JavaScript. Newlines are valid inside of an array or object definition and indentation is ignored. Commas are optional when followed by a newline. CoffeeScript-style object definitions (no {}
s) are only valid in assignments and must be preceded by a newline.
a = [1, 2, 3]
b = [1
2,
3
4]
c = {a:1,b:2,c:{d:3}}
d =
a:1, b:2
c:
d:3
Function definitions are only valid in CoffeeScript-style object definitions at this time.
d =
a:1, b:2
c:
d: function ()
return 2
e: ->
return 3
Objects work like JavaScript objects (because they are JavaScript objects), so you can access members either using array subscripts or .
notation
x =
a : 1
b : 2
print x['a'] #prints 1
print x.b #prints 2
Scoping
Variables are declared automatically and scoped within the current function unless used globally (like CoffeeScript).
By default, nothing in your .kal
file will leak to the global scope. Everything is wrapped within a function scope inside the module. If you need to export variables to global scope, you should use
module.exports.my_export = my_variable_or_function # in node.js
window.my_export = my_variable_or_function # in a browser
or you can compile the file with the --bare
option.
Conditionals
The following defines a conditional statement:
x = 5
if x is 5
print 'five'
will print five
.
x = 6
if x is 5
print 'five'
else
print 'not five'
will print not five
The conditional has useful synonyms:
when
is equivalent toif
unless
is equivalent toif not
except when
is equivalent toif not
otherwise
is equivalent toelse
So the following is valid, as are other permutations:
unless name is 'Steve'
print 'Impostor!'
otherwise
print 'Steve'
else
(and synonyms) can be chained with if
(and synonyms), so
if name is 'Steve'
print 'Steve'
else if name is 'Brian'
print 'Brian'
otherwise when name is 'Joe'
print 'Joe'
else
print 'Somebody'
is valid.
Conditionals can also tail a statement:
print 5 if 5 > 10
will do nothing.
Conditionals can be used in a ternary statement as well
print(5 if name is 'Joe' otherwise 6)
will print 5
if the variable name is equal to 'Joe'
, otherwise it will print 6
. Kind of like it says. Parentheses are required because tail conditionals associate right, meaning the following are equivalent:
print 5 if name is 'Joe' otherwise 6
(print(5) if name is 'Joe') otherwise 6
Loops
for
loops work as follows:
for x in [1,2,3]
print x
Will print the numbers 1, 2, and 3. The value n the right of the for ... in
expression is called the iterant
. Currently it must be an array. Python-like iterable object support is coming soon.
for ... in
loops can also have an index variable:
for x at index in [10,20,30]
print index, x
Will print the 0 10
, 1 20
, 2 30
.
for
loops can also be used on objects:
obj = {a:1,b:2}
for key of obj
print key, obj[key]
Will print a 1
and b 2
.
When used on asynchronous code, the parallel
and series
specifiers are available:
for parallel x in y
wait for z from f(x)
for series x in y
wait for z from f(x)
series
is the default if neither is specified. Parallel for loops are not guaranteed to execute in order! In fact, they often won't. Take special care when accessing variables separated by wait for
asynchronous statements. Remember that a wait for
releases control of execution, so other loop iterations running in parallel may alter local variables if you are not careful. See the wait for
section for more details.
You can use a generator object as follows:
for val from generator_object
print val
A generator object is any object that has a next()
method. The loop will keep calling the next()
method until it does not return a value (returns undefined
in JavaScript-speak).
You can also use a range of numbers as an implicit generator to save memory:
for val from 1 to 100000000 # will not instantiate a giant list
print val
while
loops continuously run their code block until a condition is satisfied.
x = 0
while x < 5
x += 1
print x
prints the numbers 1 through 5. until
provides a similar function.
x = 0
until x is 5
x += 1
print x
Comprehensions
List comprehensions are a quick and useful way to create an array from another array:
y = [1,2,3]
x = [value * 2 for value in y]
will set x
equal to [2,4,6]
. Comprehensions also support an iterable object. Iterable objects support a next()
method which returns the next value in the sequence each time it is called. When there are no more values in the sequence, it should return null
.
class RandomList:
method initialize(size)
me.counter = size
method next()
me.counter -= 1
if me.counter >= 0
return Math.random()
else
return null
x = [r * 10 for r in new RandomList(20)]
will set x
to an array of 20 random numbers between 0 and 10.
List comprehensions work on objects, too:
obj = {a:1, b:2}
x = [p for property p in obj] # ['a','b']
x = [v for property value v in obj] # [1, 2]
x = [p+v for propert p with value v in obj] # ['a1', 'b2']
Conditionals in list comprehensions are also supported:
new_list = [i for i in old_list if i > 5]
Operators And Constants
Listed below are Kal's operators and their other-language equivalents. Note that Kal has a lot of synonyms for some keywords, all of which compile to the same function.
Kal | CoffeeScript | JavaScript | Function |
---|---|---|---|
true , yes , on |
true , yes , on |
true |
Boolean true |
false , no , off |
false , no , off |
false |
Boolean false |
and , but |
and |
&& |
Boolean and |
or |
or |
|| |
Boolean or |
nor |
none | none | Boolean or, inverted |
not |
not , ! |
! |
Boolean not |
xor , bitwise xor |
^ |
^ |
Bitwise xor |
bitwise not |
~ |
~ |
Bitwise not (invert) |
bitwise and |
& |
& |
Bitwise and |
bitwise or |
| |
| |
Bitwise or |
bitwise left |
<< |
<< |
Bitwise shift left |
bitwise right |
>> |
>> |
Bitwise shift right |
+ , - , * , / , mod |
+ , - , * , / , % |
+ , - , * , / , % |
Math operators |
^ |
none | none | Exponent (Math.pow ) |
exists , ? |
? |
none | Existential check |
doesnt exist |
none | none | Existential check (inverted) |
is , == |
is , == |
=== |
Boolean equality |
isnt , is not , != |
isnt , == |
!== |
Boolean inequality |
> , >= , < , <= |
> , >= , < , <= |
> , >= , < , <= |
Boolean comparisons |
me , this |
@ , this |
this |
Current object |
in , not in |
in , not in |
none | Boolean search of array/string |
of |
of |
in |
Boolean search of object |
nothing , empty , null |
null |
null |
Null value |
undefined |
undefined |
undefined |
no value |
instanceof |
instanceof |
instanceof |
inheritance check |
print |
console.log |
console.log |
alias for console.log |
Exisential Checks
Kal implements the same existential operator features of CoffeeScript, with the addition of the exists
and doesnt exist
keyword suffixes, which perform the same function as the ?
operator. Examples:
a = {a:1}
b = [1,2,3]
print(c exists) # false
print(c doesnt exist) #true
print(c?) #false
print(c.something) #throws an error!
print(c?.something) #prints undefined
print(a?.a) # 1
print(a?.a?) # true
print(b?[2]) # 3
print c() # error!
print c?() # prints undefined
Classes and Inheritence
Classes are defined with member method
definitions. Methods are just functions that are added to the prototype of new instance objects (in other words, they are available to all instances of a class). The initialize
method, if present, is used as the constructor when the new
keyword is used. me
(or its synonym this
) is used in methods to access the current instance of the class. instanceof
checks if an object is an instance of a class.
class Person
method initialize(name)
me.name = name
method printName()
print me.name
method nameLength()
return me.name.length
jen = new Person('Jen')
jen.printName() # prints 'Jen'
print(jen instanceof Person) # prints true
Classes can inherit from other classes and override or add to their method definitions. The super
keyword can be used in a method to call the same function in the parent class.
class FrumpyPerson inherits from Person
method printName()
print 'Frumpy ' + me.name
method nameLength()
return 0
sue = new FrumpyPerson('Sue')
sue.printName() # prints 'Frumpy Sue'
print(sue instanceof Person) # prints true
print(sue instanceof FrumpyPerson) # prints true
print(jen instanceof FrumpyPerson) # prints false
You can add or alter a method or task to a class after it is defined (or from another file) using late binding using the of
keyword.
class MyClass
method my_method(v)
me.v = v
x = new MyClass()
x.my_method(10)
print x.v # prints 10
method my_method(v) of MyClass
me.v = v + 1
method my_other_method() of MyClass
print me.v
x = new MyClass()
x.my_method 10
x.my_other_method() # prints 11
Try/Catch
try
and catch
blocks work similarly to JavaScript/CoffeeScript. finally
blocks are not supported yet but are coming eventually. The throw
statement (and its synonyms raise
and fail with
) work like JavaScript as well.
try
a = 'horse' / 2 # what are we doing? this will throw an error!
b = 1 # never runs
catch e
print 'caught it!', e
The e
variable above stores the error object thrown with throw
or by the system. You can give it any name you want and it is optional. The following is valid:
try
throw 'a string'
b = 1 # never runs
catch
print 'caught it!'
try
/catch
blocks can be nested. try
blocks can contain asynchronous wait for
calls, but catch
blocks cannot at this time.
Strings
Strings can either be double-quoted ("
) or single quoted ('
). Backslashes can be used to escape quotes within strings if necessary.
x = 'this is a "string" with quotes in it'
y = "so is 'this'"
z = 'this one is, \'too\' but I\'m not proud of it'
String Interpolation
Double-quoted strings can contain interpolated values using #{...}
blocks. These blocks can contain any valid Kal expression (including variables and function calls). This is the recommended way to do string concatenation as it is usually more readable.
print "This is a string with the number 3: #{1+1+1}"
"This is a string with the number 3: 3"
a = cow
n = moo
print "The #{a} says #{n}, #{n}, #{n}!"
"The cow says, moo, moo, moo!"
Regular Expressions
Kal supports JavaScript's regex syntax, but not CoffeeScript style block regex syntax.
Asynchronous Wait For
The wait for
statement executes a task
and pauses execution (yielding to the runtime) until the task
is complete. The following reads a file asynchronously and prints its contents (in node.js).
fs = require 'fs'
wait for data from fs.readFile '/home/user/file.txt'
print data.toString()
Note that:
- For users familiar with node.js and JavaScript,
fs.readFile
is called with the file name argument and a callback. You don't need to supply a callback. - After the
wait for
line, execution is paused and other code can run. Keep this in mind if you have global variables that are modified asynchronously as they may change between thewait for
line and the line after it. - Any errors reported by
fs.readFile
(returned via callback) will be thrown automatically. You should wrap thewait for
in atry
/catch
if you want to catch these errors.
wait for
can be used to call your own asynchronous tasks. It can also be used within for
and while
loops, try
blocks, if
statements, and any nesting combination you can think of. Really!
fs = require 'fs'
task readFileSafe(filename)
if 'secret' in filename
throw 'Illegal Access!'
else
wait for d from fs.readFile filename
return d
for parallel filename in ['secret/data.txt', 'test.txt', 'test2.txt']
try
wait for data from readFileSafe '/home/secret/file.txt'
print data.toString()
catch error
print "ERROR: #{error}"
print 'DONE!'
wait for
can also be used without arguments by omitting the from
keyword
wait for my_task()
Some node.js API functions (like http.get
) don't follow the normal convention of calling back with an error argument. For these functions you must use the safe
prefix, otherwise it will throw an error:
http = require 'http'
safe wait for request from http.get 'http://www.google.com'
print request.responseCode
wait for
statements also support multiple return values
wait for a, b from my_task()
Asynchronous Pause
You can pause for a specified amount of time using the pause for
keyword
print 'starting'
pause for 1 second
print 'done!'
pause for
uses JavaScript's setTimeout
function. Note that the argument is in seconds, not milliseconds like setTimeout
.
The second
keyword is optional. seconds
also works. Use your best judgement to keep your code readable.
pause for 2 # seconds is implied
pause for 10 seconds # also valid
milliseconds = 1293
pause for milliseconds/1000 seconds # expressions are valid for the timeout
Parallel Tasks
You can kick off tasks in parallel with the run in parallel
block
run in parallel
task1()
wait for task2 a, b, c
wait for x from task3()
safe wait for y, z from task4()
print 'all tasks finished'
Code after the run in parallel
block will not run until all tasks have completed. If any errors are thrown by one or more tasks, an array of errors will be thrown after all tasks in the block complete (or fail). Array elements are in the order that the tasks were specified. If no error was thrown by a task, its error element will be undefined
(doesnt exist
will be true). safe
waits will not check for errors.
try
run in parallel
task_that_fails()
task_that_succeeds()
catch errors
print errors[0] # prints the error thrown by task_that_fails
print errors[1] exists # prints false
Unpacking Objects
A shorthand syntax for the following:
a = myobj.a
b = myobj.b
c = myobj.sea
d = myobj.subprop.d
is available with the unpack into
statement:
unpack myobj into a, b, sea as c, subprop.d as d