33
loading...
This website collects cookies to deliver better user experience
This series is a partial rewrite and update of my own series on Medium on Functional Programming. It has been modernized and updated a bit.
x = 5
x += 2 # Mutation of state!
def remove(array, item)
array.reject! { |v| v == item }
end
array = [1,2,3]
remove(array, 1)
# => [2, 3]
array
# => [2, 3]
def remove(array, item)
array.reject { |v| v == item }
end
array = [1,2,3]
remove(array, 1)
# => [2, 3]
array
# => [1, 2, 3]
reverse
to test the output of a game board. It would look fine, but when I added one more reverse
to it all of my tests broke!reverse
function was mutating my board.In Ruby, frequently state mutations are indicated with !
as a suffix. Not always, though, because methods like concat
break those rules so keep an eye out.
def add(a, b)
a + b
end
def count_by(array, &fn)
array.each_with_object(Hash.new(0)) { |v, h|
h[fn.call(v)] += 1
}
end
count_by([1,2,3], &:even?)
# => {false=>2, true=>1}
Note: Newer versions of Ruby have the tally
function which would be used like this to get a similar result: [1, 2, 3].map(&:even?).tally
class RubyClub
attr_reader :members
def initialize
@members = []
end
def add_member
print "Member name: "
member = gets.chomp
@members << member
puts "Added member!"
end
end
gets
is going to pause the test, waiting for input, and puts
is going to return nil
afterwards.describe '#add_member' do
before do
$stdin = StringIO.new("Havenwood\n")
end
after do
$stdin = STDIN
end
it 'adds a member' do
ruby_club = RubyClub.new
ruby_club.add_member
expect(ruby_club.members).to eq(['Havenwood'])
end
end
STDIN
(standard input) to make it work which makes our test code a lot harder to read as well.class RubyClub
attr_reader :members
def initialize
@members = []
end
def add_member(member)
@members << member
end
end
describe '#add_member' do
it 'adds a member' do
ruby_club = RubyClub.new
expect(ruby_club.add_member('Havenwood')).to eq(['Havenwood'])
end
end
IO
(puts
, gets
), another form of state.class SampleLoader
SAMPLES_DIR = '/samples/ruby_samples'
def initialize
@loaded_samples = {}
end
def load_sample(name)
@loaded_samples[name] ||= File.read("#{SAMPLES_DIR}/#{name}")
end
end
elixir_samples
or rust_samples
? We have a problem. Our constant has become a piece of static state we cannot change.class SampleLoader
def initialize(base_path)
@base_path = base_path
@loaded_samples = {}
end
def load_sample(name)
@loaded_samples[name] ||= File.read("#{@base_path}/#{name}")
end
end
class SampleLoader
SAMPLES_DIR = '/samples/ruby_samples'
def initialize(base_path: SAMPLES_DIR)
@base_path = base_path
@loaded_samples = {}
end
def load_sample(name)
@loaded_samples[name] ||= File.read("#{@base_path}/#{name}")
end
end
class RubyClub
def initialize
@members = []
end
def add_member(member)
@members << member
end
def load_members(path)
JSON.parse(File.read(path)).each do |m|
@members << m
end
end
end
JSON
format. It makes our loader very inflexible.SQLite
, or maybe even just use YAML
instead. That’s a very hard task with the code like it is.class RubyClub
attr_reader :members
def initialize(members = [])
@members = members
end
def add_members(*members)
@members.concat(members)
end
end
new_members = YAML.load(File.read('data.yml'))
RubyClub.new(new_members)