Handle dynamic argument in dynamic language

In dynamic language, most language doesn’t provide the function overload mechanism like static language does, which means you cannot define functions with same name but different arguments, and the language itself won’t help you to dispatch the call according to the arguments.
So you have to deal with the overload by yourself in dynamic languages.

In dynamic language, since you have to dispatch the call by yourself, so you can play some tricks during process the arguments. The most common trick is grammar sugar, which can help you simplify the code and increase the readability. And it is a very important feature when you’re building DSL.

Syntax sugar in argument means you can omit something unnecessary or unimportant parameters in specific context, then the function will try to infer and complete the parameters. For example, developer should be able to omit the password parameter if the system enable authentication mechanism. These kind of tricks are very common in dynamic language frameworks, such as jQuery.

Here is a list of some common grammar sugar cases:

  • Set default value for omit parameter, such as for jQuery animation function jQuery.fadeIn([duration] [, callback] ), you can omit the parameter duration, which is equivalent to provide duration as 400
  • Provide different type of value for a same parameter, still the jQuery animation function jQuery.fadeIn([duration] [, callback] ), you can provide number for duration, or you can provide string “fast” and “slow” as the value of duration.
  • Provide a single value rather than a complex hash, such as function jQuery ajax function jQuery.ajax(settings), you can provide a url string, which is equivalent to provide a hash{url: <url string>}
  • Pass a single element instead of a array of the element.

After we analyze the cases, we will find that all the grammar sugar is to allow user to provide exactly same information but in different formats. Since all the data are same piece of information, so it should be possible to unify all the information into one format! Which means to support grammar sugar, the major problem is to unify the type of parameter.
Besides unify the type, another important problem is to handle the null values, such asnull and undefined in javascript, nil in ruby, etc.

Here is a piece of ruby code that I parse the argument by unifying the parameter type:

code
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
def apply_to_items(options = nil)
options = unify_type(options, Hash) { |items| {:only => items} }
options[:only] = unify_type(options[:only], Array) { |item| item.nil? ? list_all_items : [item] }
options[:except] = unify_type(options[:except], Array) { |item| item.nil? ? [] : [item] }
options[:only] = unify_array_item_type(options[:only], String) { |symbol| symbol.to_s }
options[:except] = unify_array_item_type(options[:except], String) { |symbol| symbol.to_s }
target_items = options[:only].select { |item| options[:except].exclude? item }
target_items.each do |item|
yield item
end
end
private
def list_all_items
# return all items fetched from database or API
# ...
end
def unify_type(input, type)
if input.is_a?(type)
input
else
yield input
end
end
end

And here is the test for the code:

title
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
require 'spec_helper'
describe Module do
before do
extend Moudle
end
it "should populate all items" do
visited = []
apply_to_items do |item|
visited << item
end
visited.should =~ %w(public another_item)
end
describe "should populate the provided items" do
it "provide as string array" do
visited = []
apply_to_items(%w(another_item)) do |item|
visited << item
end
visited.should =~ %w(another_item)
end
it "provide as symbol array" do
visited = []
apply_to_items([:another_item]) do |item|
visited << item
end
visited.should =~ %w(another_item)
end
it "provide as string item" do
visited = []
apply_to_items('another_item') do |item|
visited << item
end
visited.should =~ %w(another_item)
end
it "provide as symbol item" do
visited = []
apply_to_items(:another_item) do |item|
visited << item
end
visited.should =~ %w(another_item)
end
it "provide as string array in hash" do
visited = []
apply_to_items(:only => %w(another_item)) do |item|
visited << item
end
visited.should =~ %w(another_item)
end
it "provide as symbol array in hash" do
visited = []
apply_to_items(:only => [:another_item]) do |item|
visited << item
end
visited.should =~ %w(another_item)
end
it "provide as string item in hash" do
visited = []
apply_to_items(:only => 'public') do |item|
visited << item
end
visited.should =~ %w(public)
end
it "provide as symbol item in hash" do
visited = []
apply_to_items(:only => :public) do |item|
visited << item
end
visited.should =~ %w(public)
end
end
describe "should except the not used items" do
it "except as string item in hash" do
visited = []
apply_to_items(:except => 'public') do |item|
visited << item
end
visited.should =~ %w(another_item)
end
it "except as symbol item in hash" do
visited = []
apply_to_items(:except => :public) do |item|
visited << item
end
visited.should =~ %w(another_item)
end
it "except as string array in hash" do
visited = []
apply_to_items(:except => %w(public)) do |item|
visited << item
end
visited.should =~ %w(another_item)
end
it "except as symbol array in hash" do
visited = []
apply_to_items(:except => :public) do |item|
visited << item
end
visited.should =~ %w(another_item)
end
end
end

