Lock in $30 Savings on PRO—Offer Ends Soon! ⏳

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 https://tomstu.art/hello-declarative-world, and the code is available at http://github.com/tomstuart/kanren.

Tom Stuart

August 10, 2015
Tweet

More Decks by Tom Stuart

Other Decks in Programming

Transcript

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

    x is ? x is 3 x is 2 x is 1 x is 0
  2. >> x, y = Variable.new(:x), Variable.new(:y) => [x, y] >>

    x == x => true >> x == y => false >> x == Variable.new(:x) => false
  3. class State def initialize(variables = [], values = {}) @variables,

    @values = variables, values end attr_reader :variables, :values end
  4. >> 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}
  5. 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
  6. >> 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
  7. >> 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 => {}
  8. >> 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
  9. 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
  10. >> 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
  11. 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
  12. >> 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}
  13. 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
  14. >> 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}
  15. 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
  16. >> 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"]
  17. >> 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]
  18. 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
  19. >> 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}
  20. >> 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}
  21. >> 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
  22. 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
  23. WITH VARIABLES G X Y … EQUAL V V STATE

    VARIABLE EITHER G G BOTH G G
  24. >> 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)}
  25. 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
  26. 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
  27. else key end end end class State def value_of(key) if

    values.has_key?(key) value_of values.fetch(key)
  28. 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) )
  29. >> state.values => {y=>3, x=>(5, 3)} >> state.variables => [x,

    y] >> state.results 2 => [(5, 3), 3] >> state.result => (5, 3)
  30. EMPTY_LIST = :empty def to_list(array) if array.empty? EMPTY_LIST else first,

    *rest = array Pair.new(first, to_list(rest)) end end
  31. def from_list(list) if list == EMPTY_LIST [] else first, rest

    = list.left, list.right [first, *from_list(rest)] end end
  32. >> 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}
  33. 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
  34. >> 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"]
  35. >> 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"]
  36. >> 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"]]
  37. >> 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"]]
  38. >> 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
  39. INC, ZERO = :+, :z def to_peano(number) if number.zero? ZERO

    else Pair.new(INC, to_peano(number - 1)) end end
  40. 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
  41. >> 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
  42. >> 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
  43. >> 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]
  44. >> 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
  45. 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
  46. >> 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
  47. >> 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]
  48. >> 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