Last active
December 30, 2015 16:59
-
-
Save zobar/7858447 to your computer and use it in GitHub Desktop.
Chip: composable vector operations in Ruby.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/.yardoc | |
/.yardopts | |
/coverage | |
/doc |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
2.0.0-p353 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
require 'singleton' | |
# | |
# Composable function which takes any number of inputs and produces any number | |
# of outputs. | |
# | |
# Chips can be composed with {#&} or modified with {#shift}. Once composed, a | |
# chip can be invoked any number of times by using `#call`. `#call` takes one | |
# argument, an {::Array} containing the inputs, and returns an {::Enumerable} of | |
# the outputs. | |
# | |
# chip = Chip::Identity.instance.shift(2, true) | |
# result = chip.call([false, true, false]).to_enum | |
# | |
# result.next #=> true | |
# result.next #=> true | |
# result.next #=> false | |
# result.next #=> true | |
# result.next #=> false | |
# | |
# @abstract Subclasses must implement `#call(inputs)`. | |
# | |
class Chip | |
class ResultBase | |
include Enumerable | |
end | |
# | |
# Chip that performs a bitwise AND on each of the corresponding outputs of two | |
# other chips. | |
# | |
# output[n] = input_1[n] && input_2[n] | |
# | |
class And < Chip | |
class Result < ResultBase | |
def each | |
@a.zip(@b) { |input_a, input_b| yield input_a && input_b } | |
end | |
def initialize(chip, inputs) | |
@a = chip.a.call inputs | |
@b = chip.b.call inputs | |
end | |
end | |
private_constant :Result | |
attr_reader :a, :b | |
# | |
# Filters the inputs through both chips, returning the bitwise AND of the | |
# results. | |
# | |
# @param inputs [Enumerable<Boolean>] | |
# @return [Enumerable<Boolean>] | |
# | |
def call(inputs) | |
Result.new self, inputs | |
end | |
def initialize(a, b) | |
@a = a | |
@b = b | |
end | |
end | |
# | |
# Chip that copies its inputs to its outputs unchanged. | |
# | |
# output[n] = input[n] | |
# | |
class Identity < Chip | |
include Singleton | |
# | |
# Returns `inputs` unchanged. | |
# | |
# @param inputs [Enumerable<Boolean>] | |
# @return [Enumerable<Boolean>] | |
# | |
def call(inputs) | |
inputs | |
end | |
end | |
# | |
# Chip that returns true for an output only if the corresponding input, and | |
# all lower-numbered inputs, are false. | |
# | |
# output[n] = !input[n] && !input[n - 1] && ... && !input[0] | |
# | |
class None < Chip | |
include Singleton | |
class Result < ResultBase | |
def each | |
none = true | |
@inputs.each { |input| yield (none &&= !input) } | |
end | |
def initialize(inputs) | |
@inputs = inputs | |
end | |
end | |
private_constant :Result | |
# | |
# Returns a result where each output is true only if the corresponding | |
# input, and all lower-numbered inputs, are false. | |
# | |
# @param inputs [Enumerable<Boolean>] | |
# @return [Enumerable<Boolean>] | |
# | |
def call(inputs) | |
Result.new inputs | |
end | |
end | |
# | |
# Chip that performs a left shift on its inputs. Outputs are copied from the | |
# _nth_ previous input; the first _n_ outputs all return the `fill` value. | |
# | |
# output[n] = input[n - count] # n >= count | |
# output[n] = fill # n < count | |
# | |
class Shift < Chip | |
class Result < ResultBase | |
def each | |
i = @count | |
while i > 0 | |
yield @fill | |
i -= 1 | |
end | |
@inputs.each { |input| yield input } | |
end | |
def initialize(chip, inputs) | |
@count = chip.count | |
@fill = chip.fill | |
@inputs = chip.inner.call inputs | |
end | |
end | |
private_constant :Result | |
attr_reader :count, :fill, :inner | |
# | |
# Returns `inputs` shifted left by `count` places. | |
# | |
# @param inputs [Enumerable<Boolean>] | |
# @return [Enumerable<Boolean>] | |
# | |
def call(inputs) | |
Result.new self, inputs | |
end | |
def initialize(inner, count, fill) | |
@count = count | |
@fill = fill | |
@inner = inner | |
end | |
end | |
# | |
# Build a chip representing the bitwise AND of this chip's outputs with the | |
# `other` chip's outputs. | |
# | |
# @param other [Chip] | |
# @return [And] | |
# | |
def &(other) | |
And.new self, other | |
end | |
# | |
# Build a chip representing the left shift of this chip's outputs by `count` | |
# bits. The first `count` outputs will always equal `fill`. | |
# | |
# @param count [Fixnum] | |
# @param fill [Boolean] | |
# @return [Shift] | |
# | |
def shift(count, fill) | |
Shift.new self, count, fill | |
end | |
# | |
# Chip that returns true on only the output corresponding to the | |
# lowest-numbered true input. For instance, if input 0 is false and input 1 is | |
# true, output 0 will be false, output 1 will be true, and all higher-numbered | |
# outputs will be false regardless of the value of their corresponding inputs. | |
# | |
Priority = None.instance.shift(1, true) & Identity.instance | |
end |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/usr/bin/env ruby -w | |
require_relative 'chip.shard' | |
require 'ruby-prof' | |
n_inputs = ARGV.fetch(0, 10).to_i - 1 | |
inputs = [false, true] | |
samples = inputs.product(*([inputs] * (n_inputs))) | |
chip = Chip::Priority | |
results = nil | |
puts <<TXT | |
######### | |
# # | |
# Setup # | |
# # | |
######### | |
TXT | |
profile = RubyProf.profile do | |
results = samples.map { |sample| chip.call sample } | |
end | |
RubyProf::FlatPrinter.new(profile).print(STDOUT, sort_method: :total_time) | |
puts <<TXT | |
############## | |
# # | |
# Evaluation # | |
# # | |
############## | |
TXT | |
profile = RubyProf.profile do | |
results.each { |result| result.each { } } | |
end | |
RubyProf::FlatPrinter.new(profile).print(STDOUT, sort_method: :total_time) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
begin | |
require 'simplecov' | |
rescue LoadError | |
else | |
SimpleCov.start | |
end | |
require 'minitest/autorun' | |
require 'minitest/spec' | |
require_relative 'chip.shard' | |
describe Chip do | |
let(:identity) { Chip::Identity.instance } | |
let(:none) { Chip::None.instance } | |
describe Chip::And do | |
subject { chip.call([false, true, true, false]).to_a } | |
let(:chip) { identity & identity.shift(1, false) } | |
it 'false AND false is false' do | |
subject[0].must_equal false | |
end | |
it 'false AND true is false' do | |
subject[1].must_equal false | |
end | |
it 'true AND true is true' do | |
subject[2].must_equal true | |
end | |
it 'true AND false is false' do | |
subject[3].must_equal false | |
end | |
end | |
describe Chip::Identity do | |
subject { chip.call([false, true]).to_a } | |
let(:chip) { identity } | |
it 'passes through false' do | |
subject[0].must_equal false | |
end | |
it 'passes through true' do | |
subject[1].must_equal true | |
end | |
end | |
describe Chip::None do | |
let(:chip) { none } | |
describe 'with inputs [false, true, false, false]' do | |
subject { chip.call([false, true, false, false]).to_a } | |
it 'output 0 is true if input 0 is false' do | |
subject[0].must_equal true | |
end | |
it 'output 1 is false if output 1 is true' do | |
subject[1].must_equal false | |
end | |
it 'output 2 is false if output 1 is true' do | |
subject[2].must_equal false | |
end | |
it 'output 3 is false if output 1 is true' do | |
subject[3].must_equal false | |
end | |
end | |
describe 'with inputs [true, false, false]' do | |
subject { chip.call([true, false, false]).to_a } | |
it 'output 0 is false if input 0 is true' do | |
subject[0].must_equal false | |
end | |
it 'output 1 is false if input 0 is true' do | |
subject[1].must_equal false | |
end | |
it 'output 2 is false if input 0 is true' do | |
subject[2].must_equal false | |
end | |
end | |
end | |
describe Chip::Priority do | |
let(:chip) { Chip::Priority } | |
describe 'with inputs [false, true]' do | |
subject { chip.call([false, true]).to_a } | |
it 'output 0 is false if input 1 is false' do | |
subject[0].must_equal false | |
end | |
it 'output 1 is true if input 1 is true' do | |
subject[1].must_equal true | |
end | |
end | |
describe 'with inputs [true, true]' do | |
subject { chip.call([true, true]).to_a } | |
it 'output 0 is true if input 0 is true' do | |
subject[0].must_equal true | |
end | |
it 'output 1 is false if input 0 is true' do | |
subject[1].must_equal false | |
end | |
end | |
end | |
describe Chip::Shift do | |
subject { chip.call([false, true]).to_a } | |
let(:chip) { identity.shift(1, true) } | |
it 'extends the result' do | |
subject[0].must_equal true | |
end | |
it 'shifts the first input' do | |
subject[1].must_equal false | |
end | |
it 'shifts subsequent inputs' do | |
subject[2].must_equal true | |
end | |
end | |
end |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
All 1,048,576 possible combinations of 20 inputs to a Priority, processed in 1m20s. Each invocation of
Chip::Priority.call
results in 15 method calls: 10 to set up the result and intermediate Enumerables, and 3 to evaluate the expression. This gives a grand total of 15,728,640 method calls, excluding the test harness.