MongoDB: unique sparse index of array field

MongoDB 中可以對 document (以 sql 說法就是 record)中的某個「陣列」欄位做 index, 其方式是這樣:

Multikey Indexes

To index a field that holds an array value, MongoDB creates an index key for each element in the array.

就是把陣列值通通展開一起做 index 的意思。這方法乍看合理,但是,如果把 unique, sparse 一起考慮進去,搭配 $push, $pop 等 array operator 的行為,就會發生種 種奇妙的情況。從 1.x 至今,這件事是一整本糊塗帳。

先從 unique 說起。unique 的定義是:index 中每個值只能對應到一個 document。在 multikey 的情況,就會是以下這種「可能不符直覺」的行為:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
> db.c.ensureIndex({arrayfield: 1}, {unique: true, name: 'unique'});
{
"createdCollectionAutomatically" : false,
"numIndexesBefore" : 1,
"numIndexesAfter" : 2,
"ok" : 1
}
> db.c.insert({arrayfield: [1, 2]});
WriteResult({ "nInserted" : 1 })
> db.c.insert({arrayfield: [1, 3]});
WriteResult({
"nInserted" : 0,
"writeError" : {
"code" : 11000,
"errmsg" : "E11000 duplicate key error collection: test.c index: arrayfield_1 dup key: { : 1.0 }"
}
})

原因是 insert {arrayfield: [1, 2]} 所產生的 index 為 1, 2 各一筆,而 {arrayfield: [1, 3]} 會需要產生 1, 3 各一筆,此時 1 就重複了。所以這邊的 unique 意思其實是「陣列中的值在不同筆紀錄中不能重複」的意思。值得注意的是,以 下的例子又是可以的:

1
2
> db.c.insert({arrayfield: [3, 3]});
WriteResult({ "nInserted" : 1 })

這裡產生的 index 3 只有對應到一個 document,所以沒有違反 unique。要避免這種 情況,就必須先確保產生陣列時沒有重複的值,而後續的操作不用 $push 而用 $addToSet

unique 有個常見的問題,就是「沒有」也算值,所以若有兩筆紀錄都沒有要 index 的欄 位,那「沒有欄位」這件事就重複了,這是不被允許的:

1
2
3
4
5
6
7
8
9
10
> db.c.insert({name: 'a'});
WriteResult({ "nInserted" : 1 })
> db.c.insert({name: 'b'});
WriteResult({
"nInserted" : 0,
"writeError" : {
"code" : 11000,
"errmsg" : "E11000 duplicate key error collection: test.c index: unique dup key: { : null }"
}
})

這時候一般就會加入 sparse,其定義是「不 index collection 中所有的 document,而 只管『有這個欄位』的那些」。直接看例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
> // drop previous unique index
> db.c.dropIndex('unique');
{ "nIndexesWas" : 2, "ok" : 1 }
> // create an unique & sparse index
> db.c.ensureIndex({arrayfield: 1}, {unique: true, sparse: true});
{
"createdCollectionAutomatically" : false,
"numIndexesBefore" : 1,
"numIndexesAfter" : 2,
"ok" : 1
}
> db.c.insert({name: 'b'}); // can insert this now
WriteResult({ "nInserted" : 1 })

看似問題解決了,其實不然。對陣列的操作,經常需要加入或移除元素,假定現在先把 [3, 3] 清成空陣列,後續又把 [1, 2] 清空,此時照 sparse 的原則來想,兩筆紀 錄都是「沒有值」,應該要被允許才對。在 mongo 1.x 版本,這是成立的。但這造成一個 問題,就是如果在「有 index」的情況下查詢 {arrayindex: []},會查不出東西,因為 「空陣列」沒有被 index,就被當成不存在了。然而如果不用 index,一筆一筆比對,就又 會出現,此時就有行為不一致的問題,也就是 SERVER-2258 裡面的例子:

1
2
3
4
5
6
> db.c.save({a:[]});
> db.c.ensureIndex({a:1});
> db.c.find({a:[]}); // no result
> db.c.find({a:[]}).hint( {$natural:1} );
{ "_id" : ObjectId("4d0fba6fc6237b412f53adeb"), "a" : [ ] }
> db.c.find({a:[]}).hint({a:1}); // again, no result

