読者です 読者をやめる 読者になる 読者になる

FledglingRobin’s diary

Github: https://github.com/FledglingRobin

Elixirに手を出してみる

関数型言語Elixirに手を出してみた。エリクサーって名前がとても可愛い 。

Elixirとは何か

まずは公式サイトを見てみる。http://elixir-lang.org/

Elixir is a dynamic, functional language designed for building scalable and maintainable applications.

スケールできることとメンテナンスのしやすさに重きをおいた関数型言語らしい。

To learn more about Elixir, check our getting started guide. Or keep reading to get an overview of the platform, language and tools.

素直に従って、getting started guideを読んで見る。

Elixirのインストー

Introduction - Elixir を見てみる。

If you still haven’t installed Elixir, run to our installation page. Once you are done, you can run elixir –version to get the current Elixir version.

すると、installについては別ページで解説しているとのことなのでそちらを見に行く。

Installing Elixir - Elixirを見に行くと環境毎に手段が乗っている。今回はHomebrewから入れることにする。

$ brew install elixir

とくに問題なくinstallできた。

$ elixir --version
Erlang/OTP 19 [erts-8.3] [source] [64-bit] [smp:4:4] [async-threads:10] [hipe] [kernel-poll:false] [dtrace]

Elixir 1.4.4

インタラクティブモード

最初に紹介されていたのがインタラクティブモードだった。

For now, let’s start by running iex (or iex.bat if you are on Windows) which stands for Interactive Elixir.

というわけで、素直にiexを起動してみる。

$ iex
Erlang/OTP 19 [erts-8.3] [source] [64-bit] [smp:4:4] [async-threads:10] [hipe] [kernel-poll:false] [dtrace]

Interactive Elixir (1.4.4) - press Ctrl+C to exit (type h() ENTER for help)
iex(1)> 

例を参考に少し触ってみる。

$ iex
Erlang/OTP 19 [erts-8.3] [source] [64-bit] [smp:4:4] [async-threads:10] [hipe] [kernel-poll:false] [dtrace]

Interactive Elixir (1.4.4) - press Ctrl+C to exit (type h() ENTER for help)
iex(1)> 
nil
iex(2)> 1+1
2
iex(3)> "hello" <> " " <> "world"
"hello world"
iex(4)> 

ちなみにインタラクティブモードはCtrl+Cの2度押しで抜けられる。

プログラムをファイルに書く

この辺の流れはお決まりだなあと思いつつ、サンプルを実行してみる。

$ cat hello.exs 
IO.puts "Hello world from Elixir"
$ elixir hello.exs 
Hello world from Elixir

ここから、getting started guide以外の資料を見ていく。 Basic types - Elixirには基本型 について記載がある。

Basic arithmetic Identifying functions Booleans Atoms Strings Anonymous functions (Linked) Lists Tuples Lists or tuples?

連結リストとタプルがちょっと気になる。

Anonymous functions

Anonymous functions can be created inline and are delimited by the keywords fn and end:

インラインの関数定義を実現するキーワードとして、fnendが使われているらしい。

iex(1)> add = fn a,b -> a + b end
#Function<12.118419387/2 in :erl_eval.expr/5>
iex(2)> add.(1, 2)
3
iex(3)> add.(3, 4)
7
iex(4)> add.(1, -1)
0

Functions are “first class citizens” in Elixir meaning they can be passed as arguments to other functions in the same way as integers and strings.

Elixirの関数は第一級関数だと補足がされている。関数型言語と聞いていたので、きっとそうくると思ってた。

Keep in mind a variable assigned inside a function does not affect its surrounding environment

関数内の変数は、関数外に影響を及ぼさない。下記の例だと、x = 123としたあとに、関数内でx = 256としているが、関数外のxは変わらず123を示している。

iex(9)> x = 123
123
iex(10)> (fn -> x = 456 end).()
456
iex(11)> x
123

#連結リスト

[]でリストを定義。++で連結。--で要素の削除。直感的。

[1, 2, 3, 4, 5]
iex(23)> list
[1, 2, 3, 4, 5]
iex(24)> list = list ++ [6, 7] 
[1, 2, 3, 4, 5, 6, 7]
iex(25)> list
[1, 2, 3, 4, 5, 6, 7]
iex(26)> list = list -- [1, 2]
[3, 4, 5, 6, 7]
iex(27)> list
[3, 4, 5, 6, 7]

また先頭要素の取得にhdが、先頭以外要素の取得にtlが用意されている。

iex(33)> hd(list)
3
iex(34)> tl(list)
[4, 5, 6, 7]

関数型言語であるStanderd MLにも同様の文法があったことを思い出す。書き方がなんとなく想像ついてきた。

ASCIIコードのリスト

iex(38)> [104, 101, 108, 108, 111]
'hello'

Elixirでは数字の羅列が文字列になることがある。印字可能なASCIIコードが渡されると、文字列として判断するのだそうだ。面白い。

iex(39)> i 'hello'
Term
  'hello'
Data type
  List
Description
  This is a list of integers that is printed as a sequence of characters
  delimited by single quotes because all the integers in it represent valid
  ASCII characters. Conventionally, such lists of integers are referred to as
  "charlists" (more precisely, a charlist is a list of Unicode codepoints,
  and ASCII is a subset of Unicode).
Raw representation
  [104, 101, 108, 108, 111]
Reference modules
  List
Implemented protocols
  IEx.Info, Collectable, Enumerable, Inspect, List.Chars, String.Chars

Keep in mind single-quoted and double-quoted representations are not equivalent in Elixir as they are represented by different types:

これも気をつけなきゃと思った。シングルクォートで囲った場合と、ダブルクォートで囲った場合、異なる型として判定される。

iex(6)> 'hello' == "hello"
false

つまり、下記の通りシングルクォートで囲むとListになるが、ダブルクォートの場合はStringで扱われるらしい。

