ciel/docs/language-extensions.md

14 KiB

Arrow macros

We provide the Clojure-like arrow macros and "diamond wands" from the arrow-macros library.

CIEL

;; -> inserts the previous value as its first argument:
(-> "  hello macros   "
  str:upcase
  str:words) ; => ("HELLO" "MACROS")

;; ->> inserts it as its second argument:
(->> "  hello macros   "
  str:upcase
  str:words
  (mapcar #'length)) ; => (5 6)


;; use as-> to be flexible on the position of the argument:
(as-> 4 x
  (1+ x)
  (+ x x)) ; => 10

CL

;; In pure CL, just wrap function calls…
(mapcar #'length (str:words (str:upcase "  hello world ")))

;; … or use let* and intermediate variables:
(let* ((var "hello macros")
       (upcased (str:upcase var))
       (words (str:words upcased)))
  words)

And there is more. All the available macros are:

:->
:->>
:some->
:some->>
:as->
:cond->
:cond->>
:-<>
:-<>>
:some-<>
:some-<>>

Bind, more destructuring in let (metabang-bind)

We import the bind macro from metabang-bind (GitHub).

The idiomatic way to declare local variables is let (and let*), the way to declare local functions is flet (and labels). Use them if it is fine for you.

However, if you ever noticed you write convoluted let forms, adding list destructuring, multiple values or slot access into the mix, and if you use a flet and then a let, then read on.

bind integrates more variable binding and list destructuring idioms. It has two goals. Quoting:

  1. reduce the number of nesting levels
  1. make it easier to understand all of the different forms of destructuring and variable binding by unifying the multiple forms of syntax and reducing special cases.

Bind in CIEL

We import the bind macro. However, the package has more external symbols that we don't import, such as its error type (bind-error) and its extension mechanism.

Note: if you like object destructuring in general, you'll like pattern matching.

Bind is a replacement for let and let*.

You can use bind in lieue of let*.

So, its simpler form is:

(bind (a)
  (do-something a))

a is initialized to nil.

To give it a default value, use a pair, as in (a 1) below:

(bind ((a 1)
       (b 2))
  ...)

Below, we'll use indicators for a, the binding on the left-hand side, so we can have a bind form that starts with three parenthesis. But it's OK, you know how to read them.

(bind (((:values a b c) (a-function)))
  ...)

Bind with multiple-values: (:values ...)

Use (:values ...) in the left-hand side of the binding:

(bind (((:values a b) (truncate 4.5))
       ...

In pure CL, you'd use multiple-value-bind (aka mvb for completion).

Ignore values: the _ placeholder

As in:

(bind (((_ value-1 value-2) (some-function-returning-3-values)))
       ...)

Destructuring lists

Use a list in the left-hand side:

(defun return-list (a b) (list a b))

(bind (((a b) (return-list 3 4)))
  (list a b))
;; => (3 4)

You can use usual lambda parameters for more destructuring:

(bind ((a 2)
       ((b &rest args &key (c 2) &allow-other-keys) '(:a :c 5 :d 10 :e 54))
       ((:values d e) (truncate 4.5)))
  (list a b c d e args))

Bind with plists, arrays, classes, structures, regular expressions, flet and labels

It's all well explained in the documentation!

Conditions

See https://lispcookbook.github.io/cl-cookbook/error_handling.html

From Serapeum, we import ignoring.

An improved version of ignore-errors. The behavior is the same: if an error occurs in the body, the form returns two values, nil and the condition itself.

ignoring forces you to specify the kind of error you want to ignore:

(ignoring parse-error
          ...)

Pythonic triple quotes docstring

We can enable the syntax to use triple quotes for docstrings, and double quotes within them:

CIEL

(ciel:enable-pythonic-string-syntax)

(defun foo ()
  """foo "bar"."""
  t)

CL

;; Normally single quotes must be escaped.
(defun foo ()
   "foo \"bar\"."
   t)

;; use:
(pythonic-string-reader:enable-pythonic-string-syntax)

To disable this syntax, do:

(ciel:disable-pythonic-string-syntax)

We use pythonic-string-reader.

!> This syntax conflicts with libraries that use cl-syntax to use triple quotes too, even only internally. It happens with the Jonathan library.

Lambda shortcuts

You have to enable cl-punch's syntax yourself.

We use cl-punch - Scala-like anonymous lambda literal.

(ciel:enable-punch-syntax)
;; ^() is converted into (lambda ...) .
;; Each underscore is converted into a lambda argument.

(mapcar ^(* 2 _) '(1 2 3 4 5))
;; => '(2 4 6 8 10)

;; One underscore corresponds one argument.

(^(* _ _) 2 3)
;; => 6

;; <_ reuses last argument.

(mapcar ^(if (oddp _) (* 2 <_) <_) '(1 2 3 4 5))
;; => '(2 2 6 4 10)

;; _! corresponds one argument but it is brought to top of the argument list.
;; It can be useful when you want to change argument order.

(^(cons _ _!) :a :b)
;; => (:b . :a)

(^(list _! _! _!) 1 2 3)
;; => '(3 2 1)

Pattern matching

We use Trivia (see its wiki), from which we import match.

You can start typing "match", followed by the object to match against, and the clauses, which are similar to a cond. Here's an example to match a list:

(match '(1 2)
  ((list x y)  ;; <-- pattern
   (print x)
   (print y))
  (_           ;; <-- failover clause
    :else))
;; 1
;; 2

On the above example, (list x y) is the pattern. It binds x to 1 and y to 2. Pay attention that the list pattern is "strict": it has two subpatterns (x and y) and it will thus match against an object of length 2.

If you wanted y to match the rest of the list, use list*:

(match '(1 2 3)
  ((list* x y)
   (print x)
   (print y))
  (_ :else))
;; 1
;; (2 3)

This could also be achieved with the cons pattern:

(match '(1 2 3)
   ((cons x y)
    (print x)
    (print y))
   (_ :else))
;; 1
;; (2 3)

You can of course use _ placeholders:

(match '(1 2 3)
  ((list* x _)
   (print x))
  (_ :else))
;; 1

As we saw with list and cons, Trivia has patterns to match against types (vectors, alists, plists, arrays), including classes and structures.

You can use numeric patterns (=, < and friends, that behave as you expect):

(let ((x 5))
   (match x
     ((< 10)
      :lower)))
;; :LOWER

Then, you can combine them with logic based patterns and guards. For example:

(match x
  ((or (list 1 a)
       (cons a 3))
   a))

guards allow to check the matches against a predicate. For example:

(match (list 2 5)
  ((guard (list x y)     ; subpattern1
          (= 10 (* x y))) ; test-form
   t))

Above we use the list pattern, and we verify a predicate.

Trivia has more tricks in its sleeve. See the special patterns (access and change objects), the ppcre contrib, etc.

You migth also be interested in exhaustiveness type checking explained just below.

Type declarations

Use the --> macro to gradually add type declarations.

Alternatively, use defun*, defgeneric*, defmethod*, defparameter* and defvar* to add type declarations directly in the lambda list.

These notations are not strictly equivalent though.

--> comes from Serapeum. It is a shortcut for (declaim (ftype my-function (… input types …) … return type …))

CIEL

(--> mod-fixnum+ (fixnum fixnum) fixnum)
(defun mod-fixnum+ (x y) ...)

;; --> comes straight from serapeum:->

CL

(declaim (ftype (function (fixnum fixnum) fixnum) mod-fixnum+))
(defun mod-fixnum+ (x y) ...)

Now defun* and friends allow to add type declarations directly in the lambda list. They add the (declaim (ftype as above, and additionnaly declare types inside the function body:

CIEL

(defun* foo ((a integer))
  (:returns integer)
  (* 10 a))

CL

;; In pure CL, type the functions at its boundaries with ftype.
;; It is a bit verbose, but it has the advantage, being not tied to defun,
;; that we can easily refine types during development.
(declaim (ftype (function (integer) integer)             foo))
;;                        ^^ inputs ^^ output [optional] ^^ function name

;; defstar adds the internal "declare" and "the…".
;; "the" is a promise made to the compiler, that will optimize things out.
(defun foo (a)
  (declare (type integer a))
  (the integer (* 10 a)))

A type declaration for a parameter:

CIEL

(defparameter* (*file-position* (integer 0)) 0)

CL


;; Normal defparameter:
(defparameter *file-position* 0)

;; Assigning a bad value works:
(setf *file-position* "8")
;; "8"

;; We add a type declaration:
(declaim (type (integer 0) *file-position*))

;; and now:
(setf *file-position* "8")
;;
;; Value of #1="8" in (THE INTEGER "8") is #1#, not a INTEGER.
;;   [Condition of type SIMPLE-TYPE-ERROR]
;;
;; we get a type error.

We can use any type specifier:

(deftype natural () '(real 0))
(defun* sum  ((a natural) (b natural))
  (:returns natural)
  (+ a b))

Now, we get type errors at compile time:

(foo "3")
;; =>
The value
  "3"
is not of type
  INTEGER
when binding A
   [Condition of type TYPE-ERROR]

Restarts: []

Backtrace:
  0: (FOO "3") [external]
  1: (SB-INT:SIMPLE-EVAL-IN-LEXENV (FOO "a") #<NULL-LEXENV>)
  2: (EVAL (FOO "3"))

and we get compile-time warnings on type mismatches (but to be honest on simple cases like this SBCL is already quite good):

(defun* bad-foo ((a integer))
  (:returns integer)
  (format t "~a" (* 10 a)))
;
; in: DEFUN* BAD-FOO
;     (THE INTEGER (FORMAT T "~a" (* 10 CIEL::A)))
;
; caught WARNING:
;   Constant NIL conflicts with its asserted type INTEGER.
;   See also:
;     The SBCL Manual, Node "Handling of Types"
;
; compilation unit finished
;   caught 1 WARNING condition
BAD-FOO

We could add extra protection and a check-type, evaluated at runtime. Defstar can add them automatically if defstar:*check-argument-types-explicitly?* is non-nil.

In theory, such declarations don't guarantee that Lisp will do type checking but in practice the implementations, and in particular SBCL, perform type checking.

We use the defstar library. Its README has many more examples, more features (adding assertions, :pre and :post clauses) and even an alternative notation ((defun* (foo -> integer) …)).

Note: we are not talking thorough ML-like type checking here (maybe the Coalton library will bring it to Common Lisp). But in practice, the compiler warnings and errors are helpful during development, "good enough", and SBCL keeps improving in that regard.

Note: there is no "undeclaim" form :] You can unintern a symbol and re-define it.

See also:

Type checking: exhaustiveness type checking

Write a "case" and get a compile-time warning if you don't cover all cases.

From Serapeum, we import:

:etypecase-of
:ctypecase-of
:typecase-of
:case-of
:ccase-of

etypecase-of allows to do compile-time exhaustiveness type checking.

Example with enums

We may call a type defined using member an enumeration. Take an enumeration like this:

(deftype switch-state ()
  '(member :on :off :stuck :broken))

Now we can use ecase-of to take all the states of the switch into account.

(defun flick (switch)
  (ecase-of switch-state (state switch)
    (:on (switch-off switch))
    (:off (switch-on switch))))
=> Warning
(defun flick (switch)
  (ecase-of switch-state (state switch)
    (:on (switch-off switch))
    (:off (switch-on switch))
    ((:stuck :broken) (error "Sorry, can't flick ~a" switch))))
=> No warning

Example with union types

(defun negative-integer? (n)
  (etypecase-of t n
    ((not integer) nil)
    ((integer * -1) t)
    ((integer 1 *) nil)))
=> Warning

(defun negative-integer? (n)
  (etypecase-of t n
    ((not integer) nil)
    ((integer * -1) t)
    ((integer 1 *) nil)
    ((integer 0) nil)))
=> No warning

See Serapeum's reference.

trivial-types: more type definitions

From trivial-types, we import

  • association-list-p
  • type-expand
  • string-designator
  • property-list
  • tuple
  • association-list
  • character-designator
  • property-list-p
  • file-associated-stream-p
  • type-specifier-p
  • list-designator
  • package-designator
  • tuplep
  • non-nil
  • file-associated-stream
  • stream-designator
  • function-designator
  • file-position-designator
  • pathname-designator