Lithp
Learning Racket has been a real eye-opener for me for all the reasons commonly cited when people refer to Lisp as The Righteous Path. It is a great piece of work by extraordinarily smart people.
But Scheme was designed to be a minimalistic starting point, and the more I use Racket, the more I’d like to edit it to better suit my tastes. It is probably a bit cheeky for a lisp neophyte, but I’ve been jotting down ideas for my ideal Lisp. These are just ideas for syntax and semantics. I’m no computer scientist, I’m a UX guy, so I can only describe the interface, not the implementation.
Racket pros could perhaps embed this proposal in Racket itself.
I’ve decided to name it ‘Lithp’ becauthe I think itth funny.
In brief, how would it look in comparison to Scheme?
;Racket
(define map
(lambda (f list)
(if
(null? list) null
(cons (f (car list)) (map f (cdr list))))))
;Lithp
(def map
(-> |f list|
(? (== list []) []
(: (f (. list 0)) (map f (.. list))))))
So, as you can see, core keywords and core functions like define, lambda, cond, and cons have had their alphabetic names replaced with either short, non-alphabetic names, drastically shortened names.
Why? Being base building blocks, they’re common. So keystrokes saved on them are saved often. They don’t need full names, they’re so familiar. Why would you write startlist when you can write ( ?
Their ubiquity makes them remarkable. Notice how Scheme syntax-highlighters give symbols and keywords different colourings? Naming with non-words reinforces this demarcation.
I actually prefer verbose, descriptive identifiers when description is needed, but they become tiresome when used for core functions. We all know what cons is.
Also, there is a bit of matchfix syntactic sugar. The list form is written in square brackets, as in Clojure. [1 2 3] means (list 1 2 3). So [] and () can’t mean the same thing in Lithp, as they do in Racket. Curly brackets are also non-interchangeable with parentheses in Lithp, because they’re used to express a hash – {'a 1 'b 2} rather than #('a 1 'b 2).
Basic naming changes:
(-> 1) => (lambda () 1)
(-> |n| n) => (lambda (n) n)
(-> n) => (lambda () n)
;args are optional, and are delimited with pipes
: => cons
. => car
; '.' is the lookup function
.. => cdr
; '..' seems a natural way to describe 'the rest of'
? => if
?? => cond
; takes a list of condition-result pairs
// => else
def => define
:: => append
Polymorphism
For core constructs, I’d like to be able to use a function on any argument in relation to which it makes sense, e.g. use map on any Functor type, such as list or a hash or a vector or function, without needing map, hash-map, vector-map. Something akin to Haskell’s typeclass system could handle this. I’ll consider this in more depth later, after I’m more experienced making typeclasses in Haskell.
(map (-> |v| (* v 2)) [1 2 3])
;the compiler/interpreter would figure out which 'map' to use,
;given the argument types @(a -> b) -> [a]@
(map (-> |k v| (* v 2)) {a 1 b 2 c 3})
;returns a new immutable hash with values mapped
Lookup
Lookup on any collection would be done with the . function, which should be polymorphic. So there would need to be a typeclass for ‘things that can allow lookup.’ Let the compiler/interpreter figure it out instead of using list-ref, vector-ref, hash-ref etc.
(. [1 2 3] 1) ; 2
(. {a 1 b 2 c 3} 'a) ; 1
I certainly don’t think there should any sugar for lookups. That would violate the ideal of orthogonality in lisp. You should be able to use an expression in the reference argument. If the reference argument is of a type that is a valid key for the given collection, it should be used in lookup, otherwise, it should be evaluated until it conforms to that requirement. For example, this should be valid:
(. [1 2 3] ((-> 1))) ; 2
To look up nested collections, you should be able to pass a list as the second argument, instead of calling all those car/cdr functions:
;Racket
(car (cadr (list (list 1 2 3) (list 4 5 6) (list 7 8 9)))) ; 4
;Lithp
(. [[1 2 3] [4 5 6] [7 8 9]] [1 0]) ; 4
Lookup should also be able to take a function, which takes key/index and value and returns a Bool match. It will return a list of the values of all matches:
(. {a 1 b 2 c 3} (-> |key value| (or (== key 'b) (== value 3))) ; [2 3]
If you can do lookup on lists, you should be able to do lookup on quoted s-exps, which are, after all, lists:
(. `(-> |n m| (+ n m)) 0) ; '->'
With such an increment of power in the lookup function ., you’d no longer need to use filter functions.
So any collection type (‘lookupable’ type) should have functions defined which implement lookup by key/index, lists of key/index, by test function, and nested lists of test function.
Quoting depth
With the extraction function '', you should be able to specify how deeply to evaluate an expression before returning the resulting semi-evaluated expression as data:
(def expr (+ (+ 1 1) (+ 1 1) (+ 1 1)))
('' expr 0) ; `(+ (+ 1 1) (+ 1 1) (+ 1 1))
('' expr 1) ; `(+ 2 2 2)
('' expr 2) ; `6
Collections are immutable by default
I’ve not used Clojure seriously, but am aware of its immuatable collection types.
I find it easier to think about functional code when using immutable structures. At first, I worried that making copies of everything would be slow, but suspected that some smart person had already found a way to make it fast, and found that sure enough, Clojure has a strategy in place for this (and surely other languages have solved it too):
All of the Clojure collections are immutable and persistent. In particular, the Clojure collections support efficient creation of ‘modified’ versions, by utilizing structural sharing, and make all of their performance bound guarantees for persistent use.
From clojure docs
I’m not sure what the details of ‘structural sharing’ are, but by its name, I would guess the idea is that you have a base structure, and only record what is different about the copied thing in relation to its parent – this is perhaps similar to the idea of differential inheritance – i.e. ‘Dumbo is an elephant, but with giant ears’ – you don’t need to copy all the details of ‘elephant’, you just refer to them, and record what is different in relation to them (giant ears).
I suppose the same kind of process is at work in video compression algorithms, where one frame is described in terms of its difference to its neighbours. This would take less space than describing every pixel in every frame.
But I digress and am I’m just guessing – I’m no expert on that stuff. I’m just thinking about semantics and know that as an end user of a high-level language, I’d prefer to work with immutables in most cases, and I shouldn’t need to angst about performance when doing so.
Strings are just lists of characters, as in Haskell
Characters should be any single character within single quotes 'a'. A single, starting quote will still describe a symbol.
But this choice of syntax introduces the requirement that symbols must be longer than 1 character, lest they be considered characters.
Strings should only be valid when written in double quotes.
So ['w' 'h' 'a' 't'] is equivalent to "what"
With this choice in effect, functions for lists can be used on strings.
Currying by default
Haskell has made me want to be able to do this kind of thing without much effort:
(map (* 2) [1 2 3]) ; [2 4 6]
;instead of the noisy:
(map (lambda (n) (* n 2)) '(1 2 3))
;why does this work? (Using ':t' function to get the type of an expression)
(:t *) ; ((Num a) => a -> a -> a)
(:t (* 2)) ; ((Num a) => a -> a)
(:t (* 2 3)) ; Int
Currying in Haskell is so nice. To have it in Lithp, we’d need to get rid of keyword arguments and arbitrary argument lengths. That is no big loss, as those could be replaced by containing arguments in hashes and lists respectively.