iex(7)> i 'hello' 
Term
  'hello'
Data type
  List
Description
  This is a list of integers that is printed as a sequence of characters
  delimited by single quotes because all the integers in it represent valid
  ASCII characters. Conventionally, such lists of integers are referred to as
  "charlists" (more precisely, a charlist is a list of Unicode codepoints,
  and ASCII is a subset of Unicode).
Raw representation
  [104, 101, 108, 108, 111]
Reference modules
  List
Implemented protocols
  IEx.Info, Collectable, Enumerable, Inspect, List.Chars, String.Chars
iex(8)> i "hell"
Term
  "hell"
Data type
  BitString
Byte size
  4
Description
  This is a string: a UTF-8 encoded binary. It's printed surrounded by
  "double quotes" because all UTF-8 encoded codepoints in it are printable.
Raw representation
  <<104, 101, 108, 108>>
Reference modules
  String, :binary
Implemented protocols
  IEx.Info, Collectable, Inspect, List.Chars, String.Chars

Tuple

Listの他に、Tuple型が提供されている。

iex(16)> tuple = {:ok, "hello"}     
{:ok, "hello"}
iex(17)> put_elem(tuple, 1, "world")
{:ok, "world"}
iex(18)> tuple
{:ok, "hello"}
iex(19)> tuple = put_elem(tuple, 1, "world")
{:ok, "world"}
iex(20)> tuple
{:ok, "world"}
iex(21)> tuple = put_elem(tuple, 0, :error) 
{:error, "world"}
iex(22)> tuple                             
{:error, "world"}

パターンマッチ

Elixirについて語っていた方が、パターンマッチの良さについて言及してらしたのを思い出しつつ。Elixirではパターンマッチが利用できる。Tupleのパターンマッチは下記の通りになる。

iex(25)> {:ok, result} = {:ok, 13}
{:ok, 13}
iex(26)> result
13

Listでも同様に利用できる。

iex(35)> [a, b, c] = [0, 1, 2]
[0, 1, 2]
iex(36)> a
0
iex(37)> b
1
iex(38)> c
2

|で区切ることで、先頭要素とそれ以外、とすることができるらしい。

iex(51)> list = [1, 2, 3]
[1, 2, 3]
iex(52)> [0 | list] 
[0, 1, 2, 3]

下記のように[h|t]で先頭要素hと残りtを取得できる。

iex(54)> [h | t] = list
[1, 2, 3]
iex(55)> h
1
iex(56)> t
[2, 3]

pin

Use the pin operator ^ when you want to pattern match against an existing variable’s value rather than rebinding the variable:

再代入ではなく、既存の値を使いたいときに^を使うらしい。今のところ用途にピンとこない。とりあえず、下記の通り再代入しないようになるようだ。

iex(60)> x = 1 
1
iex(61)> x = 2
2
iex(62)> ^x = 3
** (MatchError) no match of right hand side value: 3

下記の通りパターンマッチングによる代入もきかなくなる。

iex(63)> {x, y} = {1, 2}
{1, 2}
iex(64)> x
1
iex(65)> {x, y} = {3, 4}
{3, 4}
iex(66)> {^x, y} = {5, 6}
** (MatchError) no match of right hand side value: {5, 6}

分岐

case, cond, and if - Elixir には分岐について記載されている。分岐分が掛ければ、お決まりのフィボナッチ数列導出関数がかける気がする。

case文

doからendの間に、パターンと結果の組を記述することができる。

iex(1)> case {1, 2, 3} do
...(1)>  {1, 2, x} -> "Good"
...(1)>  {1, x, y} -> "So-So"
...(1)>  _ -> "Bad"
...(1)> end
"Good"
iex(2)> case {1, 4, 5} do    
...(2)>  {1, 2, x} -> "Good" 
...(2)>  {1, x, y} -> "So-So"
...(2)>  _ -> "Bad"          
...(2)> end        
"So-So"
iex(3)> case {6, 7, 8} do    
...(3)>  {1, 2, x} -> "Good" 
...(3)>  {1, x, y} -> "So-So"
...(3)>  _ -> "Bad"          
...(3)> end                  
"Bad"

cond文

case is useful when you need to match against different values. However, in many circumstances, we want to check different conditions and find the first one that evaluates to true. In such cases, one may use cond

状態で分岐させたいときにはcaseのパターンマッチはやや使い勝手が悪い気がする、と思っていたら次に別の分岐構文が 載っていた。

iex(7)> cond do   
...(7)>   0 + 1 == 2 -> "this will not be true"
...(7)>   3 * 4 == 5 -> "nor this"
...(7)>   6 - 7 == -1 ->"this will"            
...(7)> end
"this will"

これは他の言語で言うelse if文に近いとあった。なるほど。

Keyword list

Keywords and maps - Elixirによると、特定の形のtupleをKeyword listとしているらしい。

iex(11)> list = [{:a, 0}, {:b, 1}]
[a: 0, b: 1]
iex(12)> list ++ [c: 3]
[a: 0, b: 1, c: 3]
iex(13)> [a: 4] ++ list
[a: 4, a: 0, b: 1]
iex(14)> new_list = [a: 5] ++ list
[a: 5, a: 0, b: 1]
iex(15)> new_list[:a]
5
iex(16)> new_list[:b]
1

ここまでのまとめ

Elixirは関数型言語。文法は、今のところ突飛な感じはない。長くなってきたので、一度記事をここで切ります。

気軽にモーフィングを試してみる

気軽にいろいろ遊んでみたので、とくに面白かったものを記録しておく。

2人の顔写真の中間画像をうみだす

  • SqirlzMorph

中間画像を作っていくソフト。 www.xiberpix.net

モーフィングそのものの実現は難しくなさそう。 www.learnopencv.com

  • IMAGE MORPH JS

JavaScript向けのモーフィングライブラリも存在する。 matching pointを手動指定した場合のデモがあっていろいろ試せる。

