皇帝的舊衣

Fermat’s Library 觸發讀了 C. A. R. Hoare 這篇論文[1],其中 最常被引用的一段話應該是:

I conclude that there are two ways of constructing a software design: One way is to make it so simple that there are obviously no deficiencies and the other way is to make it so complicated that there are no obvious deficiencies.

The first method is far more difficult.

「我斷定軟體設計有兩種方式:第一種是非常簡單,所以明顯的沒有缺點。另一種是非常複 雜,所以沒有明顯的缺點。前者困難多了。」

最後的小故事也很有意思,簡中有翻譯但實在不好,試譯如下。

皇帝的舊衣

很久以前,有位皇帝非常喜歡衣服,把所有錢都花在買衣服上。他沒去管士兵、赴宴、或審 判等麻煩事。若是提到別位皇帝或國王,人們可能會說「他正出席議會」,但提到這位皇帝, 人們總是說「他在更衣室裡」。而他真的都在裡面。在一次不幸的場合,他因被騙而裸體示 人,臣民覺得好笑,而皇帝非常懊悔。於是他決定再也不離開王座,且為了避免再次裸體, 他規定往後每件新衣服只能覆蓋在原有的衣服上。

在首都這大城中,時間快樂地過去了。部長與大臣、織工與裁縫、訪客與臣民、縫工與繡工, 為了各類事務在王座室進進出出,而他們全都大聲讚嘆:「皇帝的盛裝真是華麗!」

有一天,皇帝最老也最忠誠的部長聽說有位出類拔萃的裁縫,受教於一間古老的高等針線術 學府,他發展出一種新的抽象刺繡藝術,其針腳如此精細,以致沒人能分辨到底存不存在。 於是部長想:這繡法一定非常傑出,若能聯絡上這位裁縫擔任顧問,我們就能將皇帝裝飾的 炫耀程度提升到一個新高,世界都會認可他是自古以來最偉大的皇帝。

所以這位誠實的老部長花了一大筆錢,把裁縫請來。裁縫被帶到王座室,在那裡他對如今已 完全蓋住王座的華服敬禮。大臣們渴望地等著他的建議。所以當他提出不要再增加更精細複 雜的刺繡潤飾,而是移除層層華麗衣飾,追求簡單優雅來代替複雜細節,你可以想像到大臣 們的震驚。他們交頭接耳:這裁縫並不是他自稱的專家,他的智慧已經被象牙塔中的長時間 研究搞混,無法了解現代皇帝的縫紉需求了。裁縫大聲爭辯了許久,說他的建議是合理的, 但大臣們聽不進去。最後,他接受了酬勞而回到象牙塔中。

直到此日之前,都無人揭露這故事的全部真相:在一個晴朗早晨,皇帝覺得很熱又無聊,於 是小心翼翼地從堆積如山的衣服下鑽出,現正在另一個故事中當個快樂的養豬人。而裁縫被 封為所有顧問的守護神,因為儘管他收了一大筆顧問費,卻從未成功說服客戶相信他的明悟: 他們的衣服下,沒有皇帝。

[1] Charles Antony Richard Hoare. 1981. The emperor’s old clothes. Commun. ACM 24, 2 (February 1981), 75-83. DOI=http://dx.doi.org/10.1145/358549.358561

Scroll Lock as Caps Lock

我的 Ubuntu /etc/default/keyboard 裡面有這設定:

1
XKBOPTIONS="terminate:ctrl_alt_bksp,ctrl:nocaps"

作用是「外加」一些選擇性的鍵盤設定。其中 terminate:ctrl_alt_bksp 這是 dpkg-reconfigure keyboard-configuration 就有選項可以設定,用途是這三個鍵一起按 就可以跳出 Xorg。而 ctrl:nocaps 則是手動加上,印象中圖形界面也可設定,但很久沒 用已不記得。這選項用途是把 Caps Lock 當成 Left Ctrl 來用。這樣設定的原因是,慣用 的編輯器 Emacs 使用時實在太常按 Ctrl,但筆電上用小指按久了會不舒服,所以用 Caps Lock 來代替。而原先的 Left Ctrl 功能仍然保留,因為外接實體鍵盤時用掌緣壓 Ctrl 很容易, 且筆電上有時仍然會用掌緣按而不用小指。

這樣設定是蠻順手,但仍有個缺點,就是沒有 Caps Lock 可用。比如要定義常數常常是 ALL_CAP_CONST 這種命名,一直按著 Shift 就有點不方便。

想到一個解決方案是拿真的沒在用的 Scroll Lock 當作 Caps Lock,偶爾要用時還有個鍵 可以按,於是想把這設定也做成一個 XKBOPTIONS,步驟如下。