The algorithm in previous code is language independent, so ideally, it could be reused in any language, such as java script or python.

The "Cursed" NULL in postgres

Comparison to NULL

In postgres, NULL is treat as a speical value, that which is not equal to any other value, which means the expression NULL = NULL yields false.
It can be verified by using the following query

SELECT NULL
1
2
3
4
5
SELECT n
FROM unnest(ARRAY(NULL,1,2,3,4,5)) n
WHERE n = NULL

The query returns empty set, because no element equals to NULL even NULL itself.
If you think this experiment is not convincing enough, then you can try this:

CASE NULL
1
2
3
4
5
6
SELECT
n,
CASE WHEN n = NULL THEN 'NULL' ELSE 'NOT NULL' END
FROM unnest(ARRAY(NULL,1,2,3,4,5)) n

This query should yield “ , NOT NULL” which means NULL does not equal to NULL.

To test whether or whether not a value equals to NULL, you should use IS NULL or IS NOT NULL

So if you replace the n = NULL with n IS NULL in previous 2 statements, you will get expected result:

SELECT NULL
1
2
3
4
5
6
7
SELECT
n,
CASE WHEN n IS NULL THEN 'NULL' ELSE 'NOT NULL' END
FROM unnest(ARRAY(NULL,1,2,3,4,5)) n
WHERE n IS NULL

NULL in Crosstab

In most cases, the NULL special character doesn’t hurt much, since we always can alter our expression to fix the problem. But if you’re using table functions to create pivot, and there is NULL in your columns, then you will find NULL is a cursed value, which brought a lot of trouble to you.

Postgres provide tablefunc extension, which can provide a series functions called “crosstab#”. And with these functions, you can convert a set of rows into a pivot table.

Function crosstab accept 2 sql queries. First query should yield 3 columns: row in pivot table, column in pivot table and value in pivot table.
Second query should yield a series of value which defines the columns of the pivot table.
crosstab function will group the rows yield by 1st query by 1st column. Then map each row in the group to a column by comparing the 2nd column in the row yield by 1st query with the value generated by 2nd query, if the value is equal then the 3rd column of the row yield by 1st query will be placed in the column defined by value of the 2nd query.
It if a very convenient feature provided by postgres, and it works perfect in most cases.

But in our case, we met a problem that we have NULL value in the 2nd column of 1st query, which means we have NULL value in pivot table columns!

And the crosstab decide which column the value should be placed to by comparing the whether the value is equal!

And NULL never equals to NULL!

BAM!!!

As a result, the column of NULL in the pivot table is always empty!

CASE WHEN

To solve the problem, we should play a little trick in the first query, we should translate all the NULL into a “normal” value.
Here is our 1st query, and we want to get a pivot table with period as row axis, rating as column axis and volume of the order as content.

Original 1st query
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
SELECT
Periods.period,
Profiles.rating,
SUM(Orders.volume)
FROM
orders
LEFT OUTTER JOIN Periods ON (Periods.id = Orders.period_id)
LEFT OUTTER JOIN Profiles ON (Profiles.id = Orders.profile_id)
GROUP BY
Periods.period,
Profiles.rating
ORDER BY
Periods.period,
Profiles.rating

And we will get NULL in Profiles.rating, so we can translate NULL as 0 in rating. To achieve this we can use CASE WHEN statement.

Original 1st query with CASE WHEN
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
SELECT
Periods.period,
CASE WHEN Profiles.rating IS NULL THEN 0 ELSE Profiles.rating,
SUM(Orders.volume)
FROM
orders
LEFT OUTTER JOIN Periods ON (Periods.id = Orders.period_id)
LEFT OUTTER JOIN Profiles ON (Profiles.id = Orders.profile_id)
GROUP BY
Periods.period,
Profiles.rating
ORDER BY
Periods.period,
Profiles.rating

