UP | HOME

Bisp

Table of Contents

Bisp is a typed lisp, with a little less parenthesis, infix/postfix-notation when it makes sense, Julia-like optional static typing and multi-dispatching.

1 Rationale

Parenthesis is a good thing, as always, because:

  • easy to parse, thus easy for syntax extension
  • unambiguous
  • structural editing

However, too much parenthesis creates readability problem, less elegent than Haskell, OCaml and Julia.

And due to insisting on parenthesis, some intuitive syntax becomes cubersome:

  • struct field accessing, the dot-notation, is missing
  • hard to do array indexing and slicing, e.g. arr[3,0:7,:]

Lisp sytems traditionally use dynamic typing. But static typing can be very useful annotations for programming in general. It is hard to add inline type annotations elegently due that additional parenthesis must be created for grouping purpose. Prior art: typed racket, contracts, clojure specs.

Lisp dialects also fall short on multi-dispatch generic functions, i.e. functions can be dispatched based on type and number of its arguments. This matters a lot to save the namespace of functions, and makes the program elegent and extensible. Although racket and gerbil have generic methods, clojure has defmulti, the support not as great as those in Julia.

Bisp is designed to maintain the good part of lisp, while overcoming the afore-mentioned cons.

2 Implementation

The semantic is mostly identical to that of Julia. Thus, it is probably best to implement the syntax atop Julia or Julia's IR.

It might also make sense to freely mix Julia and Bisp at the top-level and by a (JL 1+2*3) syntax for e.g. mathematical equations:

;; julia code
struct Pad end
foo(a,b) = a + b
;; Bisp code
(defn bar [a] (foo a 1))
(def var (JL
          1 .+ ones(3,2) .* zeros(2,3)))

Julia and Bisp functions can be freely called by each other as well.

In summary, implementing Bisp on top of Julia gives:

  • easy to implement
  • Julia's JIT compiler, optimization, LLVM backend provides a solid performance fundation.
  • access to all those Julia libraries, thus this might be the first lisp-for-statistics

3 The Language

3.1 Field Accessing with Dot-Notation

Prior art: cubersome struct field accessing, the dot-notation is missing

;; struct
(struct Rect w h)
(let ([r (Rect 2 3)])
  ;; field accessing is cubersome
  (* (Rect-w r)
     (Rect-h r)))

The problems:

  • (foo-a x), not elegent
  • The pattern matching with destructuring binds
    • fragile
    • have to bind all the fields

Instead, Bisp uses dot-notation:

(defstruct Rect
  w::Number h)

(defstruct Circle r)

(let [a (Rect)
      b (Circle)]
  ;; access using dot notation
  (+ a.w b.r))

3.2 Multi-Dimensional Array: Indexing and Slicing

Prior art: hard to do array indexing and slicing, e.g. the racket way:

;; array indexing and slicing
(array-ref arr #(2 3))
(array-set! brr #(2 3) 10)
(array-slice-ref arr (list (::) (:: #f #f -1)))

Bisp uses postfix indexing and slicing:

;; define an array
(def arr (ones 3 2))
;; indexing: I found comma probably makes it more clear here
arr[1, 2]
;; slicing
arr[:, 0:1]

;; array type
(defn foo [a::Array{Any 3} b]
  nil)

3.3 Optional Inline Type Annotation

Previous lisp is hard to do inline type annotations. Prior art: typed racket, contracts, clojure specs.

For example, typed racket:

;; outline annotation
(: distance (-> pt pt Real))
(define (distance p1 p2)
  (sqrt (+ (sqr (- (pt-x p2) (pt-x p1)))
           (sqr (- (pt-y p2) (pt-y p1))))))

;; inline annotations
(let ([x : Number 7])
  (add1 x))
(lambda ([x : Number] [y : String]) (+ x 5))

The problems:

  • I prefer inline type annotation
  • the inline notation of the typed racket introduces extra parenthesis, due to added spaced words.

Instead, the type annotations in Bisp simply uses y::String without extra spaces, and it should be nice and clear:

;; optional type
(defn foo [a::Number b] nil)
(defn foo [a::String b] nil)
;; union type
(defn foo [a::Union{Integer, Float} b] nil)

Support parametric types

;; parametric type
(defn foo [a::Number b c::T d::T
           #:where (<: T Real)]
  nil)

3.4 Multi-Dispatch Generic Functions by Default

ALL functions are generic methods. You define the same name multiple times (instead of define foobar-number, foobar-string), and they are dispatched upon calling:

;; by default, all functions are methods
(defn foo [a] nil)
(defn foo [a b] "no annotation")
;; optional type
(defn foo [a::Number b] "number")
(defn foo [a::String b] "string")

foo
; => generic function with 4 methods

3.5 function defs, default and keyword arguments

Bisp is lisp-1, i.e. unified namespace for functions and variables. Functions are first-class, the following defs are equivalent:

(defn foo [a b] nil)
;; same as
(def foo (λ [a b] nil))

Default arguments are given by infix notation. You don't specify type and default value together because it can be inferred by the value. All default values must be after non-default ones.

(defn foo [a b=3 c="default"] nil)

Keyword arguments are whatever after &:

;; keyword arguments separated by #:key. Here default values can be in any order
(defn foo [a::Number b c=3
           & x::String y z="defz"]
  nil)
;; function call with keyword arguments
(foo 1 2 x="X" y=8)

varargs support with intuitive ... syntax as Julia, in both function defs and callsite, and wherever makes sense:

;; var args in both function definition and callsite
(defn foo [a::String b::Number args...]
  body)
(foo "hello" 8 '(a l i s t)...)

;; also support slicing inside a list or wherever appropriate, not just function callsite
(1 2 '(3 4 5)... 6 7)