如前所述,multikey 是針對陣列中的每個「值」做 index 的。為了解決這問題,2.0 版之 後的空陣列就被當成包含一個值 undefined。這解了 SERVER-2258,但又產生了新的問題, 因為這樣一來,重複的空陣列就會違反 unique

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
> db.c.update({arrayfield: 1}, {$pull: {arrayfield: 1}});
WriteResult({ "nMatched" : 1, "nUpserted" : 0, "nModified" : 1 })
> db.c.update({arrayfield: 2}, {$pull: {arrayfield: 2}});
WriteResult({ "nMatched" : 1, "nUpserted" : 0, "nModified" : 1 })
> db.c.find().pretty();
{ "_id" : ObjectId("58c256eb84c90ec348fb8950"), "arrayfield" : [ ] }
{
"_id" : ObjectId("58c2582384c90ec348fb8952"),
"arrayfield" : [
3,
3
]
}
{ "_id" : ObjectId("58c25a2c84c90ec348fb8955"), "name" : "a" }
{ "_id" : ObjectId("58c25be084c90ec348fb8957"), "name" : "b" }
> db.c.update({arrayfield: 3}, {$pull: {arrayfield: 3}});
WriteResult({
"nMatched" : 0,
"nUpserted" : 0,
"nModified" : 0,
"writeError" : {
"code" : 11000,
"errmsg" : "E11000 duplicate key error collection: test.c index: arrayfield_1 dup key: { : undefined }"
}
})

可看到錯誤訊息 arrayfield_1 dup key: { : undefined },表示 undefined 這個值重 複了。而且,這個 breaking change 並沒有寫在 release note 中。此問題被反應在 SERVER-3934,至今未解。一個 workaround 是採用 {v: 0} 也就是舊版的 index, 但看來在 2.6+ 之後也不再支援。

小結一下:對於「array field 中的值在整個 collection 中不能重複,但此 array field 有可能不存在」的 use case,以目前 MongoDB (3.4) 機制是無法直接做到的。

電子支付、第三方支付、實名制

終於稍微多搞懂一點台灣的第三方支付。

首先要把名詞釐清,一般人認知的第三方支付就是金錢以「非」銀行帳戶到銀行帳戶的方式 在網路上流來流去,例如在中國,從使用者銀行帳號以支付寶付給某淘寶電商,而某電商又 以支付寶支付給上游。在整個流程中,錢沒有被領出來,也沒有實際到達鏈上每個人的銀行 帳號,只在支付寶平台上以數位紀錄型態流動。

但在台灣,類似的行為其實被細分為幾個不同營業項目以及監管機關。首先看一下金管會的 這篇 FAQ,內文提到:

於付款方及收款方間經營「代理收付實質交易款項」、「收受儲值款項」、「電子支付帳 戶間款項移轉」等業務之公司,係屬「電子支付機構」,應向本會申請許可,其營業項目 代碼為「HZ06011電子支付業」,屬本會金融監理業務之範疇。…

惟如「僅經營代理收付實質交易款項」業務,且所保管代理收付款項總餘額未逾新臺幣10 億元者,其營業項目代碼為「I301040第三方支付服務業」之公司,其主管機關為「經濟 部」,並由經濟部商業司對第三方支付服務業者進行一般商業管理。

比較兩段業務項目異同,可發現所謂「電子支付業」多了「收受儲值款項」、「電子支付帳 戶間款項移轉」這些業務,保管款項上限也比較高。前段提及「以支付寶收進來的錢,直接 再轉給其他支付寶帳號」這種業務,是屬於「電子支付帳戶間款項移轉」,會被歸類在「電 子支付」,監管單位是金管會。在台灣,這類業者審核條件較高,有智付寶、歐付寶等等, 名單可以在金管會銀行局的「電子支付機構」項目下查到。

至於「第三方支付服務業」,也就是可以經營「代收代付」的,在台灣其實很多,目前四千 三百多間,包括「台灣連線有限公司」,也就是 LINE Pay,都在其中。名單可以在 政府資料開放平臺取得。

而像悠遊卡公司,就屬於電子票證,不能代理收付也不能在電子支付帳戶間轉帳,相關法律 規範是「電子票證發行管理條例」,與前段所提「電子支付業」、「第三方支付服務業」又 不一樣了。

網路上可找到「呼叫米球」的文章說得很清楚,可參考。