COALESCE

The solution works fine. But in personal perspective, I don’t like it, because it repeat the statement and is not concise. But luckily, the value we should deal with the the special value NULL and postgres has provided a group of functions to deal with NULL.

What we want is the function COALESCE, which accept a group of value as arguments, and returns the first not null value.
So we can simplify our statement with this super function:

Original 1st query with COALESCE
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
SELECT
Periods.period,
COALESCE(Profiles.rating, 0)
SUM(Orders.volume)
FROM
orders
LEFT OUTTER JOIN Periods ON (Periods.id = Orders.period_id)
LEFT OUTTER JOIN Profiles ON (Profiles.id = Orders.profile_id)
GROUP BY
Periods.period,
Profiles.rating
ORDER BY
Periods.period,
Profiles.rating

In the statement, if the rating is not null, the COALESCE function will return the actual value if the rating is NULL, then the COALESCE will find the next not null value, which must be 0.

Besides COALESCE function, there is another function called NULLIF, which might mislead you to a totally wrong way just as what I had.
According to postgres document, the function might behave in a totally opposite way than you expected.

The NULLIF function returns a null value if value1 equals value2; otherwise it returns value1. This can be used to perform the inverse operation of the COALESCE

A pitfall in jQuery form serialization

Today, I was so surprised that I got an empty string when I call the serialize method on a jQuery wrapped form.
The html is written in Haml:

Html
1
2
3
4
5
6
7
%form#graph-option.horizontal-form
%fieldset
%label{ :for=>'start-date'} Start Date
%select#start-date
%label{ :for=>'end-date'} End Date
%select#end-date
%button#submit-option.btn.large-btn

And the script is written in coffee-script:

Script
1
2
3
4
5
6
7
$ ->
$('#submit-option').click ->
option = $('#graph-option').serialize()
$.post '/dashboard/graph', option, (data) ->
renderCharts data

When I execute the script, I got 500 error. And the reason is that the option is empty.
I believe this must be caused by a super silly mistake, so I try to call serialize methods on Twitter Bootstrap website, and I still got empty string!!!!

After half an hour debugging, I just realize that I forgot to assign the name to all the input elements. And according to html specification, the browser uses the name of the elements to identify whom the value belongs to.
So when the name is omitted, the serailizeArray method in jQuery returns an empty array, as a result, the serialize method returns empty string.

According to my experience, it is easy to identify this problem, if the html is in html-like format, such as erb. But it is really hard to identify this issue if the page is written in haml, because in haml, id is used much more often.
To fix this problem, we need to specify the name explicitly for each form element.

Here is the fixed haml code:

Html
1
2
3
4
5
6
7
%form#graph-option.horizontal-form
%fieldset
%label{ :for=>'start-date'} Start Date
%select#start-date{ :name=>'start-date' }
%label{ :for=>'end-date'} End Date
%select#end-date{ :name=>'end-date' }
%button#submit-option.btn.large-btn

Name trap in Rails

I’m a newbie to Rails, and my past few projects are all rails based, including MetaPaas, Recruiting On Rails and current SFP.
I was amazed by the convenience of Rails, and also hurt by its “smartness”.
The power of Rails was described in quite a lot of posts, so I wanna to share some failure experiences.
Actually I have felt into quite a number of pitfalls in Rails, and here is one of the most painful ones.

To explain the problem easier, I just simplify the scenario:
I have a model called “Candidate”, which holds a “Status” to store the status of the candidate, so I have the code like this:

Candidate and Status
1
2
3
4
5
6
7
8
9
10
11
class Candidate < ActiveRecord::Base
has_one status
# other definition
end
class Status < ActiveRecord::Base
belongs_to candidate
# other definition
end

For some reason, I change the relationship between Candidate and Status. It is changed from one-to-one to one-to-many.
So I changed the has_one to has_many:

Candidate and Status
1
2
3
4
5
6
7
8
9
10
11
class Candidate < ActiveRecord::Base
has_many status
# other definition
end
class Status < ActiveRecord::Base
belongs_to candidate
# other definition
end

I thought it is an easy modification, but the app fails to run, even I have done the database migration.
It said rails cannot find a constant named “Statu”!

