All objects are mutable by default. It can be nil
, using nil?
, is_a?
, to_a
, etc. to help the value validaiton for method input.
Avoid using Perl styled perlisms. Use String#match
instead of String#=~
with $1
varialbe. The ~=
special oeprator put the match result to special global variable $1 ... $n. Those variables are actually scoped locally to the current thread and method. So outside of the method, the $1
will be nil
if not assigned in outer scope yet.
We can requie('English')
to use descriptive aliases instead.
To make it immutable, freeze
will lock the modification for it. But if it reference an collection object such as array or hash, you need to freeze its elements and keys(if that is also an object) for the constraint of deep copy level.
module Defaults
SOME_CONFIG = ["val1", "val2", "val3"]
end
# only freeze array object reference, this array cannot add/remove, but can alter element value
SOME_CONFIG.freeze
# also lock element value.
SOME_CONFIG.map(:&.freeze).freeze
The constant maybe reassign to a new value, by this mean, we cannot lock it by constant.freeze
, we need to define an outer namespace through module and freeze it in where thet defined in.
module ConfigConstants
SOME_CONFIG = ["val1", "val2", "val3"]
end
ConfigConstants.freeze
ConfigConstants::SOME_CONFIG.map(:&.freeze).freeze
ConfigConstants::SOME_CONFIG.freeze
# => 5
ConfigConstants::SOME_CONFIG = 9
# (irb):9: warning: already initialized constant ConfigConstants::SOME_CONFIG
# (irb):6: warning: previous definition of SOME_CONFIG was here
# => 9
ConfigConstants.freeze
# => ConfigConstants
ConfigConstants::SOME_CONFIG = 9
# RuntimeError: can't modify frozen Module
The Array
and String
as module or class are actually constants.
The varialbe assignment is not locked yet until its top level such as class or module is frozen as well. Then no modification can performed in that class or module. Similar idea to the Array#freeze
which lock its modification to add/remove
.
We can conculde that the freeze
is only constraint the object on shallow copy level.
super
without explicitly paranthesis will act like syntax sugar:
class Bae < Parent
def speak(word, accent)
# actually call Parent.speak(word, accent) assume with same parameter list of subclass
super
end
end
If your want to call superclass method without parameters safely, use suepr()
.
For the mixin module and class, calling super
will stop at first match which is the method locate in module. In this case, use composition instead of inheritance to avoid tightly coupling.
Be aware not to override default method_missing
in inheritance chain, If super
cannot find the method, then it will call method_missing
in inheritance chain which might lost detail for NoMethodError
.
Furthermore, if the overriden method_missing
is not defined in same class definfition, then it will not with details where it actually occurred.
https://ruby-doc.org/core-2.2.0/Class.html#method-c-new
When a new class is created, an object of type Class is initialized and assigned to a global constant (Name
in this case).
class Class
alias old_new new
def new(*args)
print "Creating a new ", self.name, "\n"
old_new(*args)
end
end
class Name
end
n = Name.new
# Creating a new Name
new
keyword actually create an anonymous class object from the superclass. So we can access that object's instance variable seperately.
Creates a new anonymous (unnamed) class with the given superclass (or Object if no parameter is given). You can give a class a name by assigning the class object to a constant.
clone
has 2 features while dup
does not contain:
clone
will keepfrozen
state of the receiver, whiledup
is not.- If the reveiver has singleton methods,
clone
will also duplicate its singleont class.
Both clone
and dup
are shallow copy, so as Java's clone
. You need to manually init object memebers if you want deep copy.
Don't pass hash to array methods, it will convert to a nested array.
Using Set
for element inclusion checking. O(logn) [set] vs O(n) [array].
Consider using default hash value:
default_value = 43
hash = Hash.new(default_value)
# default value
hash[:not_exist]
# 43
But the default value will not populate nonexistent key:
# still empty, thogh has default value
hash.keys
[]
Intead of simply setting default value, try create hash with initialization block, this will not share the deafult value anymore, each nonexistent keys's value are independently.
# default value, share to all nonexistent keys
hh = Hash.new([])
hh[:non_exist1] << 1
# [1]
hh[:non_exist2] << 1
# [1, 1]
hh[:non_exist3]
# [1, 1]
# use init block
h = Hash.new do |k, v|
[]
end
h[:weekendays]
# []
require 'forwardable'
class SomeHash
extend(Forwardable)
# foward instance method to it
def_delegators(:@hash, :[], :[]=, :empty?)
# forward singleton method erase! to hash instance method
def_delegator(:@hash, :delete, :erase!)
# default_proc is the init block given to Hash.new
def replace!(hash)
hash.default_proc = @hash.default_proc
@hash = hash
end
end
If you want to set SomeHash
as frozen, then you need to freeze @hash
as well, it could either override freeze
method, or use taint
to the same way. untaint
is the contrary to taint
method.
Prefer using custom exception message.
# simply return self exception without do anything
raise CustomException
# with single argument return as message
raise CustomException, "error message"
begin
create a new scope for exceptions, use ensure
to release the resource. The scope of ensure
keep in the same as begin
body. Variables defined in begin
will also available to ensure
block. That is why File.open
usually suggest use default block to make sure its IO is ensured to closed.
Make sure to put ensure
at the end, instead of just rescue
, basically rescue will discard any exceptions which leads the error untrackable. So below pattern is often to use:
def explicit
return 'horses'
rescue SomeError => e
# recover from this type of exception.
return 'ponies'
ensure
# resource releasing.
return result
end
Note that return output will alwasy be the result
from ensure
block, not horses
.
Use catch...throw
instead of raise
.
In below example, throw
is used in catch
block, it will terminate that block and return its given value [another_result]
:
match = catch(:jump) do
if not_right
# jump back to where that label locate.
throw(:jump, [another_result])
end
end
Extend a class trigger self.extended
hook method. This is happened after all the definitions(all extend
modules) in a class are loaded.
module ExtendHook
# expect first arg as class or other mod,
# depends on how extend keyword received its first argument.
def self.extended(klass)
require("other-modules")
klass.extend(Forwardable)
end
end
module UseCase
extend ExtendHook
end
https://ruby-doc.org/core-2.2.0/Module.html#method-i-extended
included
, prepended
, extended
are all similar mechanisms. inherited
to intercept a class definition during class inhertiance. We can inject the exception handler for its inheritance behvaior.
Also, method added, removed, indefined could trigger below hooks:
class InstanceMethodMonitor
def self.method_added(m); end
def self.method_removed(m); end
def self.method_undefined(m); end
# trigger method_added
def hello; end
# trigger method_removed
remove_method(:hello)
# tigger method_undefined
undef_method(:hello)
end
In singleton class level, consider these hooked methods:
class SingletonMethodsMonitor
def self.singleton_method_added(m); end
def self.singleton_method_removed(m); end
def self.singleton_method_undefined(m); end
def self.hello; end
class << self; remove_method(:hello); end
def self.hello; end
class << self; undef_method(:hello); end
end
method_missing
is untrackable when user call obj.respond_to? :method
or obj.public_methods(false)
:
class HashProxy
def initialize
@hash = {}
end
def method_missing(name, *args &block)
if @hash.response_to?(name)
@hash.send(name, *args, &block)
end
end
end
h = HashProxy.new
h.size
# 0
h.respond_to? :size
# false
Now, when we want to use decorator class to forward all methods to decorated object, method_missing
will intercept and call at decorator level, but we should pass the calling of existed methods in decorator class to decorated object and let it decide. This is where define_method
comes in, following class use anonymous module:
class SomeDecorator
def initialize(object)
@obj = object
@logger = Logger.new($stdout)
mod = Module.new do
object.public_instance_methods.each do |name|
# this block in current SomeDecorator class scope
define_method(name) do |*args, name, &block|
@logger.info("some info")
@obj.send(name, )
end
end
end
extend(mod)
end
end
Regarding the scope, closure for define_method
, it use instance_eval
:
http://stackoverflow.com/questions/10206193/ruby-define-method-and-closures
So, can we return from a
Proc
the “normal” way? Yes, if we use thenext
keyword, instead ofreturn
. We can re-writemethod_b
so that it’s functionally the same tomethod_a
:
def method_b
res = proc { next "return from proc" }.call
return "method b returns #{res}"
end
puts method_b
Since the block won't create a scope, so it is a closure, so does proc
. But lambda
create a scope and memoized its reference vars:
outer = 1
def m a_var
inner = 99
puts "inner var = #{inner}"
# a_var is memoized
lambda {inner + a_var}
end
p = m(outer)
puts "p is a #{p.class}"
outer = 0
puts "changed outer to #{outer}"
puts "result of proc call: #{p.call}"
#=> inner var = 99
#=> p is a Proc
#=> changed outer to 0
#=> result of proc call: 100
https://www.sitepoint.com/closures-ruby/
Beward of isntance_eval
, eval
, and class_eval
(module_eval
).
instance_eval
opent an instance's singleton class and inject instance method in singleton class, treat as singleton_method for the class.
For class_eval
it open that class in lexical scope and inject the code snippet for it.
Because class, objects, and modules are always open in Ruby, so may have patch collision issue when you monkey patching in same class but different run time execution phase. We can use new feature refinements from Ruby 2.1, as follow:
module OnlySpace
refine(String) do
def only_space?
# ...
end
end
end
# the use using in pachted module
class Person
using(OnlySpace)
def initialize
@name = name
end
def valid?
!@name.only_space?
end
def display(io=$stdout)
io.puts @name
end
end
Note that refinements
only activated in current lexical scope, out of that module or class, refinements will automatically deactivated. So the string(@name
) passed to puts
in display
method is actually leave current lexical scope, then only_space?
refinement will deactivated.
Once the control leaves
display
and enterputs
, the refinements defined inOnlySpace
are deactivated.
When we mention the constraint about lexical scope, it means the inherited subclass will not be able to use that refinements from superclass as well.
Block may have different arity with different behavior, consider supporting this difference in Proic.arty
:
def stream(&block)
loop do
start = Time.now
data = @io.read(@chunk)
return if data.nil?
arg_count = block.arity
arg_list = [data]
if arg_count == 2 || ~arg_count == 2
# arg_list << ...
end
block.call(*arg_list)
end
end
The ~
unary complement operator will turn the optional arguments into the number of requirement arguments:
func = ->(x, y=1) { x + y}
# => #<Proc:0x007fa2c3108980@(irb):1 (lambda)>
# if have optional arguments, turn it to negative from one requirement + at least one optional as requirement
func.arity
# => -2
~func.arity
# => 1
func = ->(x, z, y=1, g=1) { x + y}
func.arity
# -3
Think carefully for both the prepend order in syntax, and its actual inheritance chain:
module A; end
module B; end
class C
include(A)
include(B)
end
C.ancestors
# [C, B, A, Object, Kernal, BasicObject]
class D
prepend(A)
prepend(B)
end
D.ancestors
# [A, B, D, Object, Kernal, BasicObject]
It reads the prepend
and include
from buttom-up manor [B, A]
, then append by this order, or prepend by reverse order.
prefer using suffix _test
for test file name. require('minitest/autorun'
will load unit testing, spec testing, and mocking 3 components automatically. To define an unit test, inherit MiniTest::Unit::TestCase
and prefix test_
in each testing instance method:
class SomeTest < MiniTest::UnitTest::TestCase
def test_something
# assert something
end
end
Minitest
randomizes the methods before executing each test case. We can explore assertion methods from MiniTest::Assertions
module documentation. For example assert_equal(expect_value, actual_value, failure_message)
MiniTest
only invoke methods which prefix with test_
, so you can add helper methods without that prefix.
Each describe
keyword inherited MiniTest::Spec
class, the argument can be any object, it convert to string internally as label for the new, anonymous class. A test is defined by it
keyword inside describe
block. In spec testing, assertions have been replaced with monkey patching that allows you to call must_equal
to any object.
- Prefix
assert_
in unit testing is similar asmust_
prefix in spec testing. - Prefix
wont_
in unit testing is similar asrefute_
prefix in spec testing.
To mock the object in MiniTest, create a blank object by MiniTest::Mock::new
which is eager to prepend to be any other object. Then target on mocked object's method to add expect method symbol and return value to mock the behvior:
class Monitor
def is_alive?
resp = get(echo)
resp.success? && resp.body == echo
end
def get(echo);
# return a response ...
end
end
def test_successful_monitor
monitor = Monitor.new("target.object.com")
response = MiniTest::Mock.new
# mocking get method's behavior, and its return value
monitor.define_singleton_method(:get) do |echo|
# return mocked object instead.
response.expect(:success?, true)
response.expect(:body, echo)
response
end
response.verify
end
The verify
method will check whether any of the expected methods were called. It will raise exception which cause test fail if not all methods were called.
Testing should not be only positive, add fuzzy testing and property testing to make your code robust. Both are feeding random data to ensure the executed functionality still work as expected. Fuzzy testing focus on finding out the crashes or security hole.
The fuzzy test should made up of two parts, the generator
and test
, ex fuzzybert
gem:
https://github.com/krypt/FuzzBert
require 'fuzzbert'
fuzz('URI::HTTP.build') do
data('random server names') do
FuzzBert::Generators.random
end
deploy do |data|
URI::HTTP.build(host: data, path: '/')
end
end
Fuzzy testing is usually not terminated automatically, so the longer the test running, the code is more confident to be secured. For fuzzy testing, you should leave the test for several days to make some degree of confidence for your program.
Property testing:
http://www.scalatest.org/user_guide/property_based_testing
where a property is a high-level specification of behavior that should hold for a range of data points. For example, a property might state that the size of a list returned from a method should always be greater than or equal to the size of the list passed to that method. This property should hold no matter what list is passed.
The difference between a traditional test and a property is that tests traditionally verify behavior based on specific data points checked by the test. A test might pass three or four specific lists of different sizes to a method under test that takes a list, for example, and check the results are as expected. A property, by contrast, would describe at a high level the preconditions of the method under test and specify some aspect of the result that should hold no matter what valid list is passed.
We can use MrProper
ruby gem :
require 'mrproper'
class Version
def initialize(version)
@major, @minor, @patch = version.split(".").map(&:to_i)
end
def to_s
[@major, @minor, @patch].join(".")
end
# we can have assumption @major must > 1 or some rules
# these rules as assumptions can be examined by property testing
# not just presumed data ponits feed in test then expect presumed output correct only.
end
propertties("Verison") do
data([Integer, Integer, Integer])
property("new(str).to_s should always equal to str") do |data|
str = data.join('.')
assert_equal(str, Version.new(str).to_s)
end
end
-
Ruby use
mark and sweep
with generational garbage collector. -
First phase is traverse object graph and mark alive objects.
-
Second phase will sweep away unmarked objects and release memory back to Ruby memory pool then possibly release to operating system.
-
Mark phase hase major and minor mode:
- major is expensive, all objects are considered for marking. The generation will not considered in this phase.
- minor marking only consider young objects and automatically mark old objects without checking to see if they were reachable. This means old object can only be swept away in major marking.
- The garbage collector has some threshold to trigger major marking.
-
Sweep phase hase immediate and lazy two phases:
- immediate phase will free all unmarked objects.
- lazy free minimum of objects, each time you create ruby object, ruby might trigger lazy sweeping to free some space.
- Objects are divided into young and old two generations by its surviving time.
- If old objects is marked as reachable, then skipping entire sections of the object graph while traversing.
- Ruby's memory pool a.k.a heap(not data structure), which divided into pages and subdivided into slots.
- Each slot holds a single ruby object.
- Ruby only ask the operating system for memory when pool is empty.
- If ruby cannot find empty slot for storing new object, it will attempt a lazy sweeping. If it still cannot find empty slot, then allocate a new page to heap. During sweeping phase, if all slots in page are released, it might return the page to operating system. The memory footprint of a ruby process will appear to grow and shrink over time. From the inside, a ruby process grow and shrink by pages and not individual objects.
Revisit closure:
http://www.skorks.com/2010/05/closures-a-simple-explanation-using-ruby/
How do they retain the values of the variables that were in scope when the closure was defined? This must be supported by the language and there are two ways to do that.
The closure will create a copy of all the variables that it needs when it is defined. The copies of the variables will therefore come along for the ride as the closure gets passed around.
The closure will actually extend the lifetime of all the variables that it needs. It will not copy them, but will retain a reference to them and the variables themselves will not be eligible for garbage collection (if the language has garbage collection) while the closure is around.
Point 2 is how ruby does things.
https://aprescott.com/posts/variables-closures-and-scope
Since
foo
opens a new level of scope,x
has no associated value insidefoo
. Scope can be understood as a set of identifier-value associations which are visible within some hierarchy in the program. Any method definition in Ruby opens up its own scope, clearing out the identifiers currently visible within the method body.
def foo
x
end
def bar(b)
b.eval("x")
end
x = 1
current_binding = binding
foo #=> NameError, undefined local variable or method `x'
bar(current_binding) #=> 1
By passing around a
Binding
object explicitly, we can keep a hold of associations and use them in place of some other set of associations. That is what’s happening inbar
.
Binding is actually ref to that associated enviroment variables:
x = 1
@l = lambda { x }
def foo
# opens a new scope to make the point about which
# binding we're changing
x = 2
puts x
puts @l.binding.eval("x")
@l.binding.eval("x = 50")
puts x
end
x #=> 1
foo #=> 2
# 1
# 2
x #=> 50
Scope will be created when:
- Reach the beginning of your scope (a
def
/class
/module
/do
-end
block)- Reach the code that does the assignment to that local variable.
https://www.sitepoint.com/understanding-scope-in-ruby/
A closure is simply code containing behavior that can:
- be passed around like an object (which can be called later)
- remember the variables that were in scope when the closure (lambda in this example) was defined.
def foo
x = 1
lambda { x }
end
x = 2
p foo.call
# 1
http://awaxman11.github.io/blog/2013/08/05/what-is-the-difference-between-a-block/
Profiling tools: objspace
, stackprof
, or memory_profiler
.
# actually create 3 string object + 1 array => 4 objects
# 4 * n errors
errors.any? { |e| %w(F1, F2, F3) }
# more efficient way
"F1 F2 F3".split
Better store the string literals into a constant oust side of loops, which only create once:
FATAL = %w(F1 F2 F3).map(&:freeze).freeze
errors.any? { |e| FATAL }
The frezze
guarantee string literal will never mutate. In ruby 2.1, we can use frezze
directly to string literal object only, which equivalent to constant. This freeze
trick is not work on arbitrary other string objects.
errors.any? { |e| "FATAL".freeze }
Consider using ||=
to memoize your expansive operation, but if side effect occurs in later execution, we need to reconsider it:
# memo db orm operation
@memo ||= User.find(:some_id)
# if memo is updated(side effect), then you need to reload to get updated data
@memo.reload
@memo ||= expression
# syntax sugar as
@memo || @memo = expression
https://ruby-doc.org/core-2.2.0/Class.html#method-c-new
https://ruby-doc.org/core-2.2.0/Module.html#method-i-extended
http://stackoverflow.com/questions/10206193/ruby-define-method-and-closures
https://www.sitepoint.com/closures-ruby/
https://github.com/krypt/FuzzBert
http://www.scalatest.org/user_guide/property_based_testing
http://www.skorks.com/2010/05/closures-a-simple-explanation-using-ruby/
https://aprescott.com/posts/variables-closures-and-scope
https://www.sitepoint.com/understanding-scope-in-ruby/
http://awaxman11.github.io/blog/2013/08/05/what-is-the-difference-between-a-block/