Lokke intends to provide a full dialect of Clojure
for Guile. It also consists of a set of Guile modules providing some
of Clojure’s functionality in two different guises.
Note: everything is currently very experimental, including the
various API “levels” — they may not all survive, depending in part
on whether or not they end up seeming valuable enough to be worth
the maintenance costs.
For Clojure itself
The Clojure dialect is currently available via ./lokke
which can run
code, serve as a !#
interpreter, or provide a REPL. See
below for further information.
For a more Clojure oriented experience in Scheme
Lokke provides one set of modules, starting with (lokke core)
that
create an environment that looks suspiciously like Clojure. In most
cases these modules prefer Clojure’s approach when there’s a conflict,
and they’re not shy about using generic functions. For example, =
is a generic, and implements Clojure’s semantics, not Scheme’s, and
currently (lokke core)
and (lokke base syntax)
, will replace let
and when
, via use-modules
, so be cautious. We recommend following
the Clojure practice of explicitly #:select
ing the symbols you want
to import, (or #:prefix
ing the namespace) to avoid the problem
entirely, and to make it much easier to discover the origin of a
binding. In some cases, Scheme vectors may be required in place of
Clojure’s immutable vectors, perhaps something like (let #(x 1 y 2) ...)
, though the approach to these binding forms on the Scheme side
is still experimental.
For a more Scheme oriented experience in Scheme
Lokke may also provide (lokke scm ...)
modules, which define a more
Scheme-friendly, and possibly more efficient interface — think
(lokke-vector-length v)
as compared to (count v)
. Perhaps the
most notable existing module is (lokke scm vector)
which intends to
provide a C backed implementation of Clojure’s persistent vectors.
Getting started
Currently Lokke can be found at
GitHub and
sourcehut.
To build Lokke, you’ll need
Your system may already provide these. For Debian, for example:
# apt install autoconf automake libpcre2-dev libunistring-dev
# apt install make gettext gcc git
and then for Guile:
# apt-get install guile-3.0 guile-3.0-dev
See INSTALL for additional platform-specific information.
Once you have the dependencies installed, you should be able to build
lokke like this:
$ git clone https://github.com/lokke-org/lokke
$ cd lokke
$ ./setup
$ autoreconf -fi
$ ./configure
$ make check
Hopefully the tests will pass. If not, please report them to the
Lokke list. Parallel builds are supported, so
depending on the host, something like make -j5 check
can be much
faster.
If you have more than one version of Guile installed, you may be able
to select a particular version at configuration time like this:
$ ./configure GUILE_EFFECTIVE_VERSION=3.0
unless your platform requires other arrangements, which should be
mentioned in the relevant section in INSTALL.
After building, you can run Clojure programs like this:
$ ./lok -l hello.clj
...
hello
or the REPL like this:
./lok ...
is equivalent to ./lokke run ...
.
Currently the Lokke REPL is the Guile REPL, with the initial
language and environment set for Lokke, and so all of the Guile
features should be available. For now, lokke
loads
$XDG_CONFIG_HOME/lokke/interactive.scm
if $XDG_CACHE_HOME
is set,
otherwise ~/.config/lokke/interactive.scm
rather than ~/.guile
.
See ./lokke --help
or man -M . lokke.1
for additional information.
A plain text version of the manual page is also
available.
Assuming your guile was compiled with readline support, it’s likely
you’ll want to add something like this to
~/.config/lokke/interactive.scm
:
;;; -*-scheme-*-
(use-modules (ice-9 readline))
(activate-readline)
The REPL history will be stored in the file indicated by the
environment variable LOKKE_HISTORY
if set, otherwise
$XDG_CACHE_HOME/lokke/history
if $XDG_CACHE_HOME
is set, otherwise
~/.cache/lokke/history
.
There is also a ./guile
wrapper which just runs Guile with the
correct environment for Lokke (and which ./lokke
relies on). It can
be useful during development, or if you would like to try out the
Scheme specific facilities:
As you can see, seqs are not written like lists. Currently the Scheme
write
representation of many Clojure objects is intentionally
distinct. Of course prn
from (lokke core)
prints the Clojure
representation.
From ./guile
, you can switch to a Lokke REPL manually like this:
subdirectory of one of the directories specified by the Guile
load path.
The path can be adjusted by setting
GUILE_LOAD_PATH
in theenvironment. For example, since
./mod
is in ./lokke
‘s defaultload path, Lokke will look for
clojure.string
inmod/lokke/ns/clojure/string.go (compiled version),
mod/lokke/ns/clojure/string.clj, and mod/lokke/ns/clojure/string.scm
in that order (namspaces can be implemented in Clojure or Scheme).
See the DESIGN document for an overview of the
implementation, and detailed information about Guile is available in
the Guile Reference Manual
which should also be available via info guile
if installed.
General comparison with Clojure/JVM
-
The implementation should be properly tail-recursive.
-
Argument evaluation order is (currently) unspecified.
-
Clojure namespaces may be implemented in either Clojure or Scheme,
and Clojure namespaces are Guile modules
with the entire Clojure namespace tree situated under(lokke ns)
in the Guile module tree. -
Lokke
vars
are Guilevariables
, and unlike JVM vars, they
include no information about their origin (JVMvars
include a
namespace). As a result Lokke vars cannot be printed as a reader
macro (e.g.#'clojure.core
). -
Lokke’s reader conditional identifier is
:cljl
, for example,
#?(:cljl x)
, and at the moment reader conditionals are always
supported by the reader functions; they are not restricted to
.cljc
files. -
Symbols, like keywords, are unique and compare very efficiently (via
pointer comparison). On the JVM, this is only promised for
keywords.
Symbols do not currently support metadata. -
There are no agents or refs yet.
-
At the moment,
format
strings are
Guileformat
strings. -
The default regular expressions are
PCRE2
regular expressions, and right now, reader literal patterns#"x"
just translate to an equivalent(re-pattern ...)
at read time.
That is, they are not compiled at read time, and so are
re-evaluated. -
lokke.io
is analogous toclojure.java.io
, andlokke.shell
is
analogous toclojure.java.shell
. At the moment, paths are
generally only handled as (Unicode) strings. We’ll fix that once
Guile does. As a workaround, you may be able to set theLC_CTYPE
to a locale that passes arbitrary bytes transparently,
e.g.(guile/setlocale LC_CTYPE "en_US.iso88591")
, but note that
setlocale
acts globally, not just with respect to the current
thread. -
Arrays (
byte-array
,aref
,aset
, etc.) are implemented using
Guile’s SRFI-4 homogeneous vectors,
and so should behave fairly similarly to JVM arrays (constant time
access, compact storage, etc.). The support is imcomplete,
currently omittingobject
arrays, support for multidimensional
arrays (includingmake-array
), and casts likebytes
, for
example.Boolean arrays are implemented as
Guile bit vectors,
and so are “packed”, but consider the type subject to change. -
There is some experimental, rudimentary
compatibility with Clojure/JVM exception handling. -
Currently,
future
andfuture-call
always create a new thread,
i.e. they do not cache/pool threads. -
Metadata support is limited: vectors, hash-sets, and hash-maps,
vars, namespaces, and atoms are supported, lists and symbols are
not. Often metadata will just be discarded when it’s unsupported. -
Persistent lists are currently not
counted?
, socount
must
traverse the list. -
Dynamic variables and
binding
behave a bit differently, and they must be defined viadefdyn
. -
The numeric tower is
Guile’s,
backed by GMP, and there is currently no
distinction between functions like+'
and+
, or*'
and*
,
etc. -
Currently
ratio?
tests whether the value is a number that’s
actually represented as a fraction, more specifically, that the
value is an exact rational, but not an integer. -
There are no explicit bigints or BigDecimal (bigint, decimal?,
bigdec, 7N, 4.2M, etc.), but of course arbitrarily large integers
are supported. -
The reader currently reads invalid integer syntaxes like 2rff as
symbols. -
abs
returns the absolute value for all integers, i.e. it does not
return the minimum 64-bit integer unchanged. -
parse-long
will return a value for integers of arbitrary
magnitude while the JVM returnsnil
for anything outside the range
of a 64-bit signed integer. -
parse-double
does not yet handle JVMDouble/valueOf
hex format
doubles, and at the moment, the floating point syntax is the same as
the reader’s. -
Rather than throwing an exception, the Clojure and edn reader
functions,read
,read-string
, etc. return the rnrs end-of-file
object,
which can be identified withguile/eof-object?
. -
There are some differences and limitations with respect to the
handling of comparisons, hashes, and equality. -
fn
condition maps (i.e.:pre
:post
, etc.) are currently ignored. -
deftest
is little more than ado
, i.e. it executes immediately,
there’s no support for*load-tests*
, and it doesn’t create a test
function to run later. -
For now, types are implemented via
GOOPS
which means that you can actually modify them viaslot-set!
. We
may eventually pursue immutable GOOPS classes in Guile, but of
course you can modify anything on the JVM too, if you really set
your mind to it. -
References like
x.y.z
(that are class references on the JVM) are
not supported, and there is noimport
or:import
. -
defrecord
anddefprotocol
define a normal bindings in the
current namespace, which must berequire
d, notimport
ed (as with
records in ClojureScript). -
deftype
is not yet supported. -
For now,
*assert*
(which is not documented inclojure.core
) is
not supported.
On the Scheme side
-
#nil is nil
-
There is no
do
, onlybegin
. -
Lists are Guile lists.
-
Strings are Guile strings.
-
As with Clojure and
:refer
, explicit symbol imports are
recommended, e.g.#:use-module ((foo) #:select (x))
rather than
just#:use-module (foo)
, and Lokke modules assume this is the norm. -
Currently
equal?
is only augmented to handle new types like
hash-map, hash-set, etc. -
Currently Clojure
=
is only available as an export from(lokke core)
, and for now, it is implemented viaclj=
. -
Bindings starting with
/lokke/
are reserved (but they’re illegal
in Clojure anyway). We use them for internal
compiler-communication, among other things. See the DESIGN document
for more information. -
In the
(lokke scm)
apis, Scheme vectors are referred to as vector,
Clojure’s as lokke-vector. -
The
num
method can be used to convert characters or any
to a number. Characters are converted via Guile’schar->integer
. -
The
integer
method is effectively(truncate (num x))
, using
Guile’struncate
. -
For now,
bit-test
treats negative values as twos-complement. -
We prefer to follow the Clojure convention of explicitly
#:select
ing
symbols for import most of the time. -
We prefer to format module declarations along the same lines
suggested here: https://stuartsierra.com/2016/clojure-how-to-ns.html
Clojure namespaces and Guile modules
Clojure namespaces are Guile modules (which have very comparable
semantics), and the entire Clojure namespace tree is situated under
(lokke ns)
in the Guile module tree, i.e. clojure.string
is
implemented by the (lokke ns clojure string)
module, and
clojure.string/join
is exactly equivalent to a Guile reference to
the join
function in the (lokke ns clojure string)
module.
All clojure namspaces starting with guile
represent direct
references to the root of the guile module tree, e.g.
(guile.srfi.srfi-19/current-date)
calls the current-date
function
in the guile (srfi srfi-19)
module. These provide a convenient way
to refer to modules that are not under a (lokke ns ...)
prefix, and
of course you can use them in forms like ns
and require
. As a
special case, the guile
namespace refers to the (guile)
module,
not (guile guile)
. For example (guile/current-time)
or
(guile/delete-file ...)
.
In many cases, you may have lokke
or lok
handle the Guile
%load-path
for you via deps.edn :paths
, but manual arrangements
like this will also work fine:
$ GUILE_LOAD_PATH=$(pwd)/mod lok -e "(require '[something.core ...])" ...
Namespace (alias ...)
calls only take full effect at the end of the
enclosing top level form (because at the moment, the compiler works
from a snapshot of the alias map, cf. rewrite-il-calls
).
Exception handling
There is experimental support for try/catch/finally
which maps
closely to Guile’s
exceptions
(also rnrs conditions), meaning that in addition to catching an
ex-info
exception via (catch ExceptionInfo ex ...)
, you can catch
Guile exceptions if you know the appropriate type, e.g