Pureli

A purely functional, dynamically typed, parallel evaluated, lisp-like programming language

Quick Start

How To Install

Using stack

stack install

Using cabal

Install GHC 7.10.* and cabal and run the following commands:

git clone https://github.com/soupi/pureli
cd pureli
cabal sandbox init
cabal install

How To Run

Execution

To run the pureli REPL, write:

pureli

To run a pureli program, write:

pureli <filepath>

Parallelism

To run with parallel execution, add +RTS -N<n> to the end of an execution command, where is the number of core you would like to use

Adding a -s flag will print run time information at the end of the execution.

Example:

pureli examples/exponent.pli +RTS -N4 -s

Or

pureli examples/exponent.pli +RTS -N2

Introduction

Pureli is a purely functional, dynamically typed, parallel evaluated, lisp-like programming language. These are all very big words, so let's analyze them one by one:

We will continue to explore these qualities that makes Pureli interesting and unique in future chapters.

Out of the Frying Pan and into the Fire

Lets start by learning about Pureli's Atomic Expressions.

Atomic Expressions

Atomic expressions are expressions that cannot be evaluated any further. In Pureli, you can find:

Syntax

XKCD 297

Pureli derives it's syntax from LiSP, a family of programming languages with similar syntax and attributes.

LiSP, as it's name, is about List Processing. In lisp, everything is either an atomic expression or a list, which is denoted by parenthesis.

For example:

(this is a list)

Lists can be nested:

(this is a (list inside (a list)))

Lisp uses polish notation. When evaluated, the first argument serves as a command and the rests are arguments:

(+ 32 1 942 444)

Expressions can be nested as well:

(+ 32 1 942 (+ 111 222 1 110))

To stop an expressions from evaluating we use the command quote:

(quote (+ 2 2 3)) ;; => '(+ 2 2 3)

It's result will be a data structure with these arguments, which can be evaluated using eval.

(eval (quote (+ 2 2 3))) ;; => 7

Since the result of quote expression is a data structure, a list, we can use list operations such as car, cdr, and ++ to manipulate it:

(eval (++ '(*) (cdr (quote (+ 2 2 3))))) ;; => 12

In Pureli (and Lisp), code is data!

Defining Things

We use define to bind names to expressions:

(define x (+ 1 2 3))

We can create our own anonymous functions using lambda:

((lambda (x y) (+ x y)) 2 3) ;; => 5

If we want to create a function and give it a name, we can use define:

(define f
  (lambda (x y) (+ x y)))

Or use another form of define which is just a syntactic sugar:

(define f (x y)
  (+ x y))

define can only be used to introduce a name at the top level of a module, which will make it available to the entire module.

For local definitions, We can use let and letrec:

(let ([x 5] [y 1]) (+ x y)) ;; => 6

(letrec ([loop (lambda () (loop))]) (loop)) ;; => <infinite-loop>

(let ([loop (lambda () (loop))]) (loop)) ;; => error. cannot find loop in environment

Unevaluated parameters

It is also possible to define functions with receives unevaluated parameters

(module main)

(define unless (test ~true-branch ~false-branch)
  (if
    test
    false-branch
    true-branch))

(define main
  (println!
    (unless
      #t
      (error "not thrown")
      (+ 1 1)))) ;; => 2

Functions with Unevaluated parameters are useful when we want to expand the language and are Pureli's alternative for simple macros.

Modules

Definition

It is possible to define multiple modules per file. In order to run a file, a 'main' module must be present.

Syntax: (module <name> ?(<exported definitions>))

Requires

It is possible to import a source file using the require keyword at the top of the module. Cyclic imports are currently not allowed.

Syntax: (require <filepath> <module name> ?(<imported definitions>) <?new name>)

Or when required module is in the same file: (module <name>)

I/O

I/O handling in Pureli might be a bit different from what you are used to. Since most functions in pureli are pure, the cannot have side-effects. This means that you need to separate pure computations from impure computations in your code, this is how you do it:

Example:

(module main)

(define main
  (do!
    (println! "calculating the sum of 1, 2 and 3:") ;; println! is impure
    (let! result (pure (+ 1 2 3))) ;; pure lifts (+ 1 2 3) into impure context and let! binds it to result
    (println! result))) ;; println! prints the result
                                          

This separation is useful for a number of things:

Built-in Procedures

Built-in IO Actions

Standard Library Modules

Use (require "stdlib/std.pli" <module-name>) to import modules from the standard library.

Examples

Code examples for Pureli can be found here.