-
Notifications
You must be signed in to change notification settings - Fork 82
Language overview
This page tells you the basics of writing programs in Harlan.
Harlan's syntax is based on Scheme's, so if you know Scheme already you'll find a lot of things look familiar. That said, Harlan has some important differences from Scheme, such as its static type system.
Every Harlan program consists of a module and some number of definitions. Below is a very simple Harlan program.
(module
(define (main)
(println "Hello, World!")
0))
This module contains a single definition, main
, which is a function taking no arguments. This is the program's entry point, like the main
function in a C program. The function must return an integer, which is why it ends with 0
. Returning 0
indicates success, just like in C.
Harlan includes several scalar data types, including float
, int
, u64
, char
, and bool
. Numeric types, like float
, int
, and u64
, support the usual set of binary operators, such as (= 1.0 1.0)
to test for equality and (* a b)
for multiplication.
Conversions between types must be explicit, using functions such as (int->float i)
.
One important data structure in Harlan is the vector. This is similar to a vector in Scheme or an array in C and other languages. Here's a simple example of how to create and display a vector.
(module
(define (main)
(println (vector 1 2 3))
0)))
If you run this program, you should see the output [ 1 2 3 ]
.
Creating vectors by hand can be tedious, so one way to create a large vector is to use the iota
form. The name comes from iota
in APL. This form takes an integer as an argument and returns a vector containing the number 0 up to (but not including) the argument to iota
. Here's an example:
(module
(define (main)
(println (iota 5))
0)))
This program should display [ 0 1 2 3 4 ]
.
iota
is a convenient building block for larger parallel computations.
Vectors need not contain only integers; they can contain any Harlan data type, including other vectors.
Kernels are Harlan's primary way of expressing parallelism. Kernels take some number of vectors as inputs and return a vector as output. They are like a parallel version of Scheme's map
. Here's an example of how we can increment every element in a vector by 1.
(module
(define (main)
(let ((xs (iota 5)))
(println (kernel ((x xs))
(+ x 1))))))
Here we've used let
to bind the variable xs
to the result of (iota 5)
. Then, the kernel
expression binds the variable x
to each value in xs
and runs the body, (+ x 1)
. The final result is a new vector containing all the intermediate results. This program should print out [ 1 2 3 4 5 ]
.
Kernels can work on multiple vectors at the same time. For example, here's how to add two vectors together:
(module
(define (main)
(let ((xs (vector 5 4 3 2 1))
(ys (vector 1 2 3 4 5)))
(println (kernel ((x xs) (y ys))
(+ x y)))
0)))
Here x
is bound to an element in xs
and y
is bound to the corresponding element in ys
. The program displays [ 6 6 6 6 6 ]
, since each pair (5 and 1, 4 and 2, etc.) adds up to 6.
Harlan will do its best to run the kernel on the GPU. The block size and other parameters to the kernel are automatically determined by Harlan. Harlan also automatically manages copying data back and forth between the CPU and GPU memory.
It's often useful to combine a vector of values into a single value. Harlan provides the reduce
operation to do this. Reduce takes an operator and a vector and uses the operator to combine all of the elements in the vector into a single result. For example, here is how to add up all the numbers in a vector:
(module
(define (main)
(reduce + (vector 1 1 1))
0))
As you'd expect, this program displays 3
.
The reduction operator can be any function that takes two arguments, but for correctness it should be both commutative and associative.