100 Languages Speedrun: Episode 92: newLISP

newLISP, for some reason spelled in this weird way, is another attempt at bringing Lisp into the modern age.

newLISP also makes some extremely questionable claims about figuring out "One True Solution To Memory Management", and these claims are obviously bullshit, but let's just look at the language itself first.

Hello, World!

We need to tell newLISP to exit after it finishes the program, otherwise it would drop us into REPL:

#!/usr/bin/env newlisp

(print "Hello, World!\n")
(exit)
$ ./hello.lsp
Hello, World!

Fibonacci

Everything is very neat, we can just use define and for. Of course you'll need some kind of parentheses colorizer to read this, but that's true for any Lisp, and VSCode already comes with parentheses colorizer builtin, you just need to turn it on.

There's no string interpolation, but we can pass a bunch of arguments to print:

#!/usr/bin/env newlisp

(define (fib n)
  (if (<= n 2)
    1
    (+ (fib (- n 1)) (fib (- n 2)))))

(for (i 1 30)
  (print "fib(" i ")=" (fib i) "\n"))
(exit)
$ ./fib.lsp
fib(1)=1
fib(2)=1
fib(3)=2
fib(4)=3
fib(5)=5
fib(6)=8
fib(7)=13
fib(8)=21
fib(9)=34
fib(10)=55
fib(11)=89
fib(12)=144
fib(13)=233
fib(14)=377
fib(15)=610
fib(16)=987
fib(17)=1597
fib(18)=2584
fib(19)=4181
fib(20)=6765
fib(21)=10946
fib(22)=17711
fib(23)=28657
fib(24)=46368
fib(25)=75025
fib(26)=121393
fib(27)=196418
fib(28)=317811
fib(29)=514229
fib(30)=832040

Unicode

Default length function returns size in bytes, but there are separate functions for UTF8 lengths. Some string operations like upper and lower casing are Unicode-aware, some take byte addresses.

#!/usr/bin/env newlisp

(set 'a "Żółw")
(set 'b "💩")

(print (length a) "\n")
(print (length b) "\n")

(print (utf8len a) "\n")
(print (utf8len b) "\n")

(print (upper-case a) "\n")
(print (lower-case a) "\n")

(exit)
$ ./unicode.lsp
7
4
4
1
ŻÓŁW
żółw

FizzBuzz

Like with many Lisps, we can use cond instead of chaining ifs:

#!/usr/bin/env newlisp

(for (i 1 100)
  (print
    (cond
      ((= 0 (% i 15)) "FizzBuzz")
      ((= 0 (% i 5)) "Buzz")
      ((= 0 (% i 3)) "Fizz")
      (true i))
    "\n"))
(exit)

Functional Programming

Let's do something really simple:

#!/usr/bin/env newlisp

(set 'a (list 1 2 3 4 5))
(set 'add10 (lambda (x) (+ x 10)))

(print a "\n")
(print (map (lambda (x) (+ x 10)) a) "\n")
(print (map add10 a) "\n")

(exit)
$ ./functional.lsp
(1 2 3 4 5)
(11 12 13 14 15)
(11 12 13 14 15)

OK, so far so good. Now let's try to extract that adder to a function:

#!/usr/bin/env newlisp

(define (adder n) (lambda (x) (+ x n)))

(set 'a (list 1 2 3 4 5))
(set 'add10 (adder 10))

(print a "\n")
(print (map (lambda (x) (+ x 10)) a) "\n")
(print (map add10 a) "\n")

(exit)
$ ./functional2.lsp
(1 2 3 4 5)
(11 12 13 14 15)

ERR: value expected in function + : n

What's going on? Dynamic scoping! So we discovered what completely disqualifies newLISP as an acceptable language.

At this point we could just stop. Dynamic scoping is incompatible with functional programming, and Lisp without functional programming is just pointless.

Wordle

But let's complete the usual set of programs with a Wordle.

#!/usr/bin/env newlisp

(define (random-element lst) (nth (rand (length lst)) lst))

(define (report-wordle guess word)
  (dotimes (i 5)
    (print
      (cond
        ((= (nth i guess) (nth i word)) "🟩")
        ((= (member (nth i guess) word) "🟨"))
        (true "🟥"))))
  (print "\n"))

; we need to seed random generator, or it will always return same number
(seed (time-of-day))

(define words (parse (read-file "wordle-answers-alphabetical.txt")))
(define word (random-element words))

(set 'guess "")
(while (!= guess word)
  (print "Guess: ")
  (set 'guess (read-line))
  (if (= 5 (length guess))
    (report-wordle guess word)
    (print "Guess must be 5 characters\n")))
(exit)
$  ./wordle.lsp
Guess: crane
🟩🟩🟥🟥🟩
Guess: crime
🟩🟩🟥🟥🟩
Guess: crude
🟩🟩🟥🟥🟩
Guess: crepe
🟩🟩🟩🟩🟩

The code is fairly straightforward. Notably, due to newLISP mistakenly believing it discovered "One True Solution To Memory Management", calling random-element deep-copies the whole list, turning the usually O(1) function into O(n) by memory. In this case it would be O(n) by time anyway, as Lisp insists on single linked lists, and length and nth are O(n) instead of O(1), but at least other Lisps don't copy things all the damn time.

This is fine for such tiny programs, but don't try to write anything more complicated in newLISP for sure.

Should you use newLISP?

No.

There are languages where dynamic scoping could work, but Lisp variants are not among them.

It's not even the only issue. newLISP thinks it found "One True Solution To Memory Management", but its solution is copying everything all the time. And the idea is just as bad as it sounds.

Either of these issues would disqualify a language, but newLISP suffers from both.

If you want a Lisp, there are two decent choices - Racket and Clojure.

Code

All code examples for the series will be in this repository.

Code for the newLISP episode is available here.