釐清這些差異後,再回來看金管會之前引起軒然大波的「電子支付機構使用者身分確認機制 及交易限額管理辦法」第22條修正草案,才比較看得懂:

一、調整期間最終時點定為106年9月30日:

鑑於我國將於107年第4季接受亞太防制洗錢組織(APG)之相互評鑑,評鑑結果如不理想, 除影響我國國際聲譽外,將衝擊我國金融機構業務發展及國人金融交易活動。為兼顧防制 洗錢國際標準及社會大眾對法規鬆綁之需求,考量相關評鑑作業之準備須於106年底前完 成,針對該辦法修正草案第22條第1項規定,調整期間最終時點為106年9月30日。

二、調整期間須「確認使用者提供之電子郵件信箱或社群媒體帳號」之身分確程序:

為適度提高簡化身分確認程序之嚴謹度,於調整期間內除採取「確認使用者提供之行動電 話號碼」之方式外,並保留「確認使用者提供之電子郵件信箱或社群媒體帳號」之身分確 認程序,維持雙重確認機制。

三、調整期間預告期間條文草案係為3階段,改採2階段配套措施:

(一) 第1階段(106年6月30日前):每月總交易金額上限為等值新臺幣1萬元。

(二) 第2階段(106年7月1日至9月30日止):每月總交易金額上限為等值新臺幣5千元。

(三) 另考量調整期間最終時點定為106年9月30日,且設有金額限制機制可適度控管風險, 刪除原定交易次數限制。

原文網址

乍看之下這很像「從此以後第三方支付如果沒用實名制,交易上限就變成一萬,然後變五千, 今年10月後還非得要實名制不可」,但其實這針對的只是「電子支付業」,而「第三方支付 服務業」是不受影響的。這篇新聞稿出來以後一片嘩然,金管會第二天還趕緊再發一篇 試圖滅火。

所以,若有需要用到這些服務,可視本身需求來決定採用那一類業者的服務。若只有代收代 付需求,選擇第三方支付業者即可,限制比較少些。

初學 functional programming

初學 Scala,限定只能使用 functional 以及 immutable 時,被卡在以下問題:

1
2
3
4
5
/**
* This function should return the first position of character `c` in the
* 2 dimensional vector `v`.
*/
def findChar(c: Char, v: Vector[Vector[Char]]): Option[(Int, Int)] = ???

看起來真夠簡單,但想不出怎麼回傳第二層的 indexOf 值。第一版是這樣寫的:

1
2
3
4
5
6
7
8
9
10
11
def findChar(c: Char, v: Vector[Vector[Char]]): Option[(Int, Int)] = {
var y = -1
val x = v.indexWhere(vv => {
y = vv.indexOf(c)
y > -1
})
if (x > -1)
Some((x, y))
else
None
}

這樣是不差啦,但就是多了個 var y。自己硬寫的結果是這樣:

1
2
3
4
5
6
7
def findChar(c: Char, v: Vector[Vector[Char]]): Option[(Int, Int)] = {
val x = v.indexWhere(_.indexOf(c) > -1)
if (x > -1)
Some((x, v(x).indexOf(c)))
else
None
}

用了兩次 indexOf 實在有夠難看。最後忍不住 Google:

1
2
3
4
5
6
7
def findChar(c: Char, v: Vector[Vector[Char]]): Option[(Int, Int)] = {
val r = v.map(_ indexOf c).zipWithIndex.find(_._1 > -1)
r match {
case Some((x, y)) => Some((y, x))
case None => None
}
}

但找到以後馬上結束就好,map 會轉換整個 Vector,其實是多的,還是感覺有點硬寫。

感想是:

  1. 要記得有 zip 這類東西。
  2. 思考方式還是偏 imperative.

另外一個大問題就是如果自我要求每個 recursive 都要是 @tailrec tail recursive 也是很障礙。

Go standard library net/rpc/jsonrpc 不支援 HTTP

之前寫 JSON-RPC over HTTP 的時候是直接用 github.com/gorilla/rpc,蠻簡 單就寫出來。這次打算用 standard library 去做,不額外引用 gorilla,結果 踢到鐵板。

目前 net/rpc 支援兩種 codec,一種是 Go 內建的 encoding/gob,另一種 是 net/rpc/jsonrpc。gob 可以透過 HTTP 包起來使用,也可以獨立作為 TCP service。而 jsonrpc 則是「只能獨立使用」。這在 API 裡面並沒有說明得很清 楚。相關討論在: https://github.com/golang/go/issues/2738 ,裡面提到原因是

