💾 Archived View for perso.pw › blog › articles › lisp-compare.gmi captured on 2023-01-29 at 04:18:59. Gemini links have been rewritten to link to archived content
⬅️ Previous capture (2021-12-17)
-=-=-=-=-=-=-
NIL=> Comment on Mastodon
We will refer to Common LISP as CL in the following article.
I wrote it to share what I like about CL. I'm using Perl to compare CL
features. I am using real world cases for the average programmer. If
you are a CL or perl expert, you may say that some example could be
rewritten with very specific syntax to make it smaller or faster, but
the point here is to show usual and readable examples for usual
programmers.
This article is aimed at people with programming interest, some basis
of programming knowledge are needed to understand the following. If
you know how to read C, Php, Python or Perl it should be
enough. Examples have been choosed to be easy.
I thank my friend killruana for his contribution as he wrote the
python code.
(defparameter *variable* "value")
Defining a variable with defparameter on top-level (= outside of a
function) will make it global. It is common to surround the name of
global variables with **\*** character in CL code. This is only for
readability for the programmer, the use of **\*** has no
incidence.
my $variable = "value";
variable = "value";
This is where it begins interesting in CL. Declaring a local variable
with **let** create a new scope with parenthesis where the variable
isn't known outside of it. This prevent doing bad things with
variables not set or already freed. **let** can define multiple
variables at once, or even variables depending on previously declared
variables using **let\***
(let ((value (http-request)))
(when value
(let* ((page-title (get-title value))
(title-size (length page-title)))
(when page-title
(let ((first-char (subseq page-title 0 1)))
(format t "First char of page title is ~a~%" first-char))))))
{
local $value = http_request;
if($value) {
local $page_title = get_title $value;
local $title_size = get_size $page_title;
if($page_title) {
local $first_char = substr $page_title, 0, 1;
printf "First char of page title is %s\n", $first_char;
}
}
}
The scope of a local value is limited to the parent curly brakets, of
a if/while/for/foreach or plain brakets.
if True:
hello = 'World'
print(hello) # displays World
There is no way to define a local variable in python, the scope of the
variable is limited to the parent function.
CL has a VERY powerful function to print and format text, it's even
named format. It can even manage plurals of words (in english only) !
(let ((words (list "hello" "Dave" "How are you" "today ?")))
(format t "~{~a ~}~%" words))
format can loop over lists using **~{** as start and **~}** as end.
my @words = @{["hello", "Dave", "How are you", "today ?"]};
foreach my $element (@words) {
printf "%s ", $element;
}
print "\n";
# Printing and format text
# Loop version
words = ["hello", "Dave", "How are you", "today ?"]
for word in words:
print(word, end=' ')
print()
# list expansion version
words = ["hello", "Dave", "How are you", "today ?"]
print(*words)
Sometimes we need to pass to a function a not known number of
arguments. CL supports it with **&rest** keyword in the function
declaration, while perl supports it using the **@\_** sigil.
(defun my-function(parameter1 parameter2 &rest rest)
(format t "My first and second parameters are ~a and ~a.~%Others parameters are~%~{ - ~a~%~}~%"
parameter1 parameter2 rest))
(my-function "hello" "world" 1 2 3)
sub my_function {
my $parameter1 = shift;
my $parameter2 = shift;
my @rest = @_;
printf "My first and second parameters are %s and %s.\nOthers parameters are\n",
$parameter1, $parameter2;
foreach my $element (@rest) {
printf " - %s\n", $element;
}
}
my_function "hello", "world", 0, 1, 2, 3;
def my_function(parameter1, parameter2, *rest):
print("My first and second parameters are {} and {}".format(parameter1, parameter2))
print("Others parameters are")
for parameter in rest:
print(" - {}".format(parameter))
my_function("hello", "world", 0, 1, 2, 3)
The trick in python to handle rests arguments is the wildcard
character in the function definition.
CL supports named parameters using a keyword to specify its
name. While it's not at all possible on perl. Using a hash has
parameter can do the job in perl.
CL allow to choose a default value if a parameter isn't set,
it's harder to do it in perl, we must check if the key is already set
in the hash and give it a value in the function.
(defun my-function(&key (key1 "default") (key2 0))
(format t "Key1 is ~a and key2 (~a) has a default of 0.~%"
key1 key2))
(my-function :key1 "nice" :key2 ".Y.")
There is no way to pass named parameter to a perl function. The best
way it to pass a hash variable, check the keys needed and assign a
default value if they are undefined.
sub my_function {
my $hash = shift;
if(! exists $hash->{key1}) {
$hash->{key1} = "default";
}
if(! exists $hash->{key2}) {
$hash->{key2} = 0;
}
printf "My key1 is %s and key2 (%s) default to 0.\n",
$hash->{key1}, $hash->{key2};
}
my_function { key1 => "nice", key2 => ".Y." };
def my_function(key1="default", key2=0):
print("My key1 is {} and key2 ({}) default to 0.".format(key1, key2))
my_function(key1="nice", key2=".Y.")
CL has only one loop operator, named loop, which could be seen as an
entire language itself. Perl has do while, while, for and foreach.
(loop for i from 1 to 100
do
(format t "Hello ~a~%" i))
for(my $i=1; $i <= 100; $i++) {
printf "Hello %i\n";
}
for i in range(1, 101):
print("Hello {}".format(i))
(let ((elements '(a b c d e f)))
(loop for element in elements
counting element into count
do
(format t "Element number ~s : ~s~%"
count element)))
# verbose and readable version
my @elements = @{['a', 'b', 'c', 'd', 'e', 'f']};
my $count = 0;
foreach my $element (@elements) {
$count++;
printf "Element number %i : %s\n", $count, $element;
}
# compact version
for(my $i=0; $i<$#elements+1;$i++) {
printf "Element number %i : %s\n", $i+1, $elements[$i];
}
# Loop foreach
elements = ['a', 'b', 'c', 'd', 'e', 'f']
count = 0
for element in elements:
count += 1
print("Element number {} : {}".format(count, element))
# Pythonic version
elements = ['a', 'b', 'c', 'd', 'e', 'f']
for index, element in enumerate(elements):
print("Element number {} : {}".format(index, element))
The simplest way to store data in LISP is to write a data structure
into a file, using **print** function. The code output with **print**
can be evaluated later with **read**.
(defun restore-data(file)
(when (probe-file file)
(with-open-file (x file :direction :input)
(read x))))
(defun save-data(file data)
(with-open-file (x file
:direction :output
:if-does-not-exist :create
:if-exists :supersede)
(print data x)))
;; using the functions
(save-data "books.lisp" *books*)
(defparameter *books* (restore-data "books.lisp"))
This permit to skip the use of a data storage format like XML or
JSON. Common LISP can read Common LISP, this is all it needs. It can
store objets like arrays, lists or structures using plain text
format. **It can't dump hash tables directly.**
Sometimes we have cases where we need to repeat code and there is no
way to reduce it because it's too much specific or because it's due to
the language itself. Here is an example where we can use a simple
macro to reduce the written code in a succession of conditions doing
the same check.
We will start from this
(when value
(when (string= line-type "3")
(progn
(print-with-color "error" 'red line-number)
(log-to-file "error")))
(when (string= line-type "4")
(print-with-color text))
(when (string= line-type "5")
(print-with-color "nothing")))
to this, using a macro
(defmacro check(identifier &body code)
`(progn
(when (string= line-type ,identifier)
,@code)))
(when value
(check "3"
(print-with-color "error" 'red line-number)
(log-to-file "error"))
(check "4"
(print-with-color text))
(check "5"
(print-with-color "nothing")))
The code is much more readable and the macro is easy to
understand. One could argue that in another language a switch/case
could work here, I choosed a simple example to illustrate the use of a
macro, but they can achieve more.
I'm using macros when I need to repeat code that affect variables. A
lot of CL modules offers a structure like **with-something**, it's a
wrapper macro that will do some logic like opening a database,
checking it's opened, closing it at the end and executing your code
inside.
Here I will write a tiny http request wrapper, allowing me to write
http request very easily, my code being able to use variables from the
macro.
(defmacro with-http(url)
`(progn
(multiple-value-bind (content status head)
(drakma:http-request ,url :connection-timeout 3)
(when content
,@code))))
(with-http "https://dataswamp.org/"
(format t "We fetched headers ~a with status ~a. Content size is ~d bytes.~%"
status head (length content)))
In Perl, the following would be written like this
sub get_http {
my $url = $1;
my %http = magic_http_get $url;
if($http{content}) {
return %http;
} else {
return undef;
}
}
{
local %data = get_http "https://dataswamp.org/";
if(%data) {
printf "We fetched headers %s with status %d. Content size is %d bytes.\n",
$http{headers}, $http{status}, length($http{content});
}
}
The curly brackets are important there, I want to emphase that the
brackets. Lisp is written in a successive of local scope and this is
something I really like.
import requests
with requests.get("https://dataswamp.org/") as fd:
print("We fetched headers %s with status %d. Content size is %s bytes." \
% (list(fd.headers.keys()), fd.status_code, len(fd.content)))