首先新增 /usr/share/X11/xkb/symbols/local

1
2
3
xkb_symbols "sccaps" {
replace key <SCLK> { [ Caps_Lock ] };
};

接下來修改 /usr/share/X11/xkb/rules/evdev,在 ! option = symbols 這段最後面 補上

1
local:sccaps = +local(sccaps)

以及 /usr/share/X11/xkb/rules/evdev.lst, /usr/share/X11/xkb/rules/evdev.xml 都按照原先的格式加入 local:sccaps 這選項。最後把 /etc/default/keyboard 那行改為

1
XKBOPTIONS="terminate:ctrl_alt_bksp,ctrl:nocaps,local:sccaps"

重跑 dpkg-reconfigure keyboard-configuration 就生效。

Digital Room Correction (DRC) on Ubuntu 16.04

由於去年裝潢時各種因素權衡下折衷,目前音響喇叭是放在客廳大型衣/帽/鞋櫃的前面,當 時已知擺位不佳,沒想到開聲比預期更差,整體相當不平衡。尤其 68Hz 左右有個無法忽視 的響應尖峰,只要音樂本身有些低頻,播放時就轟轟作響,摸櫃子都可感到振動。近來終於 有時間處理,採用 Digital Room Correction (DRC) 技術,效果很明顯。 中文相關資訊雖然有,但多半只貼結果,過程怎麼做講得不多,就在這裡簡述一下。相關的 設定檔、script 等等都已公開在 GitHub

硬體

訊源採用 Lavry DA-11,麥克風 miniDSP UMIK-1,不過這樣設定沒辦法 做 reference channel,也就是把左右聲道分開,左聲道用麥克風錄,右聲道直接用線接 loopback. 沒有 reference 會導致時間同步不完美,以目前設備這問題只能改善,沒辦法 解。

UMIK-1 每支都提供個別的校正檔,可按照序號下載。

Filter

採用 drc,參數很多、功能強,但文件實在又長又專業,門檻有點高。看懂怎麼跑以後, 文件 4.4.3 Sample automated script file 裡面提到的 measure 這個 script 已經相當可用,但他的假設是有 reference channel,所以必須做些改寫才能使用。 為了減輕時間不同步的程度,必須盡量讓播音與錄音同時開始,這裡用了 parallel,可 以經由安裝 moreutils 得到。

把麥克風放在聆聽位置,音響調到平時習慣音量(但太大聲會傷喇叭),然後分別跑 aplay -l 以及 arecord -l 來確定設備名稱。這兩個命令需要 alsa-utils,一般都 已經有預設安裝。例如這樣的輸出:

1
2
3
4
5
$ arecord -l
**** List of CAPTURE Hardware Devices ****
card 0: PCH [HDA Intel PCH], device 0: Generic Analog [Generic Analog]
Subdevices: 1/1
Subdevice #0: subdevice #0

注意 card 0device 0,要使用這設備錄音的話就要下 arecord -D hw:0,0,第 一個 0 表示 card,第二個 0 表示 device。接下來確定 aplay 可放出聲音,而 arecord 可錄到聲音,就可以設定 work/run.sh 裡面的參數了。要改 INDEV, OUTDEV 兩個,分別對應到 arecordaplay-D 設定。

要跑完整個 run.sh 需要 moreutils, sox 以及當然的 drc 這些 packages. run.sh 會先呼叫 measure-noref 產生 impulse response 檔案,然後呼叫 sox 轉成適合 drc 使用的格式,接下來呼叫 drc 產生最後的 filter erb-44.1.pcm

measure-noref 是將 measure 改寫成不用 reference channel 的版本,其餘行為都是 相同的。執行時盡量保持整個環境沒有背景聲音,喇叭會播放從 10Hz 到 21KHz 的音頻讓 麥克風錄下,產生 impulse-48.pcm 這個 impulse response 測量結果。

這邊值得注意的是,由於 UMIK-1 是以 48KHz 錄音,所以到這裡都是用相同取樣頻率來處 理。但我一般訊源格式都是 44.1KHz 的 CD 格式,所以先用 sox 轉換檔案格式及取樣頻 率,接下來 drc 的部份就都是用 44.1KHz 處理。

另外,對應到 44.1KHz,UMIK-1 的校正檔也需要稍微修改後,drc 才會接受。原本的校 正檔內容大概是這樣:

1
2
3
4
5
6
"Sens Factor =-.7423dB, SERNO: 7023040"
10.054 -5.0400
10.179 -4.8727
10.306 -4.7086
<snipped>
20016.816 -0.3030

