Dawkins’ Weasel
November 14, 2014
We begin with a simple function that returns a random letter or space:
(define (rand-letter)
(let ((r (randint 27)))
(if (zero? r) #\space
(integer->char (+ r 64)))))
Next we write a function that takes a guess and returns a mutation of that guess. Each letter in the input is replaced with one chance in twenty, when a 0 is randomly chosen from the half-open range [0, 20):
(define (mutate guess)
(map (lambda (g) (if (zero? (randint 20)) (rand-letter) g)) guess))
The score
function compares a guess to a target and counts the number of equal characters.
(define (score guess target)
(apply + (map (lambda (g t) (if (char=? g t) 1 0)) guess target)))
The next function mutates its input one hundred times and returns the highest-scored mutation:
(define (get-best guess target)
(let loop ((k 100) (best 0) (mutation guess))
(if (zero? k) (values best mutation)
(let* ((m (mutate guess)) (s (score m target)))
(if (< best s)
(loop (- k 1) s m)
(loop (- k 1) best mutation))))))
We're almost finished. The display-weasel
function writes the current guess on the output screen, using lower case letters (a space is made visible using an underscore) for non-matching letters and upper case letters where the guess matches the target, making it easy to see right and wrong:
(define (display-weasel guess target)
(for-each (lambda (t g)
(if (char=? t g)
(display g)
(if (char=? #\space g)
(display #\_)
(display (char-downcase g)))))
target guess))
All that’s left is the main processing loop, which continues running until it reaches the target:
(define (dawkins target)
(let* ((target (string->list (string-upcase target)))
(len (length target))
(guess (map (lambda (x) (rand-letter)) (range len)))
(current (score guess target)))
(let loop ((i 0) (current current) (guess guess))
(display (align (number->string i) 4 'left))
(display (align (number->string current) 4 'left))
(display-weasel guess target) (newline)
(when (< current len)
(call-with-values
(lambda () (get-best guess target))
(lambda (best mutation)
(loop (+ i 1) best mutation)))))))
Here’s a sample run:
> (dawkins "METHINKS IT IS LIKE A WEASEL")
0 2 Mvmdav_krhrxehgr_uz ojeomhhx
1 3 MvmdavvkrIrxehgr_nz ojeomhhx
2 4 MvmdavvSrIrxehgr_nz ojeomhhx
3 5 MvmdavvSrIrxehgr_nE ojeomhhx
4 6 MvmdavvSrITxehgr_nE ojeomhhx
5 7 MvmdavvSrITyehgrInE ojeqmhhx
6 8 MvmdavvS ITyehgrInE ojgqmhhx
7 8 MvmdavvS ITyehgrInE ojgqmhhx
8 9 MvmdavvS ITgehgrInE ojgqAhhx
9 10 MvmdavvS ITgeh rInE ojgpAhhx
10 11 MvmdavvS ITgeh _InE ojWpAhhx
11 12 MvmdavvS ITgeh zInE ojWfAhEx
12 12 MvmdavvS ITgeh zInE ojWfAhEx
13 12 MvmdavvS ITgeh zInE ojWfAhEx
14 13 MvmdavvS ITgeh zInE ojWfASEx
15 14 MEmdavvS ITgeh zInE ojWfASEx
16 15 MEmdavvS ITgeh zInE o WfASEx
17 15 MEmdavvS ITgeh zInE o WfASEx
18 15 MEmdavvS ITgeh zInE o WfASEx
19 16 MEmdaNvS ITgeh zInE o WfASEx
20 17 MEmdaNoS ITgeh zIKE o WfASEx
21 17 MEmdaNoS ITgeh fIKE o WfASEx
22 18 MEmdaNKS ITgeh fIKE o WfASEx
23 19 MEmdaNKS ITgeS fIKE o WfASEx
24 19 MEmdaNKS ITgeS fIKE o WfASEx
25 19 MEe_aNKS ITgeS fIKE o WfASEx
26 20 MEe_aNKS ITgeS fIKE o WEASEx
27 21 MEe_aNKS ITgeS fIKE A WEASEx
28 21 MEe_aNKS ITgeS fIKE A WEASEx
29 21 MEe_aNKS ITgeS fIKE A WEASEx
30 22 MEeHaNKS ITgeS fIKE A WEASEx
31 22 MEeHaNKS ITgeS fIKE A WEASEx
32 23 MEeHaNKS ITgIS fIKE A WEASEx
33 23 MEeHaNKS ITgIS fIKE A WEASEx
34 24 MEeHaNKS IT IS fIKE A WEASEx
35 24 MEeHaNKS IT IS fIKE A WEASEx
36 24 MEeHaNKS IT IS fIKE A WEASEx
37 24 MEeHaNKS IT IS fIKE A WEASEx
38 24 MEeHaNKS IT IS fIKE A WEASEx
39 24 MEeHaNKS IT IS fIKE A WEASEz
40 24 MEeH_NKS IT IS fIKE A WEASEz
41 24 ME_H_NKS IT IS fIKE A WEASEz
42 25 METH_NKS IT IS fIKE A WEASEz
43 26 METHINKS IT IS fIKE A WEASEz
44 26 METHINKS IT IS iIKE A WEASEz
45 27 METHINKS IT IS iIKE A WEASEL
46 27 METHINKS IT IS iIKE A WEASEL
47 27 METHINKS IT IS iIKE A WEASEL
48 27 METHINKS IT IS iIKE A WEASEL
49 28 METHINKS IT IS LIKE A WEASEL
We used range
and randint
from the Standard Prelude and rept
and align
from a previous exercise. You can run the program at http://programmingpraxis.codepad.org/26naQWOy.
String manipulation suits Perl down to the ground, here we use implicit lambda functions (which is one of the beauties of perl)
Duration approx 0.15-0.25 seconds depending on randomness…
A Common Lisp version I wrote a number of years ago for a class on the evolution of language:
This is a fun genetic algorithm experiment! Thanks for posting it.
oops! s/ME THINKS/METHINKS
A Java solution:
My attempt at a clojure version here (apologies for formatting):