jsonrpc can be used with HTTP by writing a suitable codec. Given that there is no standard definition of jsonrpc over http, I think that’s the best we can do.

原來 jsonrpc over http 沒有標準定義啊…

reflect.DeepEqual 與 time.Time

Go 很像加強版的 C,例如只要把 type 想像成 function call 的第一個參數, 那 method 就只是自動放參數而已。比如 POSIX C 裡頭的 time:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
NAME
time - get time in seconds
SYNOPSIS
#include <time.h>
time_t time(time_t *t);
DESCRIPTION
time() returns the time as the number of seconds since the
Epoch, 1970-01-01 00:00:00 +0000 (UTC).
If t is non-NULL, the return value is also stored in the memory
pointed to by t.

對照 Go 的 time.Unix():

1
2
3
4
func (t Time) Unix() int64
Unix returns t as a Unix time, the number of seconds elapsed since
January 1, 1970 UTC.

這樣就很明顯了。

會講到這個是因為這陣子碰到一個奇怪的 bug,相同的情況在 native 環境下 reflect.DeepEqual 是相等,而 Docker 環境下 DeepEqual 不相等。印 log 出來發現不相等原因是 struct 裡面 time.Time 這個 field 比出來不相等, 但用 %v 印出來卻是一樣的。參考 DeepEqual 的說明,推測應該是針對 Time 裡面的每個 field 用 == 去比對,所以就改用 %#v 印,抓到是 loc *Location 這欄位不一致,原因是 Docker 預設 timezone UTC,而我本機環境 是 CST。參考原始碼,time.Time 定義如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
type Time struct {
// sec gives the number of seconds elapsed since
// January 1, year 1 00:00:00 UTC.
sec int64
// nsec specifies a non-negative nanosecond
// offset within the second named by Seconds.
// It must be in the range [0, 999999999].
nsec int32
// loc specifies the Location that should be used to
// determine the minute, hour, month, day, and year
// that correspond to this Time.
// Only the zero Time has a nil Location.
// In that case it is interpreted to mean UTC.
loc *Location
}

sec, nsec 這兩個欄位放的是 Epoch 絕對時間,而用 loc 放時區。所以 同樣的「時間點」,不同的「時區」時,會造成 struct 內容不一致, DeepEqual 就認為不相等了。追到這裡覺得似乎很合理,就把兩邊的時間都轉換 成 func (Time) Local 再來比對,結果卻仍然不相等,而且 %#v 印出來 「每個欄位」的值,包括 *Location 的 pointer 指向位址都一樣,這樣還是不 相等,就很傻眼了。

要再知道原因當然有辦法,讀 DeepEqual 的原始碼就可以,但已經花了不少時間, 而且這方向感覺解法不漂亮。因為 reflect 相關函數的速度都會慢,已經看這個 DeepEqual 不爽很久了,就直接另外用 type switches 寫了針對不同型別的比對 方式,碰到 time.Time 就直接呼叫 func (Time) Equal,把 DeepEqual 當 成最後手段來用。

然而這個疑惑還是沒完全解開,最近時間不夠用,只好先丟著以後再追。

Netflix says Geography, Age, and Gender are “Garbage” for Predicting Taste

FORTUNE 原文連結

一如往常的這背後有很多可以談,但所知不足,也恐怕沒有足夠空閒好好整理出 來,只能簡短打一些。首先這篇點出個人資訊的差異性遠遠不如「好惡」的差異, Netflix 以使用者喜好去 grouping 時,地理位置、年齡、性別等等幾乎完全不 列入考慮。在全世界都是同一個演算法。比如文中例子,90% 動漫流量來自日本 之外,國籍影響遠不如宅度來得大。

以這個類比到廣告產業,有趣的因素就多了。首先影片與文學、繪畫等等有類似 性質,往往可以跨越語言、地理限制等,但廣告常常是與地理高度相關的。比如 新開一家餐廳,若不是高度針對某特殊族群,例如高價位米其林美食,或是主打 韓劇歐巴主角等,往往較為重視餐廳周遭的客人,而非對全市或全縣打廣告,更 不用說全台灣。但若是大品牌廣告,例如某車廠新車上市,就會是全台。再例如 新的化妝品,針對女性就遠大於針對男性。之前忘記從哪裡看到的資料,也說若 發傳單這行為能達到某種針對性,轉化率可以有很大的差異。