After my first sight on this error message, I believed it is caused by a typo, I must mistyped “Status” as “Statu”.
So I full-text search the whole project for “Statu”, but I cannot find any.

This error message is quite weird to me, since I have no idea about where the word “Statu” come from!
After spent half an hour on pointless trying, I suddenly noticed that the word “status” is end with “s”, and according to Rails’ convention, rails must think “status” is the plural form of “statu”. So according to the convention again, it try to find a class named “Statu”.
And we should use the plural form noun as name for one-to-many field, since that holds an array rather than a single object.

So after changing status to statuses, the problem solved.

Convention based system is powerful, a lot magic just happened there. But also the magic things are hard to debug when some special case breaks the convention presumption.

Install specific version of tool with HomeBrew

HomeBrew is a convenient package manager for Mac user. For some reason I prefer Home Brew to Mac Ports.
Brew has a younger package repository since it has shorter history comparing to MacPorts. Younger repository means less options. And sometime it is hard for you to install the old-fashioned tool with brew.

Brew uses git to manage its formula repository, so you can list with git.
Typically, the repo is located at /usr/local. But since this path can be changed, so it is safer to reference this path via brew.
Brew call the repo path as prefix, so you can reference the path with brew --prefix
You can use the following shell command to enter the brew repo.

1
cd $(brew --prefix)

Since brew load formula from local, so before we install the app with brew, we need to ensure the repo is updated. We can use the following command to update the brew repo:

1
2
3
4
5
# Update brew
brew update
# update with git
cd $(brew --prefix) && git pull --rebase

To install specific version of the app, we need to checkout the specific version of the formula, we can get the versions and related git revision by brew versions command, and checkout specific version, then install the app:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
brew versions postgresql
# Output:
# 9.1.3 git checkout e088818 /usr/local/Library/Formula/postgresql.rb
# 9.1.2 git checkout dfcc838 /usr/local/Library/Formula/postgresql.rb
# 9.1.1 git checkout 4ef8fb0 /usr/local/Library/Formula/postgresql.rb
# 9.0.4 git checkout 2accac4 /usr/local/Library/Formula/postgresql.rb
# 9.0.3 git checkout b782d9d /usr/local/Library/Formula/postgresql.rb
# 9.0.2 git checkout 2c3b88a /usr/local/Library/Formula/postgresql.rb
# 9.0.1 git checkout b7fab6c /usr/local/Library/Formula/postgresql.rb
# 9.0.0 git checkout 1168d8f /usr/local/Library/Formula/postgresql.rb
# 8.4.4 git checkout c32bea0 /usr/local/Library/Formula/postgresql.rb
# 8.4.3 git checkout 9b2ef7c /usr/local/Library/Formula/postgresql.rb
# 8.4.1 git checkout 0495cf5 /usr/local/Library/Formula/postgresql.rb
# 8.4.0 git checkout a82e823 /usr/local/Library/Formula/postgresql.rb
git checkout a82e823 /usr/local/Library/Formula/postgresql.rb
brew install postgresql

If we cannot find the specific version that we want (Such as Postgres 8.3.11). don’t be disappointed, we can try to search the version repository.
Some of the old-fashioned tool which is not included in brew’s master repo might be provided in version repository.

Begin from Brew 0.9 provide the multiple repository support, user can use brew tap command to register alternative repositories besides the master repo. There are quite a some interesting alternative repos, such as versions and games.
These official alternative repos can be found on github

The formulas in alternative repositories cannot be used directly, but luckily the official ones are included in the search result.

1
2
3
4
5
brew search postgresql
# Output:
# postgresql
# homebrew/versions/postgresql8 homebrew/versions/postgresql9

In the search result, we can see there are 2 formulas are displayed with a path rather than just the formula name, which means these formulas are in a alternative repo.
The path to the formula follows the convention: <github username>/<repository name without "homebrew-">/<formula name>.
So homebrew/versions/postgresql8 means the file is located at https://raw.github.com/Homebrew/homebrew-versions/master/postgresql8.rb

To install it, we can install it directly or tap the repo first:

1
2
3
4
5
6
7
8
# Install directly
brew install homebrew/versions/postgresql8
# Tap
brew tap homebrew/versions
brew install postgres8