drc 需要從 0 到 f/2 的範圍,以 44.1Khz 為例,就是 0 ~ 22050,而且 drc 也 不認得 "Sens Factor.. 這樣的檔頭,必須刪掉。修改結果會像這樣:

1
2
3
4
5
6
7
0 0
10.054 -5.0400
10.179 -4.8727
10.306 -4.7086
<snipped>
20016.816 -0.3030
22050 0

新加入的 0Hz 以及 22050Hz 的調整值為 0,因為超過了這支麥克風可以錄到的範圍。改好 後,存在 work 目錄下,並修改 run.sh 裡面的 MIC_CALIBRATION 參數。

整理一下流程:

  1. 安裝 alsa-utils, drc, sox, moreutils
  2. 設定 INDEV, OUTDEV, MIC_CALIBRATION
  3. 執行 run.sh
  4. 若成功的話,會看到 erb-44.1.pcm 這個檔案。

播放

平時慣用的 Audacious 本身就有 JACK output,JACK 可以把數位音訊輸入、輸出 任意搭接,其中 Jconvolver 可用 drc 的輸出將數位訊號先做處理,所以 只要把 Audacious 的輸出接到 Jconvolver 的輸入,然後輸出到 DAC 就行了。為了簡單的 管理 JACK,也裝了 qjackctl,利用裡面 patchbay 的功能自動處理這些介接。

先叫起 qjackctl,設定好輸出裝置,按 Start 然後到 terminal 執行 jconvolver drc-44.1.conf。接下來叫起 Audacious,輸出設定為 JACK output,按旁邊的 Settings 把 Automatically connect to output ports 關掉,Bit depth 設為 Floating point,先 播放然後按暫停。這時回到 qjackctl,按 Connect 應該可以看到 Audacious 跟 jconvolver 都出現了。手動把輸入輸出接成這樣

connect

恢復 Audacious 播放後,應該就可以聽到聲音。也可利用 Patchbay 自動化,設定好以後 會像這樣:

patchbay

設定檔已附上,檔名為 drc-patchbay.xml,其內還有 pulseaudio 的設定,可安裝 pulseaudio-module-jack,如此 pulseaudio 的音訊也可處理到。

jconvolver 需要不少運算,若要確保播放時不出現中斷,可用

1
$ sudo chrt -r -p 50 `pidof jconvolver`

把優先權提高。

圖形輸出

drc 下載 drc-3.2.1.tar.gz,解開後到 source/doc/octave/,照著文件 A Sample results 內的敘述,讀取 work/rmc.pcm 以及 work/rtc.pcm 就 可用 createdrcplots 產出圖形。這裡需要 octave 以及 octave-signal,比較麻煩 的是另一個需要的 plot 已經無人維護,所以 Ubuntu 也沒有打包好的 package 可以用, 必須在 octave 內執行 pkg install -forge plot 裝起來。裝好之後,要跑的指令大 致如下:

1
2
3
4
5
graphics_toolkit('gnuplot');
pkg load signal plot
ru = loadpcm("/somewhere/drc-mashup/work/rmc.pcm");
rc = loadpcm("/somewhere/drc-mashup/work/rtc.pcm");
createdrcplots(ru,-1,"R Uncorrected",rc,-1,"R Corrected","/somewhere","R",".png","-dpng");

在我的空間,效果是像這樣:

R-MRFDWSmoothed.png

完整 PDF 文件

後續調整

drc 提供從 minimal, soft, normal, strong, extreme, insane 幾種範例設定,insane 可以清楚聽到一些改過頭造成的失真,用來訓練耳朵辨識,用以調整參數。另外還提供 erb 這設定,類似 minimal,改得很少,適合多人(多位置)聆聽。run.sh 之中預設是用 erb 作為起始點,聽感已有差異且不易有反效果。接下來就是嘗試不同範例,了解參數、比 較差異再去微調。相關設定可搭配聆聽訊源的 sample rate 一起修改。例如,要產生 96KHz 的 normal 設定,就把 TARGET_RATE 改為 96000CONFIG 改為 normal-96.0 即可。缺的設定檔從 /usr/share/drc/config 底下複製過來,這裡只提 供 erb-44.1.drc. 另外也要複製對應的「修正目標」檔,例如 44.1Khz 就是 pa-44.1.txt,96Khz 就是 pa-96.0.txt,位置在 /usr/share/drc/target.

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 在決定這邊下多少、那邊下 多少、怎麼下。在我沒有實際看到的地方,聽說有很多大咖在互相接近,消滅中 間人,直接以各自掌握的資源對接,並藉以壓迫沒跟上時代的人。

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