Elixirの存在を知ったのが2014年7月14日。それからおよそ3年経つというのに一向に理解した気になれないでいます。特にSLAナイン・ナインはなぜその数値なのか腑に落ちないでいました。今回『Learn You Some Erlang for Great Good!(通称LYSE本)』を読むことで積年の謎を解明してみようと臨みました。
というわけで、「LYSE本」を読むことにしました。各セクションにはElixirに関係ありそうな箇所を抜粋しています。長い長いメモになるので、TLDRだけで済ませて問題ありません。結論、輪郭は見えてきましたがまだその探求の入り口に来たに過ぎないのだということは理解しました。
> 47 = 45 + 2.
> 47 = 45 + 3.
** exception error: no match of right hand side value 48
> _ = 14+3.
17
> _.
* 1: variable '_' is unbound
after and andalso band begin bnot bor bsl bsr bxor case catch cond div end fun if let not of or orelse query receive rem try when xor false true
number < atom < reference < fun < port < pid < tuple < list < bit string
> 0 == false.
false
> 1 < false.
true
<<"this is a bit string!">>
4-5章からIDEがないとreplなどに時間がとられるので整備しておこう。Emacsならerlang.elがある。インデント、フィルコメント、コメントアウト、OTPなどのscafold、Eshell、コンパイル等ひととおりそろっている。
まず、パフォーマンス上かわらない。
つぎに、引数が複数あるときは関数をつかう。
beach(Temperature) ->
case Temperature of
{celsius, N} when N > 20 andalso N =< 45 ->
'favorable';
{kelvin, N} when N >= 293 andalso N =< 318 ->
'scientifically favorable';
{fahrenheit, N} when N >= 68 andalso N =< 113 ->
'favorable in the US';
_ ->
'avoid beach'
end.
上記のようだと可読性がさがる、冗長的。以下のように関数でまとめる。
beachf({celsius, N}) when N >= 20 andalso N =< 45 ->
'favorable';
beachf({kelvin, N}) when N >=293 andalso N =< 318 ->
'scientifically favorable';
beachf({fahrenheit, N}) when N >= 68 andalso N =< 113 ->
'favorable in the US';
beachf(_) ->
'avoid beach'.
ただし、引数が評価関数の対象の場合はcase文が向いている。
prepend(X, []) ->
[X];
prepend(X, Set) ->
case lists:member(X, Set) of
true -> Set;
false -> [X | Set]
end.
erlang:<type>_to_<type>
という形式をとっているため、型が追加されるたびに変換用関数を BIF (built-in function)
に追加しなければいけないlists
モジュール
sort/1
join/2
last/1
flatten/1
all/1
reverse/1
map/2
filter/2
gb_tree
モジュール
lookup/2
map/2
> (fun() -> a end)().
a
> lists:filter(fun(X) -> X rem 2 == 0 end, lists:seq(1, 10)).
[2,4,6,8,10]
type | error | description |
---|---|---|
Module | Module name 'madule' does not match file name 'module' |
-module 属性内に書いたモジュール名がファイル名と一致していない |
Function | Warning: function somefunction/0 is unused | 関数を公開していない、あるいはその関数が使われている場所が間違った関数名やアリティになっている |
Function | function somefunction/1 undefined | 関数が存在していない: -export 属性内あるいは関数を宣言するときに間違った関数名やアリティを書いてる |
Function | head mismatch | 関数定義を他の関数での先頭の節の間に差し込んでいる |
Syntax | syntax error before: 'SomeCharacterOrWord' | Ex: 括弧の閉じ忘れやタプルやおかしな式接尾辞、予約語・おかしな文字コードにエンコードされたUnicode文字の使用 |
Syntax | syntax error before: | 行末がおかしい |
Variable | Warning: this expression will fail with a 'badarith' exception | Ex: llama + 5
|
Variable | Warning: a term is constructed, but never used | 関数の中に、リス作成、タプル宣言、どんな変数にも束縛されていない無名関数・自身を返す無名関数の宣言がある |
Variable | Warning: variable 'Var' is unused | 使わない変数を宣言している |
Variable | Warning: this clause cannot match because a previous clause at line 4 always matches | モジュール内で定義された関数が catch-all 節のあとに特定の節を持っている |
Variable | variable 'A' unsafe in 'case' |
case ... of の中で宣言されている変数を、その外側で使っている |
type | error | description |
---|---|---|
function clause | no function clause matching somefunction | 関数内のすべてのガード節で失敗、あるいはすべてのパターンマッチで失敗 |
case clause | no case clause matching 'value' | 条件の書き忘れ、誤った種類のデータ送信、 catch-all 節が必要な場合 |
if clause | no true branch found when evaluating an if expression | 条件の書き忘れ、 catch-all 節が必要な場合 |
badmatch | no match of right hand side value | 無理なパターンマッチ、変数束縛の繰り返し |
badarg | bad argument | 誤った引数の呼び出し |
undef | undefined function somefunction | 未定義関数の呼び出し |
badarith | bad argument in an arithmetic expression | 誤った算術演算 |
badfun | bad function | 変数を関数として呼び出した場合 |
badarity | interpreted function with arity 1 called with two arguments | 誤ったアリティ |
1> erlang:error(badarith).
** exception error: bad argument in an arithmetic expression
2> erlang:error(custom_error).
** exception error: custom_error
-record(robot, {name, type=industrial, hobbies, details=[]}).
5> Crusher = #robot{name="Crusher", hobbies=["Crushing people","petting cats"]}.
#robot{name = "Crusher",type = industrial,
hobbies = ["Crushing people","petting cats"],
details = []}
6> Crusher#robot.hobbies. %% 参照
["Crushing people","petting cats"]
7> NestedBot = #robot{details=#robot{name="erNest"}}.
#robot{name = undefined,type = industrial,
hobbies = undefined,
details = #robot{name = "erNest",type = industrial,
hobbies = undefined,details = []}}
8> NestedBot#robot.details#robot.name. %% ネスト参照
"erNest"
キーバリューストアは4つのモジュールで提供されている。
module | methods | description |
---|---|---|
proplist |
get_value/2 , get_all_values/2 , lookup/2 , lookup_all/2
|
少量データ向け, 緩いデータ構造 |
orddict |
store/3 , find/2 , fetch/2 , erase/2
|
少量データ向け(75要素くらい) |
dict |
store/3 , find/2 , fetch/2 , erase/2 , map/2 , fold/2
|
大量データ向け、ordict と同じインターフェイス |
gb_trees |
enter/2 , lookup/2 , delete_any/2 , insert/3 , get/2 , update/3 , delete/2
|
大量データ向け、データ構造の入出力がわかるスマートモードとわからないネイティブモードがあるため状況に応じて安全性とパフォーマンスのバランスを取ることができる |
セット(集合)は4つのモジュールで提供されている。
module | methods | description |
---|---|---|
ordsets |
new/0 , is_element/2 , add_element/2 , del_element/2 , union/1 , intersection/1
|
少量セット向け、遅いが可読性が高い |
sets |
- | 大量セット向け、ordsets と同じインターフェイス |
gb_sets |
- | 大量セット向け、スマートモード・ネイティブモードがあるため状況に応じ安全性とパフォーマンスのバランスを取ることができる |
sofs |
- | 一意な要素の集合だけではなく、数学的な概念として集合が必要な場合 |
有効グラフは2つのモジュールで提供されている。
module | description |
---|---|
digraph |
生成、変更、エッジ・辺の操作、経路・周の検索 |
digraph_utils |
グラフの誘導、木のテスト、隣接ノードの検索 |
queue
モジュール
api | methods | description |
---|---|---|
Original API |
new/0 , in/2 , out/1
|
キューの作成・削除 |
Extended API |
get/1 , peek/1 , drop/1
|
キューの中身の確認 |
Okasaki API | - | 関数型データ構造にもとづく |
以下、3つの関数をつかい実装する。
erlang:exit(Pid, Why)
- Pid
に対して自分が異常終了で死んだとつたえる。自身は死なない。erlang:process_flag(trap_exit, Value)
- Value
が true ならシステムプロセスとなり、 false ならシステムプロセスでなくなる。デフォルトは false。この関数によりクラッシュしたプロセスをリスタートさせるerlang:register(Atom, Pid)
と erlang:make_ref()
- register/2
で予測不可能な Pid
を Atom
として登録し、make_ref/0
で生成された Reference
をもとにメッセージ送信をおこなう。なお、ここで生成される Reference
は同一Erlang VM上、あるいはクラスタリングしたVM上のみでユニークになる。start_critic2() ->
spawn(?MODULE, restarter, []).
restarter() ->
process_flag(trap_exit, true),
Pid = spawn_link(?MODULE, critic2, []),
register(critic2, Pid),
receive
{'EXIT', Pid, normal} -> % not a crash
ok;
{'EXIT', Pid, shutdown} -> % manual termination, not a crash
ok;
{'EXIT', Pid, _} ->
restarter()
end.
judge2(Band, Album) ->
Ref = make_ref(),
critic2 ! {self(), Ref, {Band, Album}},
receive
{Ref, Criticism} ->
Criticism
after 2000 ->
timeout
end.
critic2() ->
receive
{From, Ref, {"Rage Against the Turing Machine", "Unit Testify"}} ->
From ! {Ref, "They are great!"};
{From, Ref, {"System of a Downtime", "Memoize"}} ->
From ! {Ref, "They're not Johnny Crash but they're good."};
{From, Ref, {"Johnny Crash", "The Token Ring of Fire"}} ->
From ! {Ref, "Simply incredible."};
{From, Ref, {_Band, _Album}} ->
From ! {Ref, "They are terrible!"}
end,
critic2().
27> functions:start_critic2().
<0.125.0>
28> functions:judge2("The Doors", "Light my Firewall").
"They are terrible!"
29> exit(whereis(critic2), kill).
true
30> whereis(critic2).
<0.129.0>
31> functions:judge2("The Doors", "Light my Firewall").
"They are terrible!"
ebin/
include/
priv/
src/
event.erl
- event module
evserv.erl
- event server
sup.erl
- supervisor
Emakefile
{'src/*', [debug_info,
{i, "src"},
{i, "include"},
{outdir, "ebin"}]}.
$ erl -make && erl -pa ebin/
Erlang/OTP 19 [erts-8.1] [source] [64-bit] [smp:2:2] [async-threads:10] [kernel-poll:false]
Eshell V8.1 (abort with ^G)
1> evserv:start().
<0.59.0>
2> evserv:subscribe(self()).
{ok,#Ref<0.0.2.84>}
3> evserv:add_event("Hey there3", "test", { {2017, 7, 9}, {1, 23, 59} }).
ok
4> evserv:listen(2000).
[{done,"Hey there3","test"}]
今回はよくつかわれるビヘイビア gen_server
。この汎用化によって、メンテナンス、コードの単純化、テストの簡易化へとつながる。下記に gen_server
のコールバックの概要を記す。
init/1
の反対今回は有限ステートマシン (finite state machine)。ビヘイビア gen_fsm
をとりあげる。このビヘイビアは gen_server
に基本似た挙動をするが、呼び出しやメッセージ投入をあつかうのではなく同期や非同期のイベントをあつう。
今回はイベントハンドラ。ビヘイビアは gen_event
をつかう。
今回のビヘイビアはスーパバイザ supervisor
。 監視戦略の概要にふれてみる。
{ok, { {RestartStrategy, MaxRestart, MaxTime}, [{ChildId, StartFunc, Restart, Shutdown, Type, Modules}] } }.
{ok, { {one_for_all, 5, 60}, [ {fake_id, {fake_mod, start_link, [SomeArg]}, permanent, 5000, worker, [fake_mod]}, {other_id, {event_manager_mod, start_link, []}, transient, infinity, worker, dynamic} ] } }
.one_for_one
one_for_all
rest_for_one
simple_one_for_one
one_for_one
はワーカのリストを起動した順で保持している一方 、simple_one_for_one
はワーカへの定義を dict
で保持している今回はプロセスプール管理アプリケーションをOTPを使ってつくる。
機能
状態
systools
により複数の appup
から relup
として構成されている。バージョンがあがるほどリリースの更新は煩雑になっていく。
appup
ファイルを作成するappup
ファイルをこれらのリリースから生成するリリース作業として以下の通り。relup
構成ツールとして、Erlangは relx
、Elixirは distillery
がある。
appup
ファイルに記述config
ファイルに記述systools:make_relup
で relup
を作成release_hanlder
によって更新%% {NewVersion,
%% [{VersionUpgradingFrom, [Instructions]}]
%% [{VersionDownGradingTo, [Instructions]}]}.
{"1.1.0",
[{"1.0.0", [{add_module, pq_quest},
{load_module, pq_enemy},
{load_module, pq_events},
{update, pq_player, {advanced, []}, [pq_quest, pq_events]}]}],
[{"1.0.0", [{update, pq_player, {advanced, []}},
{delete_module, pq_quest},
{load_module, pq_enemy},
{load_module, pq_events}]}]}.
{"1.0.1",
[{"1.0.0", [{load_module, sockserv_serv}]}],
[{"1.0.0", [{load_module, sockserv_serv}]}]}.
eunit:test(モジュール名, [verbose])
で実行_test()
に対してテストをおこなう_tests.erl
foo_test -> ?assert(is_number(ops:add(1, 2))), ?assertEqual(4, ops:add(2, 2)).
foo_test_ -> [test_them_1(), test_them_2, ?_assertError(badarith, 1/0)].
{setup, Setup, Instantiator}
{setup, Setup, Cleanup, Instantiator}
{setup, Where, Setup, Instantiator}
{setup, Where, Setup, Cleanup, Instantiator}
{foreach, Setup, [Instantiator]}
{foreach, Setup, Cleanup, [Instantiator]}
{foreach, Where, Setup, [Instantiator]}
{foreach, Where, Setup, Cleanup, [Instantiator]}
{spawn, TestSet}
- 並行処理{timeout, (時間 秒), TestSet}
- 時間指定処理{inorder, TestSet}
- 逐次処理{inparallel, TestSet}
- 並列処理{Comment, Fixture}
?assert(expression)
?assertNot(expression)
?assertEqual(A, B)
?assertMatch(Pattern, Expression)
?assertError(Pattern, Expression)
?assertThrow(Pattern, Expression)
?assertExit(Pattern, Expression)
?assertException(Class, Pattern, Expression)
フィクスチャー (foreach) をつかったテストジェネレータ
-define(setup(F), {setup, fun start/0, fun stop/1, F}).
%% 関数some2について
some2_test_() ->
[{"SetupData1を適切に評価できること",
{foreach,
?setup(fun (SetupData1) ->
[some_instantiator1(SetupData1),
some_instantiator2(SetupData1),
...
some_instantiatorN(SetupData1)]
end}},
{"SetupData2を並列処理で適切に評価できること",
{foreach,
?setup(fun (SetupData2) ->
{inparallel,
[some_instantiator1(SetupData2),
some_instantiator2(SetupData2),
...
some_instantiatorN(SetupData2)]}
end)}}].
並列・並行でテストする際の注意点
make_ref()
によって一意の値をつかうこと。erl -env ERL_MAX_ETS_TABLES Number
で設定set
ordered_set
O(log N)
)bag
duplicate_bag
ets:new/2
ets:insert(Table, ObjectOrObjects)
ets:lookup(Table, Key)
ets:delete(Table, Key)
ets:match(Table, MatchClause)
ets:match_object(Table, MatchClause)
ets:fun2ms(MatchSpecClause)
setテーブルでnamed_tableオプションをつける場合
21> ets:new(ingredients, [set, named_table]).
ingredients
22> ets:insert(ingredients, {bacon, great}).
true
23> ets:lookup(ingredients, bacon).
[{bacon,great}]
24> ets:insert(ingredients, [{bacon, awesome}, {cabbage, alright}]).
true
25> ets:lookup(ingredients, bacon).
[{bacon,awesome}]
26> ets:lookup(ingredients, cabbage).
[{cabbage,alright}]
27> ets:delete(ingredients, cabbage).
true
28> ets:delete(ingredients, cabbage).
true
29> ets:lookup(ingredients, cabbage).
[]
32> ets:insert_new(ingredients, {tomato, hey}).
true
33> ets:insert_new(ingredients, {tomato, hey}).
false
bagテーブルでnamed_tableオプションをつけない場合
34> TabId = ets:new(ingredients, [bag]).
16401
35> ets:insert(TabId, {bacon, delicious}).
true
36> ets:insert(TabId, {bacon, fat}).
true
37> ets:insert(TabId, {bacon, fat}).
true
38> ets:lookup(TabId, bacon).
[{bacon,delicious},{bacon,fat}]
ordered_setターブルで、named_tableオプションをつける場合
42> ets:new(ingredients, [ordered_set, named_table]).
ingredients
43> ets:insert(ingredients, [{ketchup, "not much"}, {mustard, "a lot"}, {cheese, "yes", "goat"}, {patty, "moose"}, {onions, "a lot", "caramelized"}]).
true
44> Res1 = ets:first(ingredients).
cheese
45> Res2 = ets:next(ingredients, Res1).
ketchup
46> Res3 = ets:next(ingredients, Res2).
mustard
47> ets:last(ingredients).
patty
48> ets:prev(ingredients, ets:last(ingredients)).
onions
named_tableオプションつきbagテーブルでパターンマッチをする
53> ets:new(table, [named_table, bag]).
table
54> ets:insert(table, [{items, a, b, c, d}, {items, a, b, c, a}, {cat, brown, soft, loveable, selfish}, {friends, [jem, jeff, etc]}, {items, 1, 2, 3, 1}]).
true
55> ets:match(table, {items, '$1', '$2', '_', '$1'}).
[[a,b],[1,2]]
56> ets:match(table, {items, '$114', '$212', '_', '$6'}).
[[d,a,b],[a,a,b],[1,1,2]]
57> ets:match_object(table, {items, '$1', '$2', '_', '$1'}).
[{items,a,b,c,a},{items,1,2,3,1}]
58> ets:delete(table).
true
前提
Erlangが提供する道具
Erlangクラスタを設定する
erl -name LongName
erl -sname ShortName
net_kernel:connect_node/1
- ノード接続node/0
- 現在のノード名を参照nodes/0
- 接続ノード参照link
- ノードをまたいだリンクerlang:monitor/2
- ノードをまたいだモニタ(ネットワーク分断時に一斉に活性化し負荷が増加する可能性あり)erlang:monitor_node/2
- ノードを指定したモニタspawn/2
spawn/4
spawn_link/2
spawn_link/4
erl -sname ShortName -setcookie CookieName
erlang:get_cookei/0
erlang:set_cookei/2
erlang:send(Dest, Message, [noconnect])
- クラスタを介さずにノードに接続erl -sname ShortName -hidden
- クラスタを介さずに隠しノードに接続nodes(hidden)
- 隠しノードを参照4369
erl -name LongName -kernel inet_dist_listen_min 9100 -kernel inet_dist_listen_max 9115
erl -name LongName -config ports
[{kernel, [{inet_dist_listen_min, 9100}, {inet_dist_listen_max, 9115}]}].
net_kernel
start([Name, Type, HeartbeatInMilliseconds])
- インスタンスを一時的にノード化set_net_ticktime(Milliseconds)
- Ticktime(ハートビートを4倍した時間、ノードの死亡判定時間)を設定変更global
- プロセスレジストリの代替
register_name(Name, Pid)
- gloabl登録し名前変更unregister_name(Name, Pid)
- globalから名前解除re_register_name(Name, Pid)
- 参照先の喪失を防ぎながらglobal登録し名前変更whereis_name(Name)
- PID探索send(Name, Message)
- メッセージ送信random_exit_name/3
- ランダムにプロセスをkillするrandom_notify_name/3
- 2つのプロセスのうちいかすプロセスをランダムに1つ選び、globalから解除されるプロセスに {global_name_conflict, Name}
というメッセージを送信notify_all_name/3
- 指定したPIDプロセスをglobalから解除し、 {global_name_conflict, Name, OtherPid}
というメッセージを送信、コンフリクト解消を促すrpc
call(Node, Module, Function, Args[, Timout])
async_call(Node, Mod, Fun, Args[, Timout])
yield(AsyncPid)
nb_yield(AsyncPid[, Timeout])
- ポーリング、ロングポーリングcast(Node, Mod, Fun, Args)
- 返値なしのコールmulticall(Nodes, Mod, Fun, Args)
- 複数ノードへのコールeval_everywhere(Nodes, Mod, Fun, Args)
- 複数ノードへの命令(コールとほぼ同じ)構成
ライフサイクル
再起動戦略
適切なユースケース
テーブルオプション
ram_copies
- データをETS(メモリ)にのみ保存、32ビットマシンで4Gの容量制限disc_only_copies
- データをDETSにのみ保存、2Gの容量制限disc_copies
- データをETSとDETS双方に保存、DETSの2G容量制限はない、通常はこちらはつかうset
bag
ordered_set
CLI
erl -name LongName -mnesia dir path/to/db
- dir
変数でスキーマ保存場所を指定関数
mnesia
create_schema(ListOfNodes)
create_table(TableName, Option)
{attributes, List}
- テーブルのカラム名{disc_copies, NodeList}
{disc_only_copies, NodeList}
{ram_copies, NodeList}
- テーブルの保存場所{index, ListOfIntegers}
- インデックスをはる{record_name, Atom}
- テーブルの別名(非推奨){type, Type}
- テーブル種類 (set
, ordered_set
, bag
){local_content, Boolean}
- デフォルト false
。true
にすると、多数のノード上に共有されない固有のローカルテーブルを作成するwait_for_tables
- テーブル読込完了まで待機activity(AccessContext, Fun[, Args])
- クエリの実行方法を指定
transaction
- 非同期トランザクション、トランザクションの完了を待つわけではないので正確ではないsync_transaction
- 同期トランザクションasync_dirty
- 非同期のロックなし処理sync_dirty
- 同期のロックなし処理ets
- MnesiaをつかわずETSテーブルで処理write
delete
read
match_object
select
application
set_env(mnesia, dir, "path/to/db")
- スキーマ保存場所を指定クエリリスト内包表記
qlc
q(Fun, Generator)
- クエリハンドルeval(QueryHandle)
- 評価fold(Fun, Dict, QueryHandle)
今回は静的型チェッカーDialyzerについてかんがえる。
CLI
dialyzer --build_plt --apps erts kernel stdlib mnesia sasl common_test eunit
- PLT作成dialyzer --add_to_plt --apps reltool
- PLT追加dialyzer foo/src/bar.erl
- ファイルを解析dialyzer -r foo/src bar/src --src
- ディレクトリ指定してerlファイルを解析Erlangの型
'some atom
- アトム42
- 整数[]
- 空リスト{}
- 空タプル<<>>
- 空バイナリany()
none()
pid()
port()
reference()
atom()
atom()
binary()
<<_:Integer>>
- 特定サイズのバイナリ<<_:*Integer>>
- 特定のユニットサイズで長さは指定されていないバイナリ<<_:Integer, _:_*OtherInteger>>
- 上記2つの組み合わせ、バイナリの最小の長さを指定する形式integer()
N..M
- 整数の範囲non_neg_integer()
pos_integer()
- ゼロより大きい自然数neg_integer()
- 負の整数float()
fun()
- あらゆる種類の関数fun((...) -> Type)
- 引数のアリティが決まっていない、特定の肩を返す無名関数fun(() -> Type)
fun((Type1, Type2, ..., TypeN) -> Type)
[Type()]
- 特定の型を持つリスト[Type(), ...]
- 特定の型を持つリスト、またリストが空でないことを示すtuple()
{Type1, Type2, ..., TypeN}
- 全要素の型とサイズがわかっているタプルterm()
boolean()
- 'true' | 'false'
byte()
- 0..255
char()
- 0..16#10ffff
number()
- integer() | float()
maybe_improper_list()
- maybe_improper_list(any(), any())
maybe_improper_list(T)
- maybe_improper_list(T, any())
string()
- [char()]
iolist()
- maybe_improper_list(char() | binary() | iolist(), binary() | [])
module()
- atom()
timeout()
- non_neg_integer()
node()
- アトムno_return()
- none()
判定できない例と対策
-module(cards).
-export([kind/1, main/0]).
-type suit() :: spades | clubs | hearts | diamonds.
-type value() :: 1..10 | j | q | k.
-type card() :: {suit(), value()}.
-spec kind(card()) -> 'face' | 'number'. % 注釈をくわえることでdialyzerに警告させる
kind({_, A}) when A >= 1, A =< 10 ->
number;
kind(_) ->
face.
main() ->
number = kind({spades, 7}),
face = kind({hearts, k}),
number = kind({rubies, 4}), % タプルの中の型が違う
face = kind({clubs, q}).
-module(convert).
-export([main/0]).
%% 注釈をつけないとdialyzerは下記のように判定する
% -spec convert(list() | tuple()) -> list() | tuple().
-spec convert(tuple()) -> list();
(list()) -> tuple().
main() ->
[_, _] = convert({a, b}),
{_, _} = convert([a, b]),
[_, _] = convert([a, b]),
{_, _} = convert({a, b}).
%% private
convert(Tup) when is_tuple(Tup) ->
tuple_to_list(Tup);
convert(L=[_|_]) ->
list_to_tuple(L).
テクノロジーの進化は、絶え間ない変化の中で私たちの日常を塗り替えてきました。時には経済的な危機が、新たな可能性を切り拓く契機となることもあります。そこで、過去のリセッション期に生まれたテクノロジーの足
ATKerneyの課題解決パターン は、課題の本質を見極め、効果的な戦略的構造化を通じて解決策を導き出す手法にフォーカスしています。この冒険の旅は、解決者と協力者たちが心を一つにし、課題に立ち向かう様
私はいわゆる就職氷河期世代です。周囲から時折漏れ聞こえる不平のような言葉がありますが、それを単なる不平として片付けるのはもったいない気がします。できれば、その中に新しい視点を見つけ、次のチャンスへ繋げ