把這兩個觀點結合起來會是什麼呢?個人認為這在暗示目前網路廣告準確度還有 非常大空間,而有趣的是,儘管這是個事實,但從廣告主開始,卻似乎還無法做 出很好的應對。這是指台灣情況,再遠我就無知了。廣告主目前的嘗試,多半是 採取風險轉嫁,將 CPV (cost per view), CPC (cost per click) 一步步推到 CPI (cost per installation),或是更廣義的 CPA (cost per action),簡言之 就是「我賺到錢才付你錢」。然後中間又穿插了 agent 下單到各種平台,但以實 際接觸經驗,也多半是各種經驗上、實務上理由來決定怎麼配,中間並沒牽涉到 什麼神奇的 DM/ML 技術。如此無效率的情況下必然中間有市場,往這方向談下去, 立刻就會碰到 Appier 這類公司。

數據要多大才有用?這中間牽涉到媒體、廣告平台、agent、廣告主、以及一大堆 沒寫出的各種角色競合,有興趣可以看看 LUMA Partners 廣受引用的 這些圖。似 乎掌握相當多有效資料的是 Facebook 跟 Google,但在我看到的、弱弱的場景中, 這兩間往往只被當成下單的標的之一,是 agent 在決定這邊下多少、那邊下 多少、怎麼下。在我沒有實際看到的地方,聽說有很多大咖在互相接近,消滅中 間人,直接以各自掌握的資源對接,並藉以壓迫沒跟上時代的人。

這一片亂局中,目前所處的公司正站在哪裡,該站在哪裡,要怎樣才能走過去, 這些我都還不是看得很清楚。

Go 1.5 Vendor Experiment with Git Submodule

一開始當然要先看這篇 Go 1.5 Vendor Experiment了解一下究竟 怎麼做的,簡言之就是把 /vendor 這目錄當成類似 /node_modules 這樣的 概念去用,從目錄最尾端開始往上,有就優先用,都沒有才會用到 GOPATH 裡 面的。

由於最終打算用 docker 包起來,所以想用 git submodule 來管 vendor 底下的東西,這樣 docker build 的過程中可以不需要裝 godep 這樣的 package manager,也不用把關聯的程式碼放入 vcs 裡。但在把專案換過去的時 候,覺得最麻煩的是沒有適合的自動化工具,godep save 是在 GOPATH 的想 法下運作的,若把 GO15VENDOREXPERIMENT=1 打開,會把目前用到的複製到 vendor 底下,然後不包括 vcs 目錄例如 .git,這個在轉換到 submodule 的時候會有問題。研究出來比較簡單的方式是:

  1. glide,或只要類似功能的都 可以。

  2. glide init,如果之前有用 godep 之類其他的,它會自動嘗試匯入,不 然會自己找關聯。跑完會產生 glide.yaml。注意,這邊只會找第一層,也 就是你的專案裡面直接 import 的關聯。glide.yaml是可以自己編的,要 固定版本等等的都是在這邊指定。

  3. glide install,會遍查各層關聯,把所有用到的全部抓下來放到 vendor,並把版本也一併列出放入 glide.lock。這檔案是自動產生,自 己編了下一次跑也會被蓋掉。

  4. 接下來用這支 python script 自動產生命令,餵給 shell 執行。python glide-gitsubmodule.py | sh

Method as Argument in Javascript

前些時候談過 在 Go 中使用 method 作為 argument ,它的觀念、適用場合有點像 partial application ,但當然形式完全不同。若把使用語言換為 JavaScript,要傳 function 通常都 是拿來當 callback,常常是 anonymous。如果用到超過一次,通常就會拉出來取 個名字。如果會重複用但參數略有不同,那可以寫個角色像是 factory 的 function 用來產生「要被當成 callback 的 function」。不過並非所有人都像 我一樣那麼不愛用 JavaScript 的(半殘)物件導向,自然免不了會有「想拿 method 當成 callback」的情況。以下程式碼雖然直覺,但就會掉入 this 陷 阱裡:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
'use strict';
function Bot(name) {
this.name = name;
}
Bot.prototype.selfintro = function () {
return "I'm " + this.name;
};
function roll(intro) {
console.log(intro());
}
var no5 = new Bot('Number 5');
roll(no5.selfintro);

