| 1 | """ |
|---|
| 2 | BlindWatchMaker1.cobra |
|---|
| 3 | |
|---|
| 4 | The book is "The Blind Watchmaker" by Richard Dawkins. In Chapter 3, |
|---|
| 5 | "Accumulating Small Changes", Dawkins distinguishes between "single-step |
|---|
| 6 | selection" and "cumulative selection". This doc string is a poor substitute for |
|---|
| 7 | reading the book, but here we go: |
|---|
| 8 | |
|---|
| 9 | Suppose you wish to randomly generate a desired line from Shakespeare: |
|---|
| 10 | |
|---|
| 11 | Methinks it is like a weasel |
|---|
| 12 | |
|---|
| 13 | Using a "single-step selection", you would generate 28 fresh, random letters at |
|---|
| 14 | a time like so: |
|---|
| 15 | |
|---|
| 16 | Attempt 1: oufswgonxgjsqvfwlpmhhkhfcrxe |
|---|
| 17 | Attempt 2: eqnhydvqghckpyzrvayjepnvzbbd |
|---|
| 18 | Attempt 3: m qekjigucvn pqemcsjmqlbuo q |
|---|
| 19 | Attempt 4: dsiwcrdgrz fqtytgljqziixjtsw |
|---|
| 20 | Attempt 5: pplxjtccuzkilknocmtutsvjekrw |
|---|
| 21 | Attempt 6: jmmj hrbndxyqw iabepvvvx ydy |
|---|
| 22 | Attempt 7: vbs bgtimbidtmazqwdxtnjdfcap |
|---|
| 23 | Attempt 8: rqjceuhlmyxjcswztawjuwheugfh |
|---|
| 24 | Attempt 9: tpu gerxtg eqnt uvarklegsvv |
|---|
| 25 | |
|---|
| 26 | ...until you hit upon the desired line. The odds of success are astronomical. |
|---|
| 27 | You would die before it happened. |
|---|
| 28 | |
|---|
| 29 | But using "cumulative selection", you would only *start* with 28 random letters. |
|---|
| 30 | Thereafter, you reproduce the string with a slight mutation, evaluate its |
|---|
| 31 | fitness with respect to the goal and choose between it or the original. The |
|---|
| 32 | fittest survives and the weakest is cast aside. In this way, progress |
|---|
| 33 | accumulates to reach the goal in seconds even though the micro-engineering was |
|---|
| 34 | still random (mutating the string). That's because the macro-engineering was not |
|---|
| 35 | (selection and reproduction). |
|---|
| 36 | |
|---|
| 37 | This Cobra program demonstrates the above. |
|---|
| 38 | |
|---|
| 39 | |
|---|
| 40 | At the Lang.NET 2008 Conference (http://www.langnetsymposium.com/), I met |
|---|
| 41 | Peli de Halleux from the Pex team at http://research.microsoft.com/pex/. |
|---|
| 42 | We put this program through Pex and it automatically found legit errors in the |
|---|
| 43 | code resulting in improvements to the contracts. Those additional contracts are |
|---|
| 44 | marked "Pex" below. |
|---|
| 45 | """ |
|---|
| 46 | |
|---|
| 47 | |
|---|
| 48 | class BlindWatchMaker1 |
|---|
| 49 | |
|---|
| 50 | var _random = Random() |
|---|
| 51 | |
|---|
| 52 | def main |
|---|
| 53 | goal = 'methinks it is like a weasel' |
|---|
| 54 | alphabet = 'abcdefghijklmnopqrstuvwxyz ' |
|---|
| 55 | duration1 = .runCumulativeSelection(goal, alphabet) |
|---|
| 56 | duration2 = .runSingleStepSelection(goal, alphabet) |
|---|
| 57 | factor = duration2 / duration1 |
|---|
| 58 | print 'Single step selection took [Math.round(factor)] times longer than cumulative selection and *still* failed.' |
|---|
| 59 | |
|---|
| 60 | def runCumulativeSelection(goal as String, alphabet as String) as float |
|---|
| 61 | require |
|---|
| 62 | goal.length # Pex |
|---|
| 63 | alphabet.length # Pex |
|---|
| 64 | ensure |
|---|
| 65 | result > 0 |
|---|
| 66 | body |
|---|
| 67 | print 'Cumulative Selection:' |
|---|
| 68 | start = DateTime.now |
|---|
| 69 | # wip stands for "work in progress" |
|---|
| 70 | wip = .randomString(goal.length, alphabet) |
|---|
| 71 | showAttempt = true |
|---|
| 72 | generation = 1 |
|---|
| 73 | while true |
|---|
| 74 | score = .score(wip, goal) # 0..100 |
|---|
| 75 | if showAttempt |
|---|
| 76 | print ' attempt [generation.toString.padLeft(4)] [wip] ==> [score]%' |
|---|
| 77 | if score >= 100 |
|---|
| 78 | break |
|---|
| 79 | mutantChar = alphabet[_random.next(alphabet.length)] |
|---|
| 80 | mutantIndex = _random.next(wip.length) |
|---|
| 81 | firstPart = wip[:mutantIndex] |
|---|
| 82 | secondPart = wip[mutantIndex+1:] |
|---|
| 83 | offspring = '[firstPart][mutantChar][secondPart]' |
|---|
| 84 | if .score(offspring, goal) > .score(wip, goal) |
|---|
| 85 | # we have a new, improved string |
|---|
| 86 | showAttempt = true |
|---|
| 87 | wip = offspring |
|---|
| 88 | else |
|---|
| 89 | # the offspring is no better than the parent (and possibly worse) |
|---|
| 90 | showAttempt = false |
|---|
| 91 | generation += 1 |
|---|
| 92 | duration = DateTime.now.subtract(start) |
|---|
| 93 | print ' duration: [duration]' |
|---|
| 94 | return duration.totalSeconds |
|---|
| 95 | |
|---|
| 96 | def runSingleStepSelection(goal as String, alphabet as String) as float |
|---|
| 97 | require |
|---|
| 98 | goal.length |
|---|
| 99 | alphabet.length |
|---|
| 100 | ensure |
|---|
| 101 | result > 0 |
|---|
| 102 | body |
|---|
| 103 | print 'Single Step Selection:' |
|---|
| 104 | start = DateTime.now |
|---|
| 105 | maxAttempts = 1_000_000 |
|---|
| 106 | reportInterval = 100_000 |
|---|
| 107 | maxScore = 0 |
|---|
| 108 | for i in maxAttempts |
|---|
| 109 | s = .randomString(goal.length, alphabet) |
|---|
| 110 | if s == goal |
|---|
| 111 | # this could theoretically happen, but never does |
|---|
| 112 | print ' !!! SUCCESS !!!' |
|---|
| 113 | print ' On attempt [i+1], a randomly generated string matched:' |
|---|
| 114 | print ' [s]' |
|---|
| 115 | break |
|---|
| 116 | if .score(s, goal) > maxScore |
|---|
| 117 | maxScore = .score(s, goal) |
|---|
| 118 | if (i+1) % reportInterval == 0 |
|---|
| 119 | print ' [i+1]...' |
|---|
| 120 | if s <> goal |
|---|
| 121 | print ' Giving up after [i] attempts of single step selection.' |
|---|
| 122 | print ' Maximum score was [maxScore]%.' |
|---|
| 123 | duration = DateTime.now.subtract(start) |
|---|
| 124 | print ' duration: [duration]' |
|---|
| 125 | return duration.totalSeconds |
|---|
| 126 | |
|---|
| 127 | def randomString(length as int, alphabet as String) as String |
|---|
| 128 | require |
|---|
| 129 | length > 0 |
|---|
| 130 | alphabet <> '' |
|---|
| 131 | ensure |
|---|
| 132 | result.length == length |
|---|
| 133 | test |
|---|
| 134 | x = BlindWatchMaker1() |
|---|
| 135 | assert x.randomString(5, 'ab').length == 5 |
|---|
| 136 | s = x.randomString(1000, 'a') |
|---|
| 137 | for c in s, assert c == c'a' |
|---|
| 138 | body |
|---|
| 139 | sb = StringBuilder() |
|---|
| 140 | for i in length |
|---|
| 141 | c = alphabet[_random.next(alphabet.length)] |
|---|
| 142 | sb.append(c) |
|---|
| 143 | return sb.toString |
|---|
| 144 | |
|---|
| 145 | def score(candidate as String, goal as String) as int |
|---|
| 146 | """ |
|---|
| 147 | Returns the score of the candidate string with regards to how closely it |
|---|
| 148 | matches the goal. |
|---|
| 149 | """ |
|---|
| 150 | require |
|---|
| 151 | candidate.length # Pex |
|---|
| 152 | goal.length # Pex |
|---|
| 153 | candidate.length == goal.length |
|---|
| 154 | ensure |
|---|
| 155 | result >= 0 and result <= 100 |
|---|
| 156 | test |
|---|
| 157 | bwcc = BlindWatchMaker1() |
|---|
| 158 | assert bwcc.score('aoeu', 'aoeu') == 100 |
|---|
| 159 | assert bwcc.score('aabb', 'aacc') == 50 |
|---|
| 160 | assert bwcc.score('a b ', 'axbx') == 50 |
|---|
| 161 | body |
|---|
| 162 | score = 0 |
|---|
| 163 | for i in goal.length |
|---|
| 164 | if candidate[i] == goal[i], score += 1 |
|---|
| 165 | value = (score / goal.length * 100) to int |
|---|
| 166 | return value |
|---|