Poisson image Editing

切り抜き位置の調整をした上で、部位位置指定でなめらなかなブレンディングをすると、目元はAさんで口元はBさんみたいな画像ができるはず… ということでなめらかなブレンディングを手軽に利用できそうな実装を探す。

Poisson Blending | OpenCV.jp

JavaScriptでPoisson Image Editingによる滑らかな画像合成 « Rest Term

ためしてみた。位置合わせとマスク画像を手作業で作ってブレンディング。顔の形のちがいからバランスが微妙になってしまった。特徴点をとって、きちんと元の位置にパーツを配置しないとだめだなあ。

Node.jsとasync.jsとcallstack

Node.js の call stack の性質を知る必要がでてきた。ためしてみた結果を記録。

Node.js の stack trace の出し方を知る

Errors | Node.js v7.6.0 Documentation を参考に実行してみる。

var stack = new Error().stack
console.log( stack )

実行結果:

$ node app.js 
Error
    at Object.<anonymous> (/[directory_path]/app.js:1:75)
    at Module._compile (module.js:409:26)
    at Object.Module._extensions..js (module.js:416:10)
    at Module.load (module.js:343:32)
    at Function.Module._load (module.js:300:12)
    at Function.Module.runMain (module.js:441:10)
    at startup (node.js:139:18)
    at node.js:968:3

変数 stack には起動から new Error までのcall stack が格納されたようだ。

同じく Console | Node.js v7.6.0 Documentation を参考に実行してみる。

console.trace();

実行結果:

$ node app.js 
Trace
    at Object.<anonymous> (/[directory_path]/app.js:1:71)
    at Module._compile (module.js:409:26)
    at Object.Module._extensions..js (module.js:416:10)
    at Module.load (module.js:343:32)
    at Function.Module._load (module.js:300:12)
    at Function.Module.runMain (module.js:441:10)
    at startup (node.js:139:18)
    at node.js:968:3

console.traceは、 

message and stack trace

を印字するそうだ。

基本的な callstack の挙動を見てみる

関数実行後に別途関数を呼び出すのか、関数内で関数を呼び出すのかで call stack が消化されるのか、積まれるのかが変わるはず。

function method() {
    return true;
}

method();
console.trace();

実行結果:

$ node app.js 
Trace
    at Object.<anonymous> (/[directory_path]/app.js:6:9)
    at Module._compile (module.js:409:26)
    at Object.Module._extensions..js (module.js:416:10)
    at Module.load (module.js:343:32)
    at Function.Module._load (module.js:300:12)
    at Function.Module.runMain (module.js:441:10)
    at startup (node.js:139:18)
    at node.js:968:3
function method(callback) {
    callback(true);
}

method(function(bool){
    console.trace();
});

実行結果:

$ node app.js 
Trace
    at /[directory_path]/app.js:6:10
    at method (/[directory_path]/app.js:2:2)
    at Object.<anonymous> (/[directory_path]/app.js:5:1)
    at Module._compile (module.js:409:26)
    at Object.Module._extensions..js (module.js:416:10)
    at Module.load (module.js:343:32)
    at Function.Module._load (module.js:300:12)
    at Function.Module.runMain (module.js:441:10)
    at startup (node.js:139:18)
    at node.js:968:3

両者は method が callstack に残されているかに違いが出る。

call stack overflow を起こす

一度、call stack overflow を経験しておくことにする。真っ先に思いつくのは再帰呼出し。

function method(n) {
    console.log(n);
    method(n + 1);
}

method(0);

実行結果の一部:

