A purely functional, dynamically typed, parallel evaluated, lisp-like programming language
stack install
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
To run the pureli REPL, write:
pureli
To run a pureli program, write:
pureli <filepath>
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.
pureli examples/exponent.pli +RTS -N4 -s
Or
pureli examples/exponent.pli +RTS -N2
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:
(if #t 1 "hello")
possible to run.We will continue to explore these qualities that makes Pureli interesting and unique in future chapters.
Lets start by learning about Pureli's Atomic Expressions.
Atomic expressions are expressions that cannot be evaluated any further. In Pureli, you can find:
123
, 0
, -55192293384175798123471
)12.0
, -51.5
, 0.01432
)#t
for true, #f
for false. In Pureli every value other than #f
is #t
."Hello world!"
, "Pureλi is Great!"
)x
, lines->str
, println!
) are symbols that evaluates to variables and functions.:hello
, :x
, :always-start-with-colons
)void
, Unit
or ()
in other languages. (nil
, ()
)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!
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
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.
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>))
(<exported definitions>)
will only export definitions listed. Optional.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>)
(<imported definitions>)
will only imported definitions listed. Optional.<new name>
will give the module a new name. Optional./
.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:
main
in the module main
, starts as impuredo!
pure
let!
is used to bind a result from impure context to a nameExample:
(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:
+
, -
, *
, /
, mod
)zero?
, empty?
, nil?
, number?
, integer?
, real?
, list?
. string?
, procedure?
, symbol?
, keyword?
)=
, <>
, >
, <
, >=
, <=
)list
, car
, cdr
)++
, slice
, length
)str->lines
, str->words
, lines->str
, words->str
, to-upper
, to-lower
)round
operation on realsshow
expressionif
expressionlet
and letrec
quote
, eval
and read-str
error
, try
and trace
lambda
expressiondo!
sequence IO actionslet!
binds an IO result to a variableread!
reads a line from the standard inputread-file!
reads a fileprint!
writes to the standard output without newlineprintln!
writes to the standard output with newlineprint-file!
writes a string to a filepure
raises a pure computation into IO contextstd
list
bool
numbers
dict
Use (require "stdlib/std.pli" <module-name>)
to import modules from the standard library.
Code examples for Pureli can be found here.