Hello, declarative world

Hello, declarative world

We know that a computer is an imperative machine: a CPU reads one instruction after another, and performs one operation after another, with each operation modifying the state of its registers and memory. But that doesn’t mean we HAVE to write our computer programs as imperative step-by-step recipes; there are other interesting ways to specify a computation.

Ruby’s billed as a “multi-paradigm” language, but, let’s face it, we use it to write imperative object-oriented programs. I'm interested in different ways of programming computers, because I reckon imperative programs are far too low-level and overspecified, which is why as a species we're generally terrible at writing them.

One interesting alternative is declarative programming, where we tell the computer about the problem we want to solve instead of exactly what steps to take to solve it. That idea’s been around for a long time but has recently begun to reappear everywhere.

In this talk I want to encourage you to think outside of your normal programming habits; I’ll present a more abstract and declarative style called relational programming, and show you how to implement a minimal relational language in Ruby. (Dog whistle: this is μKanren.)

Given at Joy of Coding 2015 (http://lanyrd.com/2015/joyofcoding/) and the London Ruby User Group (http://lrug.org/meetings/2015/august/). A video and expanded transcript are available at http://codon.com/hello-declarative-world, and the code is available at http://github.com/tomstuart/kanren.

Cd9b247e4507fed75312e9a42070125d?s=128

Tom Stuart

August 10, 2015
Tweet

Transcript

  1. 2.
  2. 3.
  3. 9.

    “RELATION” x is 4 x < y y is 5

    x is ? x is 3 x is 2 x is 1 x is 0
  4. 12.
  5. 13.

    >> x, y = Variable.new(:x), Variable.new(:y) => [x, y] >>

    x == x => true >> x == y => false >> x == Variable.new(:x) => false
  6. 15.
  7. 16.

    class State def initialize(variables = [], values = {}) @variables,

    @values = variables, values end attr_reader :variables, :values end
  8. 17.

    >> state = State.new => #<State @variables=[], @values={}> >> state,

    (x, y, z) = state.create_variables [:x, :y, :z] => [#<State @variables=[x, y, z], @values={}>, [x, y, z]] >> state.variables => [x, y, z] >> state = state.assign_values x => y, y => z, z => 5 => #<State @variables=[x, y, z], @values={x=>y, y=>z, z=>5}> >> state.values => {x=>y, y=>z, z=>5}
  9. 18.

    class State def create_variables(names) new_variables = names.map { |name| Variable.new(name)

    } [ State.new(variables + new_variables, values), new_variables ] end def assign_values(new_values) State.new(variables, values.merge(new_values)) end end
  10. 19.

    >> state.values => {x=>y, y=>z, z=>5} >> state.value_of x =>

    5 >> state.value_of 7 => 7 >> state, (a, b, c) = state.create_variables [:a, :b, :c] => [#<State …>, [a, b, c]] >> state.value_of a => a
  11. 22.

    >> state, (x, y) = State.new.create_variables [:x, :y] => [#<State

    @variables=[x, y], @values={}>, [x, y]] >> state = state.unify(x, x) => #<State @variables=[x, y], @values={}> >> state.values => {}
  12. 23.

    >> state = state.unify(x, y) => #<State @variables=[x, y], @values={x=>y}>

    >> state.values => {x=>y} >> state = state.unify(x, 5) => #<State @variables=[x, y], @values={x=>y, y=>5}> >> state.values => {x=>y, y=>5} >> state.value_of x => 5 >> state.unify(y, 6) => nil
  13. 24.

    class State def unify(a, b) a, b = value_of(a), value_of(b)

    if a == b self elsif a.is_a?(Variable) assign_values a => b elsif b.is_a?(Variable) assign_values b => a end end end
  14. 25.
  15. 29.

    >> state, (x, y, z) = State.new.create_variables [:x, :y, :z]

    => [#<State @variables=[x, y, z], @values={}>, [x, y, z]] >> goal = Goal.equal(x, 5) => #<Goal @block=#<Proc>> >> states = goal.pursue_in(state) => #<Enumerator: #<Enumerator::Generator>:each> >> states.next => #<State @variables=[x, y, z], @values={x=>5}> >> _.values => {x=>5} >> states.next StopIteration: iteration reached an end
  16. 30.

    def Goal.equal(a, b) Goal.new do |state| state = state.unify(a, b)

    Enumerator.new do |yielder| yielder.yield state if state end end end
  17. 32.

    >> goal = Goal.with_variables { |x| Goal.equal(x, 5) } =>

    #<Goal @block=#<Proc>> >> states = goal.pursue_in(State.new) => #<Enumerator: #<Enumerator::Generator>:each> >> state = states.first => #<State @variables=[x], @values={x=>5}> >> state.values => {x=>5}
  18. 33.

    def Goal.with_variables(&block) names = block.parameters. map { |type, name| name

    } Goal.new do |state| state, variables = state.create_variables(names) goal = block.call(*variables) goal.pursue_in state end end
  19. 36.

    >> goal = Goal.with_variables { |x| Goal.either(Goal.equal(x, 5), Goal.equal(x, 6))

    } => #<Goal @block=#<Proc>> >> states = goal.pursue_in(State.new) => #<Enumerator: #<Enumerator::Generator>:each> >> states.next.values => {x=>5} >> states.next.values => {x=>6}
  20. 37.

    def Goal.either(first_goal, second_goal) Goal.new do |state| first_stream = first_goal.pursue_in(state) second_stream

    = second_goal.pursue_in(state) first_stream.interleave_with(second_stream) end end
  21. 38.

    >> letters = 'a'.upto('z') => #<Enumerator: "a":upto("z")> >> numbers =

    1.upto(10) => #<Enumerator: 1:upto(10)> >> letters.interleave_with(numbers).entries => ["a", 1, "b", 2, "c", 3, "d", 4, "e", 5, "f", 6, "g", 7, "h", 8, "i", 9, "j", 10, "k", "l", "m", "n", "o", "p", "q", "r", "s", "t", "u", "v", "w", "x", "y", "z"]
  22. 39.

    >> letters = ['a', 'b', 'c'].cycle => #<Enumerator: ["a", "b",

    "c"]:cycle> >> numbers = 1.upto(Float::INFINITY) => #<Enumerator: 1:upto(Infinity)> >> letters.interleave_with(numbers).take(50) => ["a", 1, "b", 2, "c", 3, "a", 4, "b", 5, "c", 6, "a", 7, "b", 8, "c", 9, "a", 10, "b", 11, "c", 12, "a", 13, "b", 14, "c", 15, "a", 16, "b", 17, "c", 18, "a", 19, "b", 20, "c", 21, "a", 22, "b", 23, "c", 24, "a", 25]
  23. 40.

    class Enumerator def interleave_with(other) enumerators = self, other Enumerator.new do

    |yielder| until enumerators.empty? loop do enumerator = enumerators.shift yielder.yield enumerator.next enumerators.push enumerator end end end end end
  24. 42.

    >> goal = Goal.with_variables { |x, y| Goal.both( Goal.equal(x, 5),

    Goal.equal(y, 7) ) } => #<Goal @block=#<Proc>> >> states = goal.pursue_in(State.new) => #<Enumerator: #<Enumerator::Generator>:each> >> states.next.values => {x=>5, y=>7}
  25. 43.

    >> goal = Goal.with_variables { |a, b| Goal.both( Goal.equal(a, 7),

    Goal.either( Goal.equal(b, 5), Goal.equal(b, 6) ) ) } => #<Goal @block=#<Proc>> >> states = goal.pursue_in(State.new) => #<Enumerator: #<Enumerator::Generator>:each> >> states.next.values => {a=>7, b=>5} >> states.next.values => {a=>7, b=>6}
  26. 44.

    >> goal = Goal.with_variables { |x| Goal.both( Goal.equal(1, x), Goal.equal(x,

    2) ) } => #<Goal @block=#<Proc>> >> states = goal.pursue_in(State.new) => #<Enumerator: #<Enumerator::Generator>:each> >> states.next.values StopIteration: iteration reached an end
  27. 46.

    class Goal def pursue_in_each(states) Enumerator.new do |yielder| results = pursue_in(states.next)

    results = results.interleave_with(pursue_in_each(states)) results.each do |state| yielder.yield state end end end end
  28. 47.

    WITH VARIABLES G X Y … EQUAL V V STATE

    VARIABLE EITHER G G BOTH G G
  29. 48.
  30. 52.

    >> goal = Goal.with_variables { |x, y| Goal.equal( Pair.new(3, x),

    Pair.new(y, Pair.new(5, y)) ) } => #<Goal @block=#<Proc>> >> states = goal.pursue_in(State.new) => #<Enumerator: #<Enumerator::Generator>:each> >> state = states.first => #<State @variables=[x, y], @values={y=>3, x=>(5, 3)}> >> state.values => {y=>3, x=>(5, 3)}
  31. 53.

    class State def unify(a, b) a, b = value_of(a), value_of(b)

    if a == b self elsif a.is_a?(Variable) assign_values a => b elsif b.is_a?(Variable) assign_values b => a end end end
  32. 54.

    end end end elsif a.is_a?(Pair) && b.is_a?(Pair) state = unify(a.left,

    b.left) state.unify(a.right, b.right) if state class State def unify(a, b) a, b = value_of(a), value_of(b) if a == b self elsif a.is_a?(Variable) assign_values a => b elsif b.is_a?(Variable) assign_values b => a
  33. 55.

    else key end end end class State def value_of(key) if

    values.has_key?(key) value_of values.fetch(key)
  34. 56.

    else key end end end class State def value_of(key) if

    values.has_key?(key) value_of values.fetch(key) elsif key.is_a?(Pair) Pair.new( value_of(key.left), value_of(key.right) )
  35. 58.

    >> state.values => {y=>3, x=>(5, 3)} >> state.variables => [x,

    y] >> state.results 2 => [(5, 3), 3] >> state.result => (5, 3)
  36. 59.
  37. 61.

    EMPTY_LIST = :empty def to_list(array) if array.empty? EMPTY_LIST else first,

    *rest = array Pair.new(first, to_list(rest)) end end
  38. 63.

    def from_list(list) if list == EMPTY_LIST [] else first, rest

    = list.left, list.right [first, *from_list(rest)] end end
  39. 65.

    >> goal = Goal.with_variables { |x, y, z| Goal.equal( to_list([x,

    2, z]), to_list([1, y, 3]) ) } => #<Goal @block=#<Proc>> >> states = goal.pursue_in(State.new) => #<Enumerator: #<Enumerator::Generator>:each> >> states.next.values => {x=>1, y=>2, z=>3}
  40. 66.

    def append(a, b, c) Goal.either( Goal.both( Goal.equal(a, EMPTY_LIST), Goal.equal(b, c)

    ), Goal.with_variables { |first, rest_of_a, rest_of_c| Goal.both( Goal.both( Goal.equal(a, Pair.new(first, rest_of_a)), Goal.equal(c, Pair.new(first, rest_of_c)) ), append(rest_of_a, b, rest_of_c) ) } ) end
  41. 67.

    >> goal = Goal.with_variables { |x| append( to_list(['h', 'e']), to_list(['l',

    'l', 'o']), x ) } => #<Goal @block=#<Proc>> >> states = goal.pursue_in(State.new) => #<Enumerator: #<Enumerator::Generator>:each> >> state.next.result => ("h", ("e", ("l", ("l", ("o", :empty))))) >> from_list _ => ["h", "e", "l", "l", "o"]
  42. 68.

    >> goal = Goal.with_variables { |x| append( x, to_list(['l', 'o']),

    to_list(['h', 'e', 'l', 'l', 'o']) ) } => #<Goal @block=#<Proc>> >> states = goal.pursue_in(State.new) => #<Enumerator: #<Enumerator::Generator>:each> >> from_list states.next.result => ["h", "e", "l"]
  43. 69.

    >> goal = Goal.with_variables { |x, y| append( x, y,

    to_list(['h', 'e', 'l', 'l', 'o']) ) } => #<Goal @block=#<Proc>> >> states = goal.pursue_in(State.new) => #<Enumerator: #<Enumerator::Generator>:each> >> states.next.results(2) => [:empty, ("h", ("e", ("l", ("l", ("o", :empty)))))] >> _.map { |list| from_list(list) } => [[], ["h", "e", "l", "l", "o"]]
  44. 70.

    >> states.next.results(2). map { |list| from_list(list) } => [["h", "e"],

    ["l", "l", "o"]] >> states.next.results(2). map { |list| from_list(list) } => [["h"], ["e", "l", "l", "o"]] >> states = goal.pursue_in(State.new) => #<Enumerator: #<Enumerator::Generator>:each> >> states.next.results(2) => [:empty, ("h", ("e", ("l", ("l", ("o", :empty)))))] >> _.map { |list| from_list(list) } => [[], ["h", "e", "l", "l", "o"]]
  45. 71.

    >> states = goal.pursue_in(State.new) => #<Enumerator: #<Enumerator::Generator>:each> >> states.each do

    |state| p state.results(2).map { |list| from_list(list) } end [[], ["h", "e", "l", "l", "o"]] [["h"], ["e", "l", "l", "o"]] [["h", "e"], ["l", "l", "o"]] [["h", "e", "l"], ["l", "o"]] [["h", "e", "l", "l"], ["o"]] [["h", "e", "l", "l", "o"], []] => nil
  46. 72.
  47. 74.

    INC, ZERO = :+, :z def to_peano(number) if number.zero? ZERO

    else Pair.new(INC, to_peano(number - 1)) end end
  48. 78.

    def add(x, y, z) Goal.either( Goal.both( Goal.equal(x, ZERO), Goal.equal(y, z)

    ), Goal.with_variables { |smaller_x, smaller_z| Goal.both( Goal.both( Goal.equal(x, Pair.new(INC, smaller_x)), Goal.equal(z, Pair.new(INC, smaller_z)) ), add(smaller_x, y, smaller_z) ) } ) end
  49. 79.

    >> goal = Goal.with_variables { |x| add( to_peano(5), to_peano(3), x

    ) } => #<Goal @block=#<Proc>> >> states = goal.pursue_in(State.new) => #<Enumerator: #<Enumerator::Generator>:each> >> states.next.result => (:+, (:+, (:+, (:+, (:+, (:+, (:+, (:+, :z)))))))) >> from_peano _ => 8
  50. 80.

    >> goal = Goal.with_variables { |x| add( x, to_peano(3), to_peano(8)

    ) } => #<Goal @block=#<Proc>> >> states = goal.pursue_in(State.new) => #<Enumerator: #<Enumerator::Generator>:each> >> from_peano states.next.result => 5
  51. 81.

    >> goal = Goal.with_variables { |x, y| add( x, y,

    to_peano(8) ) } => #<Goal @block=#<Proc>> >> states = goal.pursue_in(State.new) => #<Enumerator: #<Enumerator::Generator>:each> >> states.each do |state| p state.results(2).map { |peano| from_peano(peano) } end [0, 8]
  52. 82.

    >> states = goal.pursue_in(State.new) => #<Enumerator: #<Enumerator::Generator>:each> >> states.each do

    |state| p state.results(2).map { |peano| from_peano(peano) } end [0, 8] [1, 7] [2, 6] [3, 5] [4, 4] [5, 3] [6, 2] [7, 1] [8, 0] => nil
  53. 83.

    def multiply(x, y, z) Goal.either( Goal.both( Goal.equal(x, ZERO), Goal.equal(z, ZERO)

    ), Goal.with_variables { |smaller_x, smaller_z| Goal.both( Goal.both( Goal.equal(x, Pair.new(INC, smaller_x)), add(smaller_z, y, z) ), multiply(smaller_x, y, smaller_z) ) } ) end
  54. 84.

    >> goal = Goal.with_variables { |x| multiply( to_peano(3), to_peano(8), x

    ) } => #<Goal @block=#<Proc>> >> states = goal.pursue_in(State.new) => #<Enumerator: #<Enumerator::Generator>:each> >> states.next.result => (:+, (:+, (:+, (:+, (:+, (:+, (:+, (:+, (:+, (:+, (:+, (:+, (:+, (:+, (:+, (:+, (:+, (:+, (:+, (:+, (:+, (:+, (:+, (:+, :z)))))))))))))))))))))))) >> from_peano _ => 24
  55. 85.

    >> goal = Goal.with_variables { |x, y| multiply( x, y,

    to_peano(24) ) } => #<Goal @block=#<Proc>> >> states = goal.pursue_in(State.new) => #<Enumerator: #<Enumerator::Generator>:each> >> states.take(8).each do |state| p state.results(2).map { |peano| from_peano(peano) } end [1, 24] [2, 12] [3, 8]
  56. 86.

    >> states = goal.pursue_in(State.new) => #<Enumerator: #<Enumerator::Generator>:each> >> states.take(8).each do

    |state| p state.results(2).map { |peano| from_peano(peano) } end [1, 24] [2, 12] [3, 8] [4, 6] [6, 4] [8, 3] [12, 2] [24, 1] => nil