原因是執行到 roll 裡面時,this 並未被定義,所以呼叫傳入的參數 no5.selfintro 時,裡面 this 也一樣沒有定義。修正方法是用 bind 指 定 this,把最後一行換成:

1
roll(no5.selfintro.bind(no5));

語法很簡單,但在 JavaScript 程式中盡量把一切都化為資料與 function 個人 覺得是蠻好的設計方式。

高科技人才難尋?

這話題也出來一陣子了,但自身看法似乎比較怪異,沒看到類似的,遂姑且寫下 留個紀錄。由於經驗限制,本文所指人才,範圍侷限在「軟體工程師」。

企業說高科技人才難尋,而觀察周遭同行,抱怨多半集中在老闆沒用、薪水太低、 長官太蠢、工時過長等等,似乎少有人抱怨失業,所以大概可判斷此事為真。從 供給與需求來看,人才難找、就業充分,表示需求高而供給低。

先從需求高談起。需求高,企業搶人,自然有動力盡量提高薪水及福利。常看到 工程師抱怨老闆小氣,但若是提高薪資福利能提高獲利,老闆就會去做。薪水停 滯,背後原因並不一定是簡單的「小氣」,而可能是企業獲利能力沒那麼好,提 高薪資無法反映在獲利上,賺不到錢,自然也付不出錢。

而從供給面談,個人覺得最受忽視的一點是:人才數量,本來就是有限的。 台灣新生的人力有限,條件好的,除資訊外,也有其他學門可選擇。如果條件沒 變好,人才選擇學資訊的比例就不會增加。增設相關科系,只會產出更多不適任 者,不會憑空變出人才來。而若讓排名好的大學增加招生人數,教學品質必定降 低,還會壓擠到原本分配已經極度不均的教學資源。

只看資訊人才的話,與為台灣企業工作相比,外商(包括中國)以及新創公司也 吃掉了不少供給。這都代表價值選擇上,後兩者能夠提供文化、環境、薪資、福 利等綜合起來相對有競爭力的選項。

談完現象,來談解法;一樣從供給與需求入手。

搶不到人,但又無法增加獲利,一個合理的解決方案就是「減少需求」,也就是 減少所需的工程師數量。台灣工程師普遍程度不錯,但賣肝的多,抱怨公司無效 率的也多,與其找更多人,不如設法改善效率。這可由幾個不同面向著手:

  1. 中高階主管的世代交替很重要,因台灣是硬體起家,研發主管懂軟體開發的相 對少,需盡快汰換。

  2. 制度上需加強中階主管的實作責任,不能只是發號施令,要把實作設定為工作 一部分。只有真正參與,才能正確辨認出提昇效率的方法,以及避免錯誤決策。

  3. 與前項搭配,制度上增進開會效率、盡可能資訊透明、組織扁平,以上種種都 是為了減少主管的「非研發庶務」。

  4. 與 2, 3 項搭配,優先升遷具有研發實力以及溝通能力的工程師。簡言之,設 法讓「喜歡研發並擅長溝通」的人出線,而非「不寫程式也沒關係,只要升遷 就好」的人。

工程師的美德是懶惰,若讓能力最佳的工程師最大程度發揮懶惰的美德,並且將 此美德在組織中擴散,整體效率就會提高。不看程式的主管容易讓「寫爛程式快 速達成上級要求」的工程師出頭,而這樣的人上位後也只會搞政治、尸位素餐, 惡性循環。科技業幾間巨頭內部不乏人才,但往往排列沒有最佳化,搖動一下, 會有很大改善空間。

而在業務擴張方面,台灣企業主常一開口就大氣地說要雇幾百個工程師,這很可 能正是問題所在。究竟這幾百人進來要做什麼事?這事能賺多少錢?而這些錢, 分給這幾百人,還剩多少?如果換一下觀念,同樣預算,人找少一點,錢給多一 點,花更多功夫在組織效率上,很可能做得到一樣的事情。差別是錢多了,找來 的是人才,錢少了,只能找人家挑剩的。

