Elixir関連でおすすめの記事を読んでみた
Elixir - The next big language for the web http://www.creativedeletion.com/2015/04/19/elixir_next_language.html
2015年4月の記事。Elixirを勉強するならこれを、とおすすめいただいたので、読んでみた。
導入は、他の言語の使われ方や特徴を書いている。聞いたことがあるお話もちらほら。
One of them is Twitter. In 2009 Twitter had been experiencing a tremendous growth of users and traffic. They decided to change core parts of their infrastructure from Ruby to Scala. The way Scala handles concurrency made it easier for Twitter to handle the growth.
Erlang is also popular for game servers. Millions of users play multiplayer games with game server infrastructure handled by Erlang for titles such as Call of Duty and Game of War.
そして、Erlangに足りていなかった部分を補うように作られたのがElixirという。
But those areas are all addressed in Elixir. Elixir creator José Valim said: “I liked everything I saw in Erlang, but I hated the things that I didn’t see”. Elixir has its own package management system, macros, an easy to use build tool and unicode handling.
Elixirをさわりはじめて感じたのは、確かに美しいコードを書くことができて、 並列性など用途によってはとても利になる長所をもっていると思う。
一方で、仕様変更・アップデートが頻繁に起こるような場合には向かないとも聞く。 どうやったらうまくいくのだろうか、考えてみているけれど、 今のところ答えは見つけられていない気がする。
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:
インラインの関数定義を実現するキーワードとして、fn
とend
が使われているらしい。
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
SqirlzMorphによる有名人の顔画像合成結果 人手による調整がだいぶ入っている気がする… www.boredpanda.com
モーフィングそのものの実現は難しくなさそう。 www.learnopencv.com
- IMAGE MORPH JS
JavaScript向けのモーフィングライブラリも存在する。 matching pointを手動指定した場合のデモがあっていろいろ試せる。
Poisson image Editing
切り抜き位置の調整をした上で、部位位置指定でなめらなかなブレンディングをすると、目元はAさんで口元はBさんみたいな画像ができるはず… ということでなめらかなブレンディングを手軽に利用できそうな実装を探す。
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プランを見つけられなくて困った。 画面右上の「お勧め」を「すべて表示」に変えたら見つかった。 ちなみに、Freeプランだと、
CPU時間は1日あたり60分まで、または、5分あたり2.5分まで
の制限がかかるので用途によっては向かない。
ローカルgitをデプロイ元にする設定場所が見つからない
App Serviceの展開元にあった。
環境変数をどうやって設定するのか
コンソールからできなかったので困った。 アプリケーション設定のアプリ設定にあった。
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、改めておさらいをしておく。
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");
日付とDEBUG
とdefault
の部分に色がついて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');
で、error
とfatal
のほかが無視されている。
$ 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('サーバーがメッセージを受信しました');
この結果、loggerSend
とloggerReceive
で保存先が変わる。
$ 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についてざっと振り返りをした。他にも設定できる項目があるので、使いながらくわしい動きを調べていきたい。