Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Solving a CSP with Monad Transformers and a Genetic Algorithm

Solving a CSP with Monad Transformers and a Genetic Algorithm

Intermediate–level talk that cover the 4 blog posts by Bartosz Milewski on solving a simple constraint satisfaction problem in C++ and Haskell.
Starting from an imperative–style implementation we will get to a pure FP–style solution.

2fa9df92d8d7eba3e17207c86f953be3?s=128

Filippo Vitale

March 28, 2018
Tweet

Transcript

  1. @filippovitale May 2017 Ann Arbor FP Solving a Constraint Satisfaction

    Problem (CSP) with Monad Transformers .
  2. SEND + MORE ------- MONEY @alexeyraga https://en.wikipedia.org/wiki/Verbal_arithmetic

  3. SEND + MORE ------- MONEY @alexeyraga E == E ==

    E
  4. SEND + MORE ------- MONEY @alexeyraga S,M ≠ 0

  5. None
  6. SEND + MORE ------- MONEY M←1

  7. SEND + 1ORE ------- 1ONEY M←1 O←0 S←8,9

  8. M←1 Wait a minute... @alexeyraga

  9. None
  10. Immutability Higher-order function Monad Referential transparency

  11. Mon*d

  12. Brute Force Naïve implementation

  13. Brute force 10!/(10 – 8)! ≈ 2 Million How big

    is the search space?
  14. for (int s = 0; s < 10; ++s) for

    (int e = 0; e < 10; ++e) for (int n = 0; n < 10; ++n) for (int d = 0; d < 10; ++d) ... s != 0 e != s n != s && n != e d != s && d != e && d != n Imperative approach 8 nested loops
  15. for (int s = 0; s < 10; ++s) for

    (int e = 0; e < 10; ++e) for (int n = 0; n < 10; ++n) for (int d = 0; d < 10; ++d) ... s != 0 e != s n != s && n != e d != s && d != e && d != n The solution boils down to: ➔ generating all those substitutions ➔ testing the constraints for each one
  16. for { s <- 0 to 9 if s !=

    0 e <- 0 to 9 if e != s n <- 0 to 9 if n != s && n != e d <- 0 to 9 if d != s && d != e && d!=n ... for (int s = 0; s < 10; ++s) for (int e = 0; e < 10; ++e) for (int n = 0; n < 10; ++n) for (int d = 0; d < 10; ++d) ... s != 0 e != s n != s && n != e d != s && d != e && d != n The solution boils down to: ➔ generating all those substitutions ➔ testing the constraints for each one
  17. Search candidate solutions Constraints satisfied?

  18. val listOfDigits = (0 to 9).toList val solutions = for

    { s <- listOfDigits if s != 0 e <- listOfDigits if s != e n <- listOfDigits if s != n && e != n d <- listOfDigits if s != d && e != d && n != d m <- listOfDigits if m != 0 if s != m && e != m && n != m && d != m o <- listOfDigits if s != o && e != o && n != o && d != o && m != o r <- listOfDigits if s != r && e != r && n != r && d != r && m != r && o != r y <- listOfDigits if s != y && e != y && n != y && d != y && m != y && o != y && r != y } yield ??? Search candidate solutions Constraints satisfied?
  19. val listOfDigits = (0 to 9).toList val solutions = for

    { s <- listOfDigits if s != 0 e <- listOfDigits if s != e n <- listOfDigits if s != n && e != n d <- listOfDigits if s != d && e != d && n != d m <- listOfDigits if m != 0 if s != m && e != m && n != m && d != m o <- listOfDigits if s != o && e != o && n != o && d != o && m != o r <- listOfDigits if s != r && e != r && n != r && d != r && m != r && o != r y <- listOfDigits if s != y && e != y && n != y && d != y && m != y && o != y && r != y send = List(s, e, n, d).reduce(_ * 10 + _) more = List(m, o, r, e).reduce(_ * 10 + _) money = List(m, o, n, e, y).reduce(_ * 10 + _) if send + more == money } yield (send, more, money) Search candidate solutions Constraints satisfied?
  20. val listOfDigits = (0 to 9).toList val solutions = for

    { s <- listOfDigits if s != 0 e <- listOfDigits if s != e n <- listOfDigits if s != n && e != n d <- listOfDigits if s != d && e != d && n != d m <- listOfDigits if m != 0 if s != m && e != m && n != m && d != m o <- listOfDigits if s != o && e != o && n != o && d != o && m != o r <- listOfDigits if s != r && e != r && n != r && d != r && m != r && o != r y <- listOfDigits if s != y && e != y && n != y && d != y && m != y && o != y && r != y send = List(s, e, n, d).reduce(_ * 10 + _) more = List(m, o, r, e).reduce(_ * 10 + _) money = List(m, o, n, e, y).reduce(_ * 10 + _) if send + more == money } yield (send, more, money)
  21. “Jimmy” and the Multiverse

  22. None
  23. 0 1 2 3 4 5 6 7 8 9

  24. 1 2 3 4 5 6 7 8 9 0

  25. ? ? ? ? ? ? ? ? ? ?

    S = …
  26. 1 2 3 4 5 6 7 8 9 0

    0 S = 0!
  27. None
  28. ? ? ? ? ? ? ? ? ? ?

    S = …
  29. 0 1 2 3 4 5 6 7 8 9

    0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 5 1 2 3 4 0 6 7 8 9 0 6 2 3 4 5 1 7 8 9 0 1 7 3 4 5 6 2 8 9 0 1 3 2 4 5 6 7 8 9 0 1 8 3 4 5 6 7 8 9 0 1 4 3 2 5 6 7 8 9 0 1 9 3 4 5 6 7 8 2
  30. 0 1 2 3 4 5 6 7 8 9

    0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 5 1 2 3 4 0 6 7 8 9 0 6 2 3 4 5 1 7 8 9 0 1 7 3 4 5 6 2 8 9 0 1 3 2 4 5 6 7 8 9 0 1 8 3 4 5 6 7 8 9 0 1 4 3 2 5 6 7 8 9 0 1 9 3 4 5 6 7 8 2 S = 1
  31. 0 1 2 3 4 5 6 7 8 9

    S = 1
  32. 0 1 2 3 4 5 6 7 8 9

    S = 1 E = …
  33. 0 1 2 3 4 5 6 7 8 9

    S = 1 E = … 0 2 3 4 5 6 7 8 9
  34. None
  35. Multi-verse Implementation using List

  36. val l = for { a <- List(1, 2) b

    <- List(1, 2) } yield Map('a' -> a, 'b' -> b) val l = ???
  37. val l = for { a <- List(1, 2) b

    <- List(1, 2) } yield Map('a' -> a, 'b' -> b) val l: List[A] = ???
  38. val l = for { a <- List(1, 2) b

    <- List(1, 2) } yield Map('a' -> a, 'b' -> b) val l: List[Map[Char, Int]] = ???
  39. val l = for { a <- List(1, 2) b

    <- List(1, 2) } yield Map('a' -> a, 'b' -> b) List(Map('a' -> 1, 'b' -> 1), Map('a' -> 1, 'b' -> 2), Map('a' -> 2, 'b' -> 1), Map('a' -> 2, 'b' -> 2))
  40. val l = for { a <- List(1, 2) b

    <- List(1, 2) if a != b } yield Map('a' -> a, 'b' -> b) List(Map('a' -> 1, 'b' -> 2), Map('a' -> 2, 'b' -> 1))
  41. Multi-verse Implementation using the State Monad

  42. “The future is a function of the past.” – A.

    P. Robertson https://goo.gl/jt6bvz
  43. // stateful computation type State[S, +A] = S => (S,

    A) https://bartoszmilewski.com/2016/11/30/monads-and-effects/#State
  44. object State { def apply[S, A](f: S => (S, A)):

    State[S, A] }
  45. // stateful computation f: S => (S, A) def select(xs:

    List[Int]): (List[Int], Int) = (xs.tail, xs.head)
  46. // stateful computation f: S => (S, A) def select(xs:

    List[Int]): (List[Int], Int) = (xs.tail, xs.head)
  47. // stateful computation f: S => (S, A) def select(xs:

    List[Int]): (List[Int], Int) = (xs.tail, xs.head)
  48. import scalaz.State val s = for { a <- State(select)

    b <- State(select) } yield Map('a' -> a, 'b' -> b) val s = ???
  49. import scalaz.State val s = for { a <- State(select)

    b <- State(select) } yield Map('a' -> a, 'b' -> b) val s: State[ S, A ] = ???
  50. import scalaz.State val s = for { a <- State(select)

    b <- State(select) } yield Map('a' -> a, 'b' -> b) val s: State[ List[Int], Map[Char, Int] ] = ???
  51. import scalaz.State val s = for { a <- State(select)

    b <- State(select) } yield Map('a' -> a, 'b' -> b) val s: State[ List[Int], Map[Char, Int] ] = ??? s.eval(List(1, 2)) ==== Map('a' -> 1, 'b' -> 2)
  52. import scalaz.State val s = for { a <- State(select)

    b <- State(select) } yield Map('a' -> a, 'b' -> b) val s: State[ List[Int], Map[Char, Int] ] = ??? s.eval(List(8, 3)) ==== Map('a' -> 8, 'b' -> 3)
  53. val s = for { a <- State(select) b <-

    State(select) c <- State(select) } yield Map('a' -> a, 'b' -> b, 'c' -> c) s.eval(1 |-> 3) ==== Map('a' -> 1, 'b' -> 2, 'c' -> 3)
  54. val s = for { a <- State(select) b <-

    State(select) c <- State(select) d <- State(select) } yield Map('a' -> a, 'b' -> b, 'c' -> c, 'd' -> d) s.eval(1 |-> 4) ==== Map('a' -> 1, 'b' -> 2, 'c' -> 3, 'd' -> 4)
  55. None
  56. Multi-verse Implementation using StateT[List, S, A]

  57. None
  58. None
  59. def select[A](xs: List[A]): (List[A], A) State S => ( S

    , A) def select[A](xs: List[A]): List[(List[A], A)] StateT[F, S, A] S => F[( S , A)]
  60. def select[A](xs: List[A]): List[(List[A], A)] = xs map { x

    => (xs.filterNot(_ == x), x) }
  61. 0 1 2 3 4 5 6 7 8 9

    0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 5 1 2 3 4 0 6 7 8 9 0 6 2 3 4 5 1 7 8 9 0 1 7 3 4 5 6 2 8 9 0 1 3 2 4 5 6 7 8 9 0 1 8 3 4 5 6 7 8 9 0 1 4 3 2 5 6 7 8 9 0 1 9 3 4 5 6 7 8 2
  62. sl.eval(1 |-> 2) == ??? import scalaz.StateT import scalaz.std.list._ val

    sl = for { a <- StateT(select) b <- StateT(select) } yield Map('a' -> a, 'b' -> b)
  63. import scalaz.StateT import scalaz.std.list._ val sl = for { a

    <- StateT(select) b <- StateT(select) } yield Map('a' -> a, 'b' -> b) sl.eval(1 |-> 2) == List(Map('a' -> 1, 'b' -> 2), Map('a' -> 2, 'b' -> 1)) https://gist.github.com/filippovitale/6cc45396ed917a2a8411
  64. val sel = StateT(select[Int]) for { s <- sel e

    <- sel n <- sel d <- sel m <- sel o <- sel r <- sel y <- sel … }
  65. val sel = StateT(select[Int]) for { s <- sel e

    <- sel n <- sel d <- sel m <- sel o <- sel r <- sel y <- sel … } For every `.flatMap` call, `sel` return a different number! 0 1 2 3 4 5 6 7 8 9
  66. Solving the puzzle using StateT[List, S, A]

  67. val sel = StateT(select[Int]) for { s <- sel if

    s != 0 e <- sel n <- sel d <- sel m <- sel if m != 0 o <- sel r <- sel y <- sel send = List(s, e, n, d).reduce(_ * 10 + _) more = List(m, o, r, e).reduce(_ * 10 + _) money = List(m, o, n, e, y).reduce(_ * 10 + _) if send + more == money } yield (s, e, n, d, m, o, r, y) MonadPlus MonadPlus https://speakerdeck.com/filippovitale/send-plus-more-equals-money-scalasyd-july-2015
  68. Solving the puzzle using the Scala APIs

  69. val maps = for { combination <- (0 to 9).combinations(8)

    // 45 combinazioni permutation <- combination.permutations // 40K permutazioni } yield ("sendmoremoney".distinct zip permutation).toMap // ~2M val solutions = for { m <- maps if m('s') != 0 && m('m') != 0 send = "send" map m reduce (_ * 10 + _) more = "more" map m reduce (_ * 10 + _) money = "money" map m reduce (_ * 10 + _) if send + more == money } yield (send, more, money) solutions.next() // (9567,1085,10652) Constraints satisfied? Search candidate solutions
  70. These are all depth-first search implementations

  71. None
  72. Solving the puzzle with a Genetic Algorithm http://dl.acm.org/citation.cfm?id=1725467

  73. Genetic Algorithm type Digit = Int type Individual = Vector[Digit]

    // “sendmory” Vector length 8 ⇒ 45 different Species - Individual
  74. Genetic Algorithm type Digit = Int type Individual = Vector[Digit]

    // “sendmory” type Population = List[Individual] - Individual - Population
  75. Genetic Algorithm type Digit = Int type Individual = Vector[Digit]

    // “sendmory” type Population = List[Individual] def fitness(individual: Individual): Int // 0 - Individual - Population - Fitness function
  76. Genetic Algorithm - Individual - Population - Fitness function -

    Mutation type Digit = Int type Individual = Vector[Digit] // “sendmory” type Population = List[Individual] def fitness(individual: Individual): Int def mutation(individual: Individual): Individual // asexual genetic algorithm
  77. None
  78. Simple Implementation of the Genetic Algorithm to solve the puzzle

  79. /** side-effect */ val r = util.Random val generationSize =

    512 val generationRetainment = 32 // top 6% // 16 of the 28 possible mutations val individualProlificness: Int = generationSize / generationRetainment
  80. def fitness(individual: Individual): Int = { math.abs(send + more -

    money) }
  81. def fitness(individual: Individual): Int = { val cs = "sendmoremoney".distinct

    val m = cs.zip(individual).toMap math.abs(send + more - money) }
  82. def fitness(individual: Individual): Int = { val cs = "sendmoremoney".distinct

    val m = cs.zip(individual).toMap val send = "send" map m reduce (_ * 10 + _) val more = "more" map m reduce (_ * 10 + _) val money = "money" map m reduce (_ * 10 + _) math.abs(send + more - money) }
  83. val mutationIndices = List(0 |-> 7).combinations(2) def everyMutation(individual: Individual): Iterator[Individual]

    = r.shuffle(mutationIndices) .map { case Seq(i, j) => individual .updated(i, individual(j)) .updated(j, individual(i)) } // +-+-+-+-+-+-+-+-+ // |S|E|N|D|M|O|R|Y| // +-+-+-+-+-+-+-+-+ // |0|1|2|3|4|5|6|7| // +-+-+-+-+-+-+-+-+ // ^ ^ // | | // +-----+ // [i] [j]
  84. val solution = ???

  85. val solution = Iterator .iterate(generatePopulation(generationSize)) (createNextGeneration)

  86. val solution = Iterator .iterate(generatePopulation(generationSize)) (createNextGeneration)

  87. def createNextGeneration(currentGeneration: Population) : Population = ???

  88. def createNextGeneration(currentGeneration: Population) : Population = currentGeneration .take(generationRetainment)

  89. def createNextGeneration(currentGeneration: Population) : Population = currentGeneration .take(generationRetainment) .flatMap(everyMutation(_).take(individualProlificness))

  90. def createNextGeneration(currentGeneration: Population) : Population = currentGeneration .take(generationRetainment) .flatMap(everyMutation(_).take(individualProlificness)) .sortBy(fitness)

  91. def createNextGeneration(currentGeneration: Population) : Population = if (fitness(currentGeneration.head) == 0)

    { ??? } else { ??? }
  92. def createNextGeneration(currentGeneration: Population) : Population = if (fitness(currentGeneration.head) == 0)

    { ??? } else { currentGeneration.take(generationRetainment) .flatMap(everyMutation(_).take(individualProlificness)) .sortBy(fitness) }
  93. def createNextGeneration(currentGeneration: Population) : Population = if (fitness(currentGeneration.head) == 0)

    { currentGeneration.take(1) } else { currentGeneration.take(generationRetainment) .flatMap(everyMutation(_).take(individualProlificness)) .sortBy(fitness) }
  94. val solution = Iterator .iterate(generatePopulation(generationSize)) (createNextGeneration) .dropWhile(_.size > 1)

  95. val solution = Iterator .iterate(generatePopulation(generationSize)) (createNextGeneration) .dropWhile(_.size > 1) .map(_.head).next()

  96. Benchmarking

  97. None
  98. None
  99. @filippovitale May 2017 Ann Arbor FP $ tail -f questions