17938
17939
17940
util.js:158
function stylizeNoColor(str, styleType) {
                       ^

RangeError: Maximum call stack size exceeded
    at Object.stylizeNoColor [as stylize] (util.js:158:24)
    at formatPrimitive (util.js:439:16)
    at formatValue (util.js:227:19)
    at inspect (util.js:109:10)
    at exports.format (util.js:17:20)
    at Console.log (console.js:39:34)
    at method (/[directory_path]/app.js:2:10)
    at method (/[directory_path]/app.js:3:2)
    at method (/[directory_path]/app.js:3:2)
    at method (/[directory_path]/app.js:3:2)

なお、Errors | Node.js v7.6.0 Documentation を見ると RangeError は、

a provided argument was not within the set or range of acceptable values for a function

とのことで、今回は後者にあたるはず。

再帰呼び出しを setTimeout でラップする

ためしに event queue に仕事をさせてみる。setTimeout は引数の function をevent queue に登録し、関数自体は即座に結果を返却するはずだ。

function method(n) {
    setTimeout(function() {
        console.log(n);
        method(n + 1);
    }, 0);
}

method(0);

こうすると、先程停止した 17940 を過ぎてもMaximum call stack size exceeded が呼ばれない。ここで、実行中の call stack を比較してみることにする。

function method(n) {
    if (n === 100) {
        console.trace();
    } else {
        console.log(n);
        method(n + 1);
    }
}

method(0);
97
98
99
Trace
    at method (/[directory_path]/app.js:5:17)
    at method (/[directory_path]/app.js:8:9)
    at method (/[directory_path]/app.js:8:9)
    at method (/[directory_path]/app.js:8:9)
    at method (/[directory_path]/app.js:8:9)
    at method (/[directory_path]/app.js:8:9)
    at method (/[directory_path]/app.js:8:9)
    at method (/[directory_path]/app.js:8:9)
    at method (/[directory_path]/app.js:8:9)
    at method (/[directory_path]/app.js:8:9)
function method(n) {
    if (n === 17940) {
        console.trace();
    } else {
        setTimeout(function() {
            console.log(n);
            method(n + 1);
        }, 0);
    }
}

method(0);
17938
17939
Trace
    at method (/[directory_path]/app.js:5:17)
    at null._onTimeout (/[directory_path]/app.js:9:13)
    at Timer.listOnTimeout (timers.js:92:15)

後者では method が複数積まれていないことがわかる。

async.js 使用時の stack trace をみてみる

async.js では、async.js に定義された関数の処理を通しながら、function list の関数を実行していく。

async.waterfall の場合

async.waterfallは、順次処理を記述する際に便利な関数だ。async.waterfall を例に call stack を見てみる。

var async = require('async');
Error.stackTraceLimit = Infinity;

function method(cb){
    cb(null);
}

async.waterfall([method, method, method], function(err, result){
    console.trace();
});

実行結果:

Trace
    at /[directory_path]/app.js:10:10
    at /[directory_path]/node_modules/async/dist/async.js:359:16
    at nextTask (/[directory_path]/node_modules/async/dist/async.js:5057:29)
    at /[directory_path]/node_modules/async/dist/async.js:5064:13
    at apply (/[directory_path]/node_modules/async/dist/async.js:21:25)
    at /[directory_path]/node_modules/async/dist/async.js:56:12
    at /[directory_path]/node_modules/async/dist/async.js:843:16
    at method (/[directory_path]/app.js:6:2)
    at nextTask (/[directory_path]/node_modules/async/dist/async.js:5070:14)
    at /[directory_path]/node_modules/async/dist/async.js:5064:13
    at apply (/[directory_path]/node_modules/async/dist/async.js:21:25)
    at /[directory_path]/node_modules/async/dist/async.js:56:12
    at /[directory_path]/node_modules/async/dist/async.js:843:16
    at method (/[directory_path]/app.js:6:2)
    at nextTask (/[directory_path]/node_modules/async/dist/async.js:5070:14)
    at /[directory_path]/node_modules/async/dist/async.js:5064:13
    at apply (/[directory_path]/node_modules/async/dist/async.js:21:25)
    at /[directory_path]/node_modules/async/dist/async.js:56:12
    at /[directory_path]/node_modules/async/dist/async.js:843:16
    at method (/[directory_path]/app.js:6:2)
    at nextTask (/[directory_path]/node_modules/async/dist/async.js:5070:14)
    at Object.waterfall (/[directory_path]/node_modules/async/dist/async.js:5073:5)
    at Object.<anonymous> (/[directory_path]/app.js:9:7)
    at Module._compile (module.js:409:26)
    at Object.Module._extensions..js (module.js:416:10)
    at Module.load (module.js:343:32)
    at Function.Module._load (module.js:300:12)
    at Function.Module.runMain (module.js:441:10)
    at startup (node.js:139:18)
    at node.js:968:3

制御のためにいちいちasync.jsの関数を呼び出すので、call stack はやや長くなる。

async.parallel の場合

async.js には、非同期に、並列処理かのように動作する関数が存在する。async.parallelもそのひとつだ。こちらも setTimeoutを使用したときと同様の現象がみられる。

var async = require('async');
Error.stackTraceLimit = Infinity;

function method(cb){
    cb(null);
}

async.parallel([method, method, method], function(err, result){
    console.trace();
});
Trace
    at /[directory_path]/app.js:10:10
    at /[directory_path]/node_modules/async/dist/async.js:3694:9
    at /[directory_path]/node_modules/async/dist/async.js:359:16
    at iteratorCallback (/[directory_path]/node_modules/async/dist/async.js:935:13)
    at /[directory_path]/node_modules/async/dist/async.js:843:16
    at /[directory_path]/node_modules/async/dist/async.js:3691:13
    at apply (/[directory_path]/node_modules/async/dist/async.js:21:25)
    at /[directory_path]/node_modules/async/dist/async.js:56:12
    at method (/[directory_path]/app.js:6:2)
    at /[directory_path]/node_modules/async/dist/async.js:3686:9
    at eachOfArrayLike (/[directory_path]/node_modules/async/dist/async.js:940:9)
    at eachOf (/[directory_path]/node_modules/async/dist/async.js:990:5)
    at _parallel (/[directory_path]/node_modules/async/dist/async.js:3685:5)
    at Object.parallelLimit [as parallel] (/[directory_path]/node_modules/async/dist/async.js:3765:3)
    at Object.<anonymous> (/[directory_path]/app.js:9:7)
    at Module._compile (module.js:409:26)
    at Object.Module._extensions..js (module.js:416:10)
    at Module.load (module.js:343:32)
    at Function.Module._load (module.js:300:12)
    at Function.Module.runMain (module.js:441:10)
    at startup (node.js:139:18)
    at node.js:968:3

stack trace をみると method が一度分しか記載されていないことがわかる。

async.reduce と call stack overflow

async.waterfall 利用時の call stack をみると、call stack overflow を起こし得ると想像がつく。何万もの関数をwaterfallにいれる事例は考えにくいが、引数に値のリストを取るような場合ならどうだろうか。

var async = require('async');
//Error.stackTraceLimit = Infinity;

var num = parseInt(process.argv.slice(2), 10);
var list = Array.from(Array(num).keys());

function method(memo, num, cb){
    cb(null, memo + num);
}

async.reduce(list, 0, method, function(err, result){
    console.log('done');
});
$ node app.js 1940
done
$ node app.js 1950
bootstrap_node.js:432
    const script = new ContextifyScript(code, options);
                   ^
RangeError: Maximum call stack size exceeded
    at runInThisContext (bootstrap_node.js:432:20)
    at NativeModule.compile (bootstrap_node.js:520:18)
    at NativeModule.require (bootstrap_node.js:466:18)
    at tty.js:4:13
    at NativeModule.compile (bootstrap_node.js:525:7)
    at NativeModule.require (bootstrap_node.js:466:18)
    at createWritableStdioStream (internal/process/stdio.js:142:19)
    at process.getStdout [as stdout] (internal/process/stdio.js:10:14)
    at console.js:100:37
    at NativeModule.compile (bootstrap_node.js:525:7)

2000件のデータをそのまま async.reduec にいれればこういうことになる。

async.reduceと async.waterfall の組み合わせとcall stack overflow を考える

多数のデータをもつcollection を順次処理にかけるリスクがわかった。より実際の処理で起こり得そうな状況として、async.waterfallとの組み合わせを考える。

var async = require('async');
//Error.stackTraceLimit = Infinity;

var num = parseInt(process.argv.slice(2), 10);
var list = Array.from(Array(num).keys());

function method(memo, num, cb) {
    cb(null, memo + num);
}

function reduce(cb) {
    async.reduce(list, 0, method, function(err, result){
        cb(err);
    });
}

async.waterfall([reduce, reduce, reduce, reduce, reduce], function(err, result){
    console.log('done');
})
$ node app.js 400
done
$ node app.js 500
[directory_path]/node_modules/async/dist/async.js:843
        callFn.apply(this, arguments);
               ^

RangeError: Maximum call stack size exceeded
    at [directory_path]/node_modules/async/dist/async.js:843:16
    at [directory_path]/node_modules/async/dist/async.js:2487:13
    at method ([directory_path]/app.js:8:2)
    at [directory_path]/node_modules/async/dist/async.js:2485:9
    at replenish ([directory_path]/node_modules/async/dist/async.js:881:17)
    at iterateeCallback ([directory_path]/node_modules/async/dist/async.js:866:17)
    at [directory_path]/node_modules/async/dist/async.js:843:16
    at [directory_path]/node_modules/async/dist/async.js:2487:13
    at method ([directory_path]/app.js:8:2)

async.doWhilstと無限ループを考える

記事冒頭でcall stack overflowの原因に再帰呼び出しを考えたが、無限ループの場合も考えておく。async.doWhilstは繰り返しを実現する関数だ。

var async = require('async');
Error.stackTraceLimit = Infinity;

var n = 0;
var limit =parseInt(process.argv.slice(2), 10);

async.doWhilst(
  function(cb) {
    n++;
    cb(null);  
  },
  function() {
   return (n < limit);
  },
  function(err) {
        console.trace('done');
    }
);
$ node app.js 2 
Trace: done
    at [directory_path]/app.js:16:13
    at [directory_path]/node_modules/async/dist/async.js:843:16
    at [directory_path]/node_modules/async/dist/async.js:2938:18
    at apply ([directory_path]/node_modules/async/dist/async.js:21:25)
    at [directory_path]/node_modules/async/dist/async.js:56:12
    at [directory_path]/app.js:10:5
    at [directory_path]/node_modules/async/dist/async.js:2937:44
    at apply ([directory_path]/node_modules/async/dist/async.js:21:25)
    at [directory_path]/node_modules/async/dist/async.js:56:12
    at [directory_path]/app.js:10:5
    at Object.doWhilst ([directory_path]/node_modules/async/dist/async.js:2940:5)
    at Object.<anonymous> ([directory_path]/app.js:7:7)
    at Module._compile (module.js:571:32)
    at Object.Module._extensions..js (module.js:580:10)
    at Module.load (module.js:488:32)
    at tryModuleLoad (module.js:447:12)
    at Function.Module._load (module.js:439:3)
    at Module.runMain (module.js:605:10)
    at run (bootstrap_node.js:422:7)
    at startup (bootstrap_node.js:143:9)
    at bootstrap_node.js:537:3

少々わかりにくいが、app.js:10:5が2回呼ばれているのがわかる。ここで、無限ループに陥った場合を考える。

var async = require('async');

var n = 0;
var limit =parseInt(process.argv.slice(2), 10);

async.doWhilst(
  function(cb) {
    n++;
    cb(null);  
  },
  function() {
    return true;
   // return (n < limit);
  },
  function(err) {
        console.log('done');
    }
);
$ node app.js 
[directory_path]/node_modules/async/dist/async.js:76
function identity(value) {
                 ^

RangeError: Maximum call stack size exceeded
    at identity ([directory_path]/node_modules/async/dist/async.js:76:18)
    at [directory_path]/node_modules/async/dist/async.js:55:24
    at [directory_path]/app.js:9:5
    at [directory_path]/node_modules/async/dist/async.js:2937:44
    at apply ([directory_path]/node_modules/async/dist/async.js:21:25)
    at [directory_path]/node_modules/async/dist/async.js:56:12
    at [directory_path]/app.js:9:5
    at [directory_path]/node_modules/async/dist/async.js:2937:44
    at apply ([directory_path]/node_modules/async/dist/async.js:21:25)
    at [directory_path]/node_modules/async/dist/async.js:56:12

この場合もcall stack があふれる。

むすび

call stack の大きさには気をつける。

Azure App Serviceをさわってみるのに困ったこと

基本的に下記を見てうまくいった。 docs.microsoft.com

一部困ったことがあったので、解決結果を記録しておく。

テスト用に無料プランがあるはずなのに見つからない

App Serviceプランを設定する際に、価格レベルの選択でFreeプランを見つけられなくて困った。 f:id:FledglingRobin:20170114221201p:plain 画面右上の「お勧め」を「すべて表示」に変えたら見つかった。 f:id:FledglingRobin:20170114220956p:plain ちなみに、Freeプランだと、

CPU時間は1日あたり60分まで、または、5分あたり2.5分まで

の制限がかかるので用途によっては向かない。

ローカルgitをデプロイ元にする設定場所が見つからない

App Serviceの展開元にあった。 f:id:FledglingRobin:20170114221612p:plain

環境変数をどうやって設定するのか

コンソールからできなかったので困った。 アプリケーション設定のアプリ設定にあった。 f:id:FledglingRobin:20170114221724p:plain

Node.jsドキュメントからbase64エンコーディングをおさらい

Node.jsでbase64エンコーディング、と検索すると下記の方法を紹介したページがずらっと見つかる。

var buffer = new Buffer('変換前の文字列');
var string = buffer.toString('base64');
console.log(string);
$ node base64_test.js 
5aSJ5o+b5YmN44Gu5paH5a2X5YiX

これってなんだろう、ということでおさらいをしてみる。

Bufferクラスは何か

Bufferクラスってなんだろう、ということで Buffer | Node.js v7.4.0 Documentation を読み直してみた。

BufferクラスはNode.js v7.4.0現段階でStable。もともとは、任意なバイナリデータ、octet-streamを扱うために使われていた。ECMAScript 2015 (ES6)でTypedArrayが導入された今は、Uint8Array APIをNode.js向けに提供している。

TypedArrayは何か

ここで一度TypedArrayについておさらいする。Javascript typed arrays - JavaScript | MDNが読みやすかった。

TypedArrayはES6で導入された、バイナリデータを効率的に扱うための型付き配列。もともとJavaScriptで利用されているArray連想配列なのに対して、純粋な配列として機能する。

var uint8 = new Uint8Array(2);
uint8[0] = 42;

例えば下記の型が提供されている。

  • Int8Array
  • Uint8Array
  • Int16Array
  • Uint16Array
  • Float32Array

このうちUnit8Arrayは、1バイト8 ビット長符号なし整数値を扱う。詳しくはECMAScript 2015 Language Specification – ECMA-262 6th Editionをみればいい。

結局Node.jsのBufferクラスは何をしているの

そのTypedArrayがES6に導入された今、Bufferクラスは何をしているのだろう。

BufferクラスはNode.jsが提供するクラスで、ES6で導入されたTypedArrayのUnit8Arrayを扱うクラスとして提供されている。Bufferクラスは固定長のバッファをメモリに確保する。

文字列については、Node.js v7.4.0 では下記8つのエンコード・デコードができる。

  • ascii
  • utf8
  • utf16le
  • ucs2
  • base64
  • latin1
  • binary
  • hex

例えば、10バイト確保して0埋めする。

const buf = Buffer.alloc(10);

例えば、文字列をASCIIのバイト列にしたときの長さ分確保して、バイト列を格納する。testのASCIIコードは[0x74, 0x65, 0x73, 0x74]になる。

const buf = Buffer.from('test');

例えば、同様のことをUTF-8で行う。

const buf = Buffer.from('tést', 'utf8');

fromを使わないコンストラクタの使用はv6.0.0で非推奨

冒頭で示したコードではnew Buffer(string[, encoding])を使っていた。

var buffer = new Buffer('変換前の文字列');
var string = buffer.toString('base64');
console.log(string);

どうやら、Buffer | Node.js v7.4.0 Documentationによると、

Stability: 0 - Deprecated: Use Buffer.from(string[, encoding]) instead.

とのことで、v6.0.0以降のNode.jsを使う場合は上記のBuffer.fromを使うのがよいらしい。

// base64 エンコード
const buf = Buffer.from('変換前の文字列');
const base64 = buf.toString('base64');
console.log(base64);    

// base64 デコード
const string = Buffer.from(base64, 'base64');
console.log(string.toString());
$ node base64_test.js 
5aSJ5o+b5YmN44Gu5paH5a2X5YiX
変換前の文字列

むすび

下記のコードは、バイナリデータを扱うためのクラスを利用してbase64エンコードを実施している。なお、Node.js v6.0.0以降はBuffer([string])が非推奨でBuffer.from([string])の使用が推奨されている。

var buffer = new Buffer('変換前の文字列');
var string = buffer.toString('base64');
console.log(string);

log4js-nodeのおさらい

なんとなくで使い始めたlog4js、改めておさらいをしておく。

github.com

log4js-nodeってなんだろう

Node.jsでlogをとるときに便利なフレームワークJavaで使われていたLog4J、それをJavaScript用に実現したLog4jsをNode.js向けにしたものがlog4js-node。npm installする際にはnpm install log4jsと書き、-nodeの部分はつけなくていい。

log4js-nodeでできること

たとえば、下記のようなことができる。

  • consoleに色付きでログを出力させる
  • Node.jsのconsole.logをおきかえる
  • ファイルサイズや日付でログをわける

使ってみる

log4js.getLoggerでloggerを用意して、logger.debug関数でログを記述していく。

var log4js = require('log4js');
var logger = log4js.getLogger();
logger.debug("Some debug messages");

日付とDEBUGdefaultの部分に色がついてconsoleに表示される。

$ node log4js_test.js 
[2017-01-06 17:29:46.544] [DEBUG] [default] - Some debug messages

ファイルに記録させる

console出力だけじゃなく、logファイルに書き出ささせることができる。

var log4js = require('log4js');
log4js.loadAppender('file');
log4js.addAppender(log4js.appenders.file('logs/console.log'));

var logger = log4js.getLogger();
logger.debug("Some debug messages");
$ node log4js_test.js 
[2017-01-06 17:37:15.824] [DEBUG] [default] - Some debug messages
$ ls logs/
console.log
$ cat logs/console.log 
[2017-01-06 17:37:15.824] [DEBUG] [default] - Some debug messages

ログレベルをわけることができる

debugログ以外のログレベルも利用できる。

  • trace
  • debug
  • info
  • warn
  • error
  • fatal
var log4js = require('log4js');
log4js.loadAppender('file');
log4js.addAppender(log4js.appenders.file('logs/console.log'));
var logger = log4js.getLogger();

// 実際にログを出力してみる
logger.trace('traceログです');
logger.debug('デバッグログです');
logger.info('infoログです');
logger.warn('警告です');
logger.error('エラーがでました');
logger.fatal('致命的な問題です');

出力がカラフルでとてもかわいい。

$ node log4js_test.js 
[2017-01-06 17:41:48.087] [TRACE] [default] - traceログです
[2017-01-06 17:41:48.093] [DEBUG] [default] - デバッグログです
[2017-01-06 17:41:48.094] [INFO] [default] - infoログです
[2017-01-06 17:41:48.094] [WARN] [default] - 警告です
[2017-01-06 17:41:48.094] [ERROR] [default] - エラーがでました
[2017-01-06 17:41:48.094] [FATAL] [default] - 致命的な問題です

低いレベルのログを無視する

setLevelを使うと、どのレベル以上のログを表示するか設定できる。

var log4js = require('log4js');
log4js.loadAppender('file');
log4js.addAppender(log4js.appenders.file('logs/console.log'));

// loggerを用意する
var logger = log4js.getLogger();
logger.setLevel('ERROR');

// 実際にログを出力してみる
logger.trace('traceログです');
logger.debug('デバッグログです');
logger.info('infoログです');
logger.warn('警告です');
logger.error('エラーがでました');
logger.fatal('致命的な問題です');

この場合はlogger.setLevel('ERROR');で、errorfatalのほかが無視されている。

$ node log4js_test.js 
[2017-01-06 17:45:06.062] [ERROR] [default] - エラーがでました
[2017-01-06 17:45:06.069] [FATAL] [default] - 致命的な問題です
$ cat logs/console.log 
[2017-01-06 17:45:06.062] [ERROR] [default] - エラーがでました
[2017-01-06 17:45:06.069] [FATAL] [default] - 致命的な問題です

設定をまとめて書く

var log4js = require('log4js');
log4js.loadAppender('file');
log4js.addAppender(log4js.appenders.file('logs/console.log'));

この部分をまとめて記述することができる。{type: 'console'}を外すとconsole出力がなくなる。{type: 'file', filename: 'logs/console.log'}を外すとファイル出力がなくなる。

var log4js = require('log4js');
log4js.configure({
  appenders: [
    {type: 'console'},
    {type: 'file', filename: 'logs/console.log'}
  ]
});

この設定は外部ファイルに記述することもできる。これが使いやすそう。

var log4js = require('log4js');
log4js.configure('config/log4js.json');

// loggerを用意する
var logger = log4js.getLogger();
logger.setLevel('ERROR');

// 実際にログを出力してみる
logger.trace('traceログです');
logger.debug('デバッグログです');
logger.info('infoログです');
logger.warn('警告です');
logger.error('エラーがでました');
logger.fatal('致命的な問題です');
{
    "appenders": [{
            "type": "console"
        },
        {
            "type": "file",
            "filename": "logs/console.log"
        }
    ]
}

Nodeのconsole.log上書き

標準だとconsole.logは上書きされない。

var log4js = require('log4js');
log4js.configure('config/log4js.json');

// loggerを用意する
var logger = log4js.getLogger();
logger.setLevel('ERROR');

// 実際にログを出力してみる
console.error('Node.jsのconsole.errorです');
$ node log4js_test.js 
Node.jsのconsole.errorです

Nodeのconsole.logを上書き利用する場合は、replaceConsoleの設定をする。

{
    "appenders": [{
            "type": "console"
        },
        {
            "type": "file",
            "filename": "logs/console.log"
        }
    ],
    "replaceConsole": true
}

これで、Node.jsのconsoleで書かれたlogをlog4jsで扱えるようになる。

$ node log4js_test.js 
[2017-01-06 18:33:15.227] [ERROR] console - Node.jsのconsole.errorです

logのカテゴリーをわける

たとえば送信時と受信時のlogをわけて保存したい場合。 まず、カテゴリーごとに保存先を指定しておく。

{
    "appenders": [{
            "type": "console"
        },
        {
            "type": "file",
            "filename": "logs/console_send.log",
            "category": "send"
        },
        {
            "type": "file",
            "filename": "logs/console_receive.log",
            "category": "receive"
        }
    ],
    "replaceConsole": true
}

カテゴリーの指定を変えてloggerを用意しておく。

var log4js = require('log4js');
log4js.configure('config/log4js.json');

// loggerを用意する
var loggerSend = log4js.getLogger('send');
var loggerReceive = log4js.getLogger('receive');

// 実際にログを出力してみる
loggerSend.info('サーバーがメッセージを送信しました');
loggerReceive.info('サーバーがメッセージを受信しました');

この結果、loggerSendloggerReceiveで保存先が変わる。

$ node log4js_test.js 
[2017-01-06 19:25:33.099] [INFO] send - サーバーがメッセージを送信しました
[2017-01-06 19:25:33.108] [INFO] receive - サーバーがメッセージを受信しました
$ cat logs/console_send.log 
[2017-01-06 19:25:33.099] [INFO] send - サーバーがメッセージを送信しました
$ cat logs/console_receive.log 
[2017-01-06 19:25:33.108] [INFO] receive - サーバーがメッセージを受信しました

日付ごとにファイルを変える

例えばpatternとして-yyyy-MM-ddを指定をすると、日付が変わる際にログファイルの末尾に-yyyy-MM-ddが付与される。

{
    "appenders": [{
            "type": "console"
        },
        {
            "type": "dateFile",
            "filename": "logs/console.log",
            "pattern": "-yyyy-MM-dd" 
        }
    ],
    "replaceConsole": true
}

expressと使う場合にはexpressのlogもlog4jsで扱うことができる

Connect Logger · nomiddlename/log4js-node Wiki · GitHub

最後に

log4js-nodeについてざっと振り返りをした。他にも設定できる項目があるので、使いながらくわしい動きを調べていきたい。

広告を非表示にする

kuromoji.jsで形態素解析をする

slack botをもう少し賢くしたい。といかけに含まれる名詞に反応できるようにしたい。そこで、kuromoji.jsによる形態素解析をためしてみる。 github.com

$ node kuromoji_test.js 
文をどうぞ 明日は関ヶ原で憎きあの男を倒すの
[ '明日', '関ヶ原', '男' ]
[ '倒す' ]
[ '憎い' ]

さっそく使ってみた

var readline = require('readline');
var kuromoji = require("kuromoji");

kuromoji.builder({
    dicPath: "node_modules/kuromoji/dict"
}).build(function(err, tokenizer) {
    if (err) {
        throw err;
    }

    // 標準入力をうけつけるインターフェース
    var rl = readline.createInterface({
        input: process.stdin,
        output: process.stdout
    });

    // 入力をうけつけて解析する
    rl.question("文をどうぞ ", function(answer) {
        var result = tokenizer.tokenize(answer);
        console.log(result);
        rl.close();
    });
});

実際に動かしてみた。

$ node kuromoji_test.js 
文をどうぞ 今日はがんばったんだよ
[ { word_id: 126270,
    word_type: 'KNOWN',
    word_position: 1,
    surface_form: '今日',
    pos: '名詞',
    pos_detail_1: '副詞可能',
    pos_detail_2: '*',
    pos_detail_3: '*',
    conjugated_type: '*',
    conjugated_form: '*',
    basic_form: '今日',
    reading: 'キョウ',
    pronunciation: 'キョー' },
  { word_id: 93010,
    word_type: 'KNOWN',
    word_position: 3,
    surface_form: 'は',
    pos: '助詞',
    pos_detail_1: '係助詞',
    pos_detail_2: '*',
    pos_detail_3: '*',
    conjugated_type: '*',
    conjugated_form: '*',
    basic_form: 'は',
    reading: 'ハ',
    pronunciation: 'ワ' },
  { word_id: 3153770,
    word_type: 'KNOWN',
    word_position: 4,
    surface_form: 'がんばっ',
    pos: '動詞',
    pos_detail_1: '自立',
    pos_detail_2: '*',
    pos_detail_3: '*',
    conjugated_type: '五段・ラ行',
    conjugated_form: '連用タ接続',
    basic_form: 'がんばる',
    reading: 'ガンバッ',
    pronunciation: 'ガンバッ' },
  { word_id: 23430,
    word_type: 'KNOWN',
    word_position: 8,
    surface_form: 'た',
    pos: '助動詞',
    pos_detail_1: '*',
    pos_detail_2: '*',
    pos_detail_3: '*',
    conjugated_type: '特殊・タ',
    conjugated_form: '基本形',
    basic_form: 'た',
    reading: 'タ',
    pronunciation: 'タ' },
  { word_id: 63530,
    word_type: 'KNOWN',
    word_position: 9,
    surface_form: 'ん',
    pos: '名詞',
    pos_detail_1: '非自立',
    pos_detail_2: '一般',
    pos_detail_3: '*',
    conjugated_type: '*',
    conjugated_form: '*',
    basic_form: 'ん',
    reading: 'ン',
    pronunciation: 'ン' },
  { word_id: 23680,
    word_type: 'KNOWN',
    word_position: 10,
    surface_form: 'だ',
    pos: '助動詞',
    pos_detail_1: '*',
    pos_detail_2: '*',
    pos_detail_3: '*',
    conjugated_type: '特殊・ダ',
    conjugated_form: '基本形',
    basic_form: 'だ',
    reading: 'ダ',
    pronunciation: 'ダ' },
  { word_id: 92300,
    word_type: 'KNOWN',
    word_position: 11,
    surface_form: 'よ',
    pos: '助詞',
    pos_detail_1: '終助詞',
    pos_detail_2: '*',
    pos_detail_3: '*',
    conjugated_type: '*',
    conjugated_form: '*',
    basic_form: 'よ',
    reading: 'ヨ',
    pronunciation: 'ヨ' } ]

品詞と標準型をとりだしてみる

var readline = require('readline');
var kuromoji = require("kuromoji");

kuromoji.builder({
    dicPath: "node_modules/kuromoji/dict"
}).build(function(err, tokenizer) {
    if (err) {
        throw err;
    }

    // 標準入力をうけつけるインターフェース
    var rl = readline.createInterface({
        input: process.stdin,
        output: process.stdout
    });

    // 入力をうけつけて解析する
    rl.question("文をどうぞ ", function(answer) {
        var result = tokenizer.tokenize(answer);

        // ひとつずつ確認する
        result.forEach(function(word) {
            console.log(word.pos + ' ' + word.basic_form);
        });


        rl.close();
    });
})

標準形にできると形容詞の使い勝手があがりそう。

$ node kuromoji_test.js 
文をどうぞ 明日はマラソン大会で今からすごい憂鬱なの
名詞 明日
助詞 は
名詞 マラソン
名詞 大会
助詞 で
名詞 今
助詞 から
形容詞 すごい
名詞 憂鬱
助動詞 だ
助詞 の

名詞・動詞・形容詞ごとにとりだせるようにしておく

var readline = require('readline');
var kuromoji = require("kuromoji");

kuromoji.builder({
    dicPath: "node_modules/kuromoji/dict"
}).build(function(err, tokenizer) {
    if (err) {
        throw err;
    }

    // 標準入力をうけつけるインターフェース
    var rl = readline.createInterface({
        input: process.stdin,
        output: process.stdout
    });

    // 入力をうけつけて解析する
    rl.question("文をどうぞ ", function(answer) {
        var result = tokenizer.tokenize(answer);

        var noun = [],
            verb = [],
            adjective = [];

        // ひとつずつ確認する
        result.forEach(function(word) {
            switch (word.pos) {
                case '名詞':
                    noun.push(word.basic_form);
                    break;
                case '動詞':
                    verb.push(word.basic_form);
                    break;
                case '形容詞':
                    adjective.push(word.basic_form);
                    break;
            }
        });

        // 一覧を確認する
        console.log(noun);
        console.log(verb);
        console.log(adjective);

        rl.close();
    });

結果はこんな感じ。使いやすそうなので、実際に組み込んでみる。

$ node kuromoji_test.js 
文をどうぞ 明日は関ヶ原で憎きあの男を倒すの
[ '明日', '関ヶ原', '男' ]
[ '倒す' ]
[ '憎い' ]