Ruby Module
is an easy and great way to encapsulate related methods/constants and extend a class, but sometimes I find myself really want to tell it something about the current class.
A pseudo code of my "wish" looks like this: of course it's invalid, Ruby doesn’t allow us to do so.
class Post < ApplicationRecord
include Publishable(column_name: :published_at)
end
Recently I read a blog post about how the author learned from reading some ruby gems' implementation to allow including a module with argument. Sounds fun so I gave it a try.
Let's first see an example, this may not be practical but at the moment I couldn't think of other great ones so please bear with me.
module Publishable
def publish
update(published_at: Time.now)
end
end
class Post < ApplicationRecord
include Publishable
end
There's nothing wrong with this code but some may argue that the Publishable
module depends on the published_at
column name, if someone renames the column to published_on
then it'll be broken, it's coupled to the Post
class.
One way to solve it may look like this:
module Publishable
def publish
update(publishable_column_name => Time.now)
end
private
def publishable_column_name
raise NotImplementedError
end
end
class Post < ApplicationRecord
include Publishable
private
def publishable_column_name
:published_at
end
end
Now the Publishable
is relying on its "host"(Post
) to define the method publishable_column_name
to explicitly tell it the column name, otherwise it will raise the not implemented error. But the downside is the module's requirement leaks to Post
, and that's arguably break the purpose to use a module, which is to group all methods of a concept in one place.
I'm thinking this could be a use case for the technique I'm gonna show you in this blog post.
How the trick work is already introduced in the article above, let's fast forward to the end result now.
class Publishable < Module
def initialize(column_name)
@column_name = column_name
end
def included(base)
column_name = @column_name
base.class_eval do # self is Post class
define_method(:publish) do
update(column_name => Time.now) # self is Post object
end
end
end
end
class Post < ApplicationRecord
include Publishable.new(:published_at)
end
The notable things are
class Publishable < Module
, we're not declaring a module, instead we're making a class that inherits from modulePublishable
"module", hence no the knowledge is decoupleddefine_method(:publish) { }
instead of def publish
, latter will make you lose the context and you can't access column_name
variable anymoreI'm gonna borrow the code from the article, this is how Rubinius handles module and is identical to MRI Ruby:
def include?(mod)
if !mod.kind_of?(Module) or mod.kind_of?(Class)
raise TypeError, "wrong argument type #{mod.class} (expected Module)"
end
# snip
end
When we do include SomeModule
, this code will be executed to check if the target is a valid module. The trick is how we could manage to pass that check, that is "something is kind of Module and not kind of Class".
class Publishable < Module
end
Publishable.kind_of?(Module) # => true
Publishable.kind_of?(Class) # => true
Publishable.new(:published_at).kind_of?(Module) # => true
Publishable.new(:published_at).kind_of?(Class) # => false
With this trick you can pass whatever dependencies from the host to the module. Hope you find it useful, and let me know if you find a good use case with it in real world 😁Not sure if it's yet another case of abusing Ruby's flexibility to driver it too far, I do concern about its possible confusion when bring to teams in which people are most likely not aware of this kind of hack.
These 2 gems are using the same technique, and their full implementations are really easy to read and follow.
If the task could be achieved by sprinkling some ruby magic, then better do it rather than trying to find the perfect reg expression.
One day I was trying to convert a liquid img tag to markdown style, one tricky part is the image caption can be omitted.
img_1 = "{% img http//example.com/image.jpg %}"
img_2 = "{% img http//example.com/photo.jpg Image Caption %}"
regex = /{% img (\S+) (.*)\s?%}/
img_1.match(regex)
#<MatchData "{% img http//example.com/image.jpg %}" 1:"http//example.com/image.jpg" 2:"">
img_2.match(regex)
#<MatchData "{% img http//example.com/photo.jpg Image Caption %}" 1:"http//example.com/photo.jpg" 2:"Image Caption ">
The caption matched for img_2
contained a trailing space: Image Caption
. I was trying to tweak the regex to eliminate the trailing space but my limited knowledge couldn't get me there.
This is a good place to use gsub with a block, so I could further borrow the String#strip
power to get rid of the trailing space.
def convert_liquid_img_tag_to_markdown(text)
text.gsub(/{% img (\S+) (.*)\s*%}/) do |liquid_img_tag|
url = Regexp.last_match[1] # same as \1 for inline style
caption = Regexp.last_match[2].strip
""
end
end
convert_liquid_img_tag_to_markdown(img_1)
# => 
convert_liquid_img_tag_to_markdown(img_2)
# => 
The link above from thoughtbot mentioned the meaning of having an intention-revealing name, liquid_img_tag
in this case, but what's more powerful is the inside the block it's all ruby and matched strings, you can do whatever you want with it.
I'm still curious about the right regex to get rid of the trailing space, but for a simple, one-off task it saves a lot of time to ulitize the ruby power inside the gsub block.
Another example is logging the before/after:
def convert_liquid_github_tag(text)
return unless text
text.gsub(/{% gist (.+) %}/) do |match|
html = "<script src=\"https://gist.github.com/kinopyo/#{Regexp.last_match[1]}.js\"></script>"
puts "#{match} to #{html}"
html
end
end
$ rvm install 1.9.3
Searching for binary rubies, this might take some time.
Found remote file https://rvm.io/binaries/osx/10.9/x86_64/ruby-1.9.3-p448.tar.bz2
Checking requirements for osx.
Installing requirements for osx.
Updating system - using Zsh, can not show progress, be patient...
Error running 'requirements_osx_brew_update_system ruby-1.9.3-p448',
please read /Users/qihuan-piao/.rvm/log/1383014621_ruby-1.9.3-p448/update_system.log
Requirements installation failed with status: 1.
$ gcc -v
Configured with: --prefix=/Applications/Xcode.app/Contents/Developer/usr --with-gxx-include-dir=/usr/include/c++/4.2.1
Apple LLVM version 5.0 (clang-500.2.79) (based on LLVM 3.3svn)
Target: x86_64-apple-darwin13.0.0
Thread model: posix
rvm install 1.9.3 --with-gcc=clang
http://stackoverflow.com/questions/8139138/how-can-i-install-ruby-1-9-3-in-mac-os-x-lion
sudo gem install railsでこんなエラーが出ちゃいました。
Error installing rails bundler requires RubyGems version >= 1.3.6
解決策は
sudo gem update --system
pdating RubyGems
Updating rubygems-update
Successfully installed rubygems-update-1.6.1
Updating RubyGems to 1.6.1
Installing RubyGems 1.6.1
RubyGems 1.6.1 installed
=== 1.6.1 / 2011-03-03
Bug Fixes:
# Installation no longer fails when a dependency from a version that won't be
installed is unsatisfied.
# README.rdoc now shows how to file tickets and get help. Pull Request #40 by
Aaron Patterson.
# Gem files are cached correctly again. Patch #29051 by Mamoru Tasaka.
# Tests now pass with non-022 umask. Patch #29050 by Mamoru Tasaka.
------------------------------------------------------------------------------
RubyGems installed the following executables:
/System/Library/Frameworks/Ruby.framework/Versions/1.8/usr/bin/gem
を実行した後にsudo gem install railsでrailsをインストールすればOKです。
temperature = 34
puts temperature
Rubyではキャメルケースより、アンダースコアが好みだそうです。
例えば変数名page_transfer_managerはOKだけど、pageTransferManagerはNG。
FILE def in self
LINE defined? module super
BEGIN do next then
END else nil true
alias elsif not undef
and end or unless
begin ensure redo until
break false rescue when
case for retry while
class if return yield
age = 99
puts "My age: " + String(age)
puts "My age: " + age.to_s
puts "My age: #{age} "
整数を文字列に変換することに注意してください。
ダブルクォーテーション内であれば任意の式を#{ }に入れることができます。
PI = 3.1415926535
コンスタントは大文字でスタートします。
これでRubyはコンスタントと認識してくれます。
temperature = 34
puts temperature
To use underscores rather than "camel case" for multiple-word names.
page_transfer_manager is good, for example, but pageTransferManager is not.
FILE def in self
LINE defined? module super
BEGIN do next then
END else nil true
alias elsif not undef
and end or unless
begin ensure redo until
break false rescue when
case for retry while
class if return yield
age = 99
puts "My age: " + String(age)
puts "My age: " + age.to_s
puts "My age: #{age} "
Note that you need to convert the integer variable to string.
You can place any expression inside #{ and } in a double-quoted string
and have it interpolated into the text.
PI = 3.1415926535
Constants in Ruby start with an uppercase letter—that’s how Ruby
knows they are constants. In fact, the entire name of a constant is usually in uppercase. But it’s that first
letter that is crucial—it must be uppercase so that Ruby knows that you intend to create a constant.