另外一個可能解法是「提高獲利能力」。但如何提高?現有的高科技業巨頭,商 業模式已經固定,不太可能突然獲利大增。新創是個機會,但由於世界最佳新創 環境仍在矽谷,以及中國的資金與市場優勢,要成氣候,無可避免要與矽谷或中 國接軌。如此結果,雖然賺到錢的不見得是台灣人,但會是台灣人才的創業經驗, 然後以這些人為種子,才容易產生成功的本土新創事業。如此案例,已經有好幾 個正在發生。

另一機會是傳產。台灣傳產一直很強,而現在的高科技,已經不能只視為單一產 業,而應該是「所有行業的高科技化」。高科技業待遇相對不差,所以傳產要高 科技化的第一個功課,是要狠下心出得起錢。當然,一開始來的不見得是實力派, 很有可能是「口才很好派」,但只要待遇好、講實績、砍人不手軟,遲早會吸引 到高科技業內幹得不爽、敢衝敢拼的實力者。

與此同時,要理解的是,並不是架了網站、發行了 app 就是高科技化,創新才是。 比如 data visualization 鼎鼎大名的 D3.js 創始者之一 Mike Bostock 當時是 任職於紐約時報,傳產要做高科技,應以此為師。從這角度思考,前面所提新創 事業人才與傳產,就有了強烈結合理由。

整理一下以上提出的幾點看法:

  1. 已建立的大型科技企業,應以改善組織效率、精簡人力、提高待遇為優先。對 新增業務,應以精兵為目標。

  2. 以國外經驗與資金為引,培植創業人才,進而培育自有、高獲利之新創企業。

  3. 傳產應跳入競爭,追求創新或新創公司,吸引人才回流。

而其後邏輯是:

人才數量有其自然上限,提高較為困難,不如設法集中資源、提高獲利能力以爭 搶人才。若以「生產更多軟體從業人員」為解方,恐怕適得其反。

Ruby or JavaScript?

最近接觸到一些 RoR 的東西,覺得很有趣,以開發速度而言 RoR 真的很快,且 生態完整,但效能就是問題。反觀 node.js 族群,效能好些,但似乎開發速度還 沒那麼快。很可能接下來會變成看是 Ruby 先出現 HHVM 那樣的 VM,還是 JavaScript 開發時間也越來越短,最終真的一統天下。

以語言來說並不喜歡 JavaScript,就算到 ES6 也還是沒有很愛,但若把目 標放在 full stack,那一定得懂前端,要懂前端,就一定得把 JavaScript 學好。 既然學好了,後端時若能繼續用同樣語言開發,就能少學一種語言。

然而,若以開發時間而論,這不一定是最快的方式。因為很多 best practice (convention) 仍然存在 RoR 之中,所以花時間去學 Ruby,然後直接用 RoR,現 階段來說省下的功夫很可能可以把學習 Ruby 的時間賺回來。

語言跟應用領域,往往有很強的相關性。例如 Linux 之於 C,userspace 的基礎 架構如 database, browser engine, server 之於 C/C++,資料科學之於 R。但 也有相當通用的語言,例如 Python 以及 Java 用途都相當廣,Python 開發快, Java 效能好。所以說,若打定主意專精於 web 領域,那為了 RoR 學習 Ruby 是 很合理的。

但這優勢其實可取代。因 JavaScript 在 web 應用太廣泛,而基於 isomorphic 的需求,JS 會很自然的往後端與 mobile app 那邊長。且 async non-blocking 的天性已經在語言中,這並不只是效能問題,還牽涉到 scale up 的難易程度。 若把 node.js 的 opinionated web framework 例如 sails.js 開發到像 RoR 一 樣的方便,那麼全部轉換到 JS 為基礎的全端架構,有明顯的優勢。

這件事適合熟悉 RoR,知道其中哪些特性相對於其他解決方案是比較有優勢的工 程師去做。相對於寫一個 VM 可能要公司的能力才做得到,把 sails.js 這類 framework 往 RoR 的方向改善則只需要幾個工程師就行了,後者或許比較容易發 生。

另一個有趣的可能性是,Ruby 到 WebAssembly 的 interpreter 被發展出來且內 建到瀏覽器。由於 client 端運算能力日漸強大,interpreter 慢些也還好,說 不定 RoR 也可以用目前的方式往前端長,直接用一樣的方式寫前端行為,打包成 一整個 full stack web app,這樣感覺也是很厲害。