Lisp lernen, Teil 1: Primzahlen berechnen
Die Syntax von Lisp ist minimalistisch, dennoch (oder gerade deswegen) birgt die Sprache ein enormes Potenzial. Um Lisp zu lernen und zu ĂŒben, hilft das regelmĂ€Ăige Schreiben von Code mehr als eine rein theoretische BeschĂ€ftigung mit der Sprache. Zum Auftakt ist das Ziel eine Funktion zum Berechnen von Primzahlen.
Die Syntax von Lisp ist minimalistisch, dennoch (oder gerade deswegen) birgt die Sprache ein enormes Potenzial. Um Lisp zu lernen und zu ĂŒben, hilft das regelmĂ€Ăige Schreiben von Code mehr als eine rein theoretische BeschĂ€ftigung mit der Sprache. Zum Auftakt ist das Ziel eine Funktion zum Berechnen von Primzahlen.
Gesucht ist eine Funktion get-primes, die die Parameter min und max entgegennimmt und eine Liste aller Primzahlen zurĂŒckgibt, die sich im durch min und max definierten Intervall befinden. EnthĂ€lt das Intervall keine Primzahlen, ist die leere Liste zurĂŒckzugeben. Ist max kleiner als min, ist die RĂŒckgabe ebenfalls die leere Liste.
Die Funktion get-primes
Das GrundgerĂŒst fĂŒr die Funktion get-primes lĂ€sst sich leicht definieren, wobei zunĂ€chst eine statische Liste mit den Primzahlen zwischen 1 und 20 zurĂŒckgegeben wird, unabhĂ€ngig von den Parametern min und max:
(defun get-primes (min max)
'(2 3 5 7 11 13 17 19))
Im nĂ€chsten Schritt ist es sinnvoll, ĂŒber alle Zahlen zwischen min und max zu iterieren und sie dynamisch als Liste aufzubauen. Sie lĂ€sst sich dann nĂ€mlich in einem weiteren Schritt filtern. Dazu dient das loop-Makro in Verbindung mit collect. Da es sich bei den Zahlen zwischen min und max nicht zwingend um Primzahlen handelt, kann man die Laufvariable beispielsweise guess nennen, um den Umstand auszudrĂŒcken, dass es sich vorerst nur um eine Vermutung handelt:
(defun get-primes (min max)
(loop
for guess from min to max
collect guess))
Als NĂ€chstes ist das Sammeln der Zahlen auf jene einzuschrĂ€nken, die tatsĂ€chlich Primzahlen sind. Angenommen, es gĂ€be eine PrĂ€dikatsfunktion is-prime, lieĂe sich das wie folgt schreiben:
(defun get-primes (min max)
(loop
for guess from min to max
when (is-prime guess)
collect guess))
Die Funktion is-prime
Da die Funktion is-prime nicht existiert, gilt es nun, sie zu definieren. Dazu ist ein Blick auf den Wikipedia-Eintrag von Primzahlen [1] hilfreich. Dort heiĂt es:
"Eine Primzahl ist eine natĂŒrliche Zahl, die genau zwei natĂŒrliche Zahlen als Teiler hat. Eine Primzahl ist also eine natĂŒrliche Zahl gröĂer als 1, die nur durch sich selbst und durch 1 ganzzahlig teilbar ist."
Da jede natĂŒrliche Zahl durch sich selbst und durch 1 teilbar ist, werden die beiden Teiler als "triviale Teiler" bezeichnet. Das Gegenteil davon sind die "nichttrivialen Teiler", die zwischen 1 und der zu teilenden Zahl liegen mĂŒssen.
Im ersten Schritt lĂ€sst sich die Funktion is-prime daher so definieren, dass sie nur Zahlen akzeptiert, die gröĂer als 1 sind:
(defun is-prime (guess)
(> guess 1))
ZusĂ€tzlich gilt es zu ĂŒberprĂŒfen, ob die Zahl nur triviale Teiler besitzt â oder, mit anderen Worten, ob sie keine nichttrivialen Teiler besitzt:
(defun is-prime (guess)
(and (> guess 1)
(not (has-non-trivial-divisors guess))))
Die Funktion has-non-trivial-divisors
FĂŒr die Funktion has-non-trivial-divisors lĂ€sst sich mit Hilfe der rem-Funktion schreiben. Sie ermittelt den Rest einer Modulo-Division. Um beispielsweise zu prĂŒfen, ob eine Zahl durch 2 teilbar ist, lĂ€sst sich has-non-trivial-divisors wie folgt definieren:
(defun has-non-trivial-divisors (n)
(zerop (rem n 2)))
Die PrĂ€dikatsfunktion zerop prĂŒft dabei, ob der ihr ĂŒbergebene Wert gleich 0 ist. Auf dem Weg liefert die Funktion den Wahrheitswert T fĂŒr alle Zahlen, die durch 2 teilbar sind. FĂŒr alle anderen gibt die Funktion NIL zurĂŒck.
Nun gilt es aber, nicht nur die 2, sondern alle möglichen Teiler zu ĂŒberprĂŒfen. Das lĂ€sst sich erneut mit dem loop-Makro lösen. Allerdings lĂ€sst sich die Schleife abbrechen, sobald ein erster Teiler gefunden wurde. Wie viele Teiler es tatsĂ€chlich gibt, ist fĂŒr die Frage, ob es ĂŒberhaupt Teiler gibt, unerheblich:
(defun has-non-trivial-divisors (n)
(loop
for i from 2 to (1- n)
thereis (zerop (rem n i))))
Zum Verbessern der Leistung bietet es sich an, die Schleife nicht bis n-1 auszufĂŒhren, sondern nur bis zur Quadratwurzel von n. Wurde dann noch kein Teiler gefunden, kann es auch keinen mehr geben:
(defun has-non-trivial-divisors (n)
(loop
for i from 2 to (sqrt n)
thereis (zerop (rem n i))))
Das Ergebnis ĂŒberprĂŒfen
Nun sind die Funktion get-primes und alle AbhÀngigkeiten implementiert. Ruft man die Funktion mit den Werten 1 und 20 auf, gibt sie die ersten acht Primzahlen korrekt aus:
(get-primes 1 20)
; => (2 3 5 7 11 13 17 19)
Der Aufruf der Funktion mit einem Intervall, in dem sich keine Primzahlen befinden, liefert wie gefordert die leere Liste zurĂŒck:
(get-primes 8 10)
; => NIL
Ruft man die Funktion schlieĂlich mit vertauschten min- und max-Werten auf, ist das Ergebnis ebenfalls die leere Liste:
(get-primes 20 1)
; => NIL
Die Aufgabe ist somit gelöst. Ein Blick auf den Code zeigt, wie elegant und zugleich mĂ€chtig das loop-Makro ist. Eine hervorragende EinfĂŒhrung in das Thema findet sich im 22. Kapitel [2] des Buchs "Practical Common Lisp [3]" von Peter Seibel.
tl;dr: Das Berechnen von Primzahlen lĂ€sst sich in Lisp gut mit dem loop-Makro lösen, das vielfĂ€ltige Möglichkeiten bietet, zu iterieren. Auch wenn die Rechenleistung des gezeigten Ansatzes deutlich verbesserungswĂŒrdig ist, zeigt das Beispiel doch gut, wie das Makro genutzt werden kann. ( [4])
URL dieses Artikels:
https://www.heise.de/-3077388
Links in diesem Artikel:
[1] https://de.wikipedia.org/wiki/Primzahl
[2] http://www.gigamonkeys.com/book/loop-for-black-belts.html
[3] http://www.gigamonkeys.com/book/
[4] mailto:webmaster@goloroden.de
Copyright © 2016 Heise Medien