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. HELLO, DECLARATIVE WORLD http://codon.com/hello-declarative-world

  2. None
  3. None
  4. UNSTRUCTURED PROCEDURAL FUNCTIONAL DECLARATIVE IMPERATIVE MORE ABSTRACT

  5. PROGRAMMER (A HUMAN) SOLVER (A MACHINE) HOW WHAT

  6. UNSTRUCTURED PROCEDURAL FUNCTIONAL RELATIONAL DECLARATIVE IMPERATIVE MORE ABSTRACT

  7. “FUNCTION” 19 23 42 x y x + y

  8. “RELATION” y is 5 x = y x is 5

    y is ?
  9. “RELATION” x is 4 x < y y is 5

    x is ? x is 3 x is 2 x is 1 x is 0
  10. LET’S BUILD OUR OWN!

  11. GOAL STATE STATE STATE STATE ⋮

  12. VARIABLES

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

    x == x => true >> x == y => false >> x == Variable.new(:x) => false
  14. class Variable def initialize(name) @name = name end def inspect

    @name end end
  15. STATES

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

    @values = variables, values end attr_reader :variables, :values end
  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}
  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
  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
  20. class State def value_of(key) if values.has_key?(key) value_of values.fetch(key) else key

    end end end
  21. UNIFICATION

  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 => {}
  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
  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
  25. GOALS

  26. class Goal def initialize(&block) @block = block end def pursue_in(state)

    @block.call state end end
  27. BASIC GOALS

  28. EQUAL VALUE VALUE

  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
  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
  31. WITH VARIABLES GOAL X Y …

  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}
  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
  34. COMBINING GOALS

  35. EITHER GOAL GOAL

  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}
  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
  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"]
  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]
  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
  41. BOTH GOAL GOAL

  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}
  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}
  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
  45. def Goal.both(first_goal, second_goal) Goal.new do |state| states = first_goal.pursue_in(state) second_goal.pursue_in_each(states)

    end end
  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
  47. WITH VARIABLES G X Y … EQUAL V V STATE

    VARIABLE EITHER G G BOTH G G
  48. µKANREN

  49. WHAT CAN IT DO?

  50. >> pair = Pair.new(5, 9) => (5, 9) >> pair.left

    => 5 >> pair.right => 9
  51. Pair = Struct.new(:left, :right) do def inspect "(#{left.inspect}, #{right.inspect})" end

    end
  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)}
  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
  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
  55. else key end end end class State def value_of(key) if

    values.has_key?(key) value_of values.fetch(key)
  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) )
  57. class State def results(n) variables.first(n). map { |variable| value_of(variable) }

    end def result results(1).first end end
  58. >> state.values => {y=>3, x=>(5, 3)} >> state.variables => [x,

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

  60. ['a', 'b', 'c'] ('a', ('b', ('c', •)))

  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
  62. >> to_list ['a', 'b', 'c'] => ('a', ('b', ('c', :empty)))

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

    = list.left, list.right [first, *from_list(rest)] end end
  64. >> from_list \ Pair.new('a', Pair.new('b', Pair.new('c', EMPTY_LIST) ) ) =>

    ['a', 'b', 'c']
  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}
  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
  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"]
  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"]
  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"]]
  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"]]
  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
  72. NUMBERS

  73. 3 (+, (+, (+, •)))

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

    else Pair.new(INC, to_peano(number - 1)) end end
  75. >> to_peano 3 => (:+, (:+, (:+, :z)))

  76. def from_peano(peano) if peano == ZERO 0 else from_peano(peano.right) +

    1 end end
  77. >> from_peano \ Pair.new(INC, Pair.new(INC, Pair.new(INC, ZERO) ) ) =>

    3
  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
  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
  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
  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]
  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
  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
  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
  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]
  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
  87. BE DECLARATIVE

  88. PROGRAMMER (A HUMAN) SOLVER (A MACHINE) HOW WHAT

  89. THANKS. @tomstuart http://codon.com/hello-declarative-world