JavascriptのPromiseコード間違い探し
Javascriptのコードをレビューしていたら以下のような少々ツッコミどころのあるコードを見つけましたのでメモしておきます。 内容は一部サマっていますが、以下のようなコードです。
function asyncFunction() { return new Promise((resolve, reject) => { //本当は別の非同期処理 setTimeout(function () { resolve('非同期処理完了'); }, 3000); }).then( (result) => { // 成功した処理 console.log(result) return true; }).catch( (_err) => { //失敗した処理 console.error(_err) return false }); } //メイン処理 if(asyncFunction()){ console.log("正常終了処理") }else{ console.log("異常終了処理") }
やりたい処理の意図は以下です。
- メイン処理でPromise を使った非同期処理を行う関数asyncFunctionを呼び出している。
- その呼び出し結果に応じて、呼び出し元がその関数呼び出しが正常か異常か判断しようとしている。
ということがやりたかったのですが、さてこのコードはどこがおかしいでしょうか。
一見すると(?)普通に動くように見えるかもしれませんが、同期処理と非同期処理の理解が混在していることが伺えます。 おかしなところを一言で言うと、以下の部分でasyncFunctionはtrue/falseを返さない。と言うことになります。
if(asyncFunction()){
Promiseのthenのなかで return true
とかかくとそのまま trueが帰ってくるように錯覚したと思うのですが、
Promise.then() 内で return したら Promiseオブジェクトが返されます . なので たとえ上記のasyncFunctionがrejectされたとしても、
ifの判定はelseの方に行きません。
MDN のサイトにもPromise.thenの戻り値について以下のように記載されています。
then メソッドは Promise を返すので、メソッドチェーンができます。 関数が then にハンドラーとして渡されると Promise を返します。同じ Promise がメソッドチェーンの次の then に現れます。
上記はasyncFunction の結果を受けてから 呼び出し元で処理を実行したい意図があったかもしれませんが、非同期処理となるために想定された順序で実行されません。
ですのでありきたりな解決策ですが、正しくは呼び出し元でも thenを使いましょう、と言うことになります。
function asyncFunction() { return new Promise((resolve, reject) => { console.log("promise start") //本当は別の非同期処理 setTimeout(function () { resolve('非同期処理完了'); }, 3000); }).then( (result) => { // 成功した処理 console.log(result) return true; }).catch( (_err) => { //失敗した処理 console.error(_err) return false }); } //thenとcatchに変更 asyncFunction().then( () => { console.log("正常終了処理") }).catch( () => { console.log("異常終了処理") })
これで正しくasyncFunctionの結果を待ってから後続処理が実行されました。これでおっけーでしょうか。 実はまだもう一つ問題があります。 asyncFunction内でcatchしてそのままreturnしているものをもう一度catchしようとしています。 catchメソッド内で単純にreturnした場合、 それは resolve扱いになるので、 asyncFunction呼び出し後の "異常終了処理"は実行されません。
なので対策としては、 2回もcatchを入れるのをやめるか、どうしても2段にしたいのであれば、 1つ目のcatchのなかで rejectを呼んでやる必要があります。
function asyncFunction() { return new Promise((resolve, reject) => { console.log("promise start") //本当は別の非同期処理 setTimeout(function () { resolve('非同期処理完了'); }, 3000); }).then( (result) => { // 成功した処理 console.log(result) return true; }) .catch( (_err) => { //失敗した処理 console.error(_err ) // rejectに変更 reject(false) //どうしても外側でもcatch処理行いたい }); } asyncFunction().then( () => { console.log("正常終了処理") }).catch( () => { console.log("異常終了処理") })
これでエラー時は2つ目もcatch処理を実行させることができました。
ちょっと例のコードが実施の処理を省略しているので分かりづらかったですが、 非同期の処理とPromiseの仕様を理解する上で良いミス(題材)だと思ったので、記載しました。
ブロックチェーンアセットがゲームの考え方を変えると思う
最近、ブロックチェーンを使用して動いているDAppsのゲームである、My Crypto Heroes で遊んでみたので、その所感を記載しておこうと思います。
My Crypto Heros とは
www.mycryptoheroes.net ざっくりいうとイーサリアムのブロックチェーンを使用するゲームで、ゲーム内で獲得したアセット(キャラクターや武器[エクステンション]など) がイーサリアムネットワーク上でやり取りすることができるのが特徴です。
2019年3月3日時点では、 DappsRadar によると、My Crypto Heroesが イーサリアムベースのDAppsでは取引量1位になっています。*1
My Crypto Heroesのゲーム内のアセットはERC721という規格に準拠しており、ERC721に準拠したトークンを売買できるサービスを使用すれば、ゲーム内で獲得したアセットを売買することができます。
また現在, My Crypto Heroesでは OpeanSea というERC721トークンの取引所で売買可能です。
ブロックチェーンアセットが与える影響
ゲームを通じて自分のキャラクターを強化し、レアアイテムを集めてバトルに勝つという点では一見すると他のスマホゲームと同じなのですが、上記で記載したようにゲーム内で獲得したアセットが、イーサリアム上のネットワークで取引可能になることで、以下のような違いがあると思います。
(事実上の)換金が公式に認められている。今までの国内のほとんどのゲームは、ゲーム内のでアイテムを現金への換金することを禁止しています(一部の海外のゲームは認められています)が、このゲームは 獲得したアセットが 仮想通貨(イーサリアム)に変換できることを認めています。
ゲームのアセットが規格化され、それを売買できる取引所が独立していることで、ゲーム開発側はERC721に準拠することでイーサリアムの売買プラットフォームとブロックチェーンの永続性を享受することができ、アセット売買もゲーム独自の方法でなく、取引所の取引方法に標準化されます。
このようなアセットの標準化と取引所が分離されることにより、よりゲームに対して、投資・投機的な観点で取引が進むのではないかと予想しています。
取引で簡単にアセットが取引できるため、ゲームを実際にプレイしない人でも、ゲームが期待できそう、将来的に人気が出ると予想される場合、初期の場合からアセットを購入しようというインセンティブが働きます。
実際の例として、 以下のアセットは直近 6.7ETH(約10万円) で売買が成立していますが、このアセットはもともとプレセールで 0.1ETHから売買が開始されていました。 当時と今ではイーサリアムの価格も違いますが、単純なイーサリアムベースの比較ですと数十倍に上がっています。これはゲームプレイ人口だけでなく、投資・投機的観点から購入している人が一定層いるのではないかと考えます。 opensea.io
また、ゲームをプレイする人も自分が苦労して獲得したアセットに価値がつくことでよりインセンティブが上がり、初期のゲームでも積極的に参加するようになると考えられます。
このようにゲームのアセットとそれを交換できるプラットフォームが結びつくことで、アセットの流動性が飛躍的に向上し、ゲームの業界に投資マネーが流れ込み金融市場マーケットが形成され、投資に近い感覚を持ってプレイするようになるのではないかと考えます。
ブロックチェーンゲームがまだ浸透していない理由
上記のように、ブロックチェーンゲームがもっと浸透すれば 今までにないゲームの潮流が現れてくると思いますが、現状はそれほど浸透していません。以下の理由からまだそれほど流行っていないのだろうと考えています。
- 参入障壁が高い。ブロックチェーンゲームをプレイするには、 Metamaskなどのウォレットを入れて、イーサリアムなどの仮想通貨を入手する必要があります。もともと仮想通貨をトレードしていた人はできるかと思いますが、ストアで検索してインストールするだけの他のアプリに比べると、かなり障壁が高いと思われます。
- 大量のトランザクションを捌けない。 現在イーサリアムの処理能力は課題となっており現在改良の話が議論されていますが、一般で使用されているスマホゲームのような大量のトランザクションを高速で捌くというのは現在できません。このような理由から、 My Crypto Heroesもイーサリアムのチェーンを使用するのはアセットの取引など、最小限にとどめているものと考えられます。
- 鍵の管理が難しく、セキュリティリスクが高い。ブロックチェーン技術が以下に改ざんに強く信頼性が高くとも、トークンの持ち主であることを証明する鍵の管理は個人に依存しています。鍵の紛失、漏洩すればトークンは二度と取り戻せません。特に投資家サイドからの視点では、資産が失われるリスクが高いとなかなか参入できないのではないかと考えられます。
ブロックチェーン技術自体がまだまだ発展途上であり、これらの技術的問題はいずれ解消すると考えています。 その時は、ブロックチェーン上でアセットを管理するのが当たり前のような時代になっているのかもしれません。
*1:イーサリアムに限らなければ、現在 取引量ベースではEOSベースのギャンブル系ゲームアプリの方が多くなっています。
3分間シェル芸クッキング: 締切まで 後X営業日です をターミナル起動時に表示する
ふと実装したくなったプチ処理をシェルでサクッと実施するメモしておきます。
本日は, ちょっと先の期日の締切(プロジェクト完了日とか上期終了日など) まで何営業日あるんだろう、それを 毎日表示することができたら便利かも?と思ったので、それをシェルでサクッと作ったらどうなるかをご紹介しようと思います。
仕様としては以下の通りです。
- 指定した期日まで、本日から数えて後何営業日(土日祝除く) をターミナル起動時に表示する。
- 指定した期日は頻繁に変更にされる想定ではないので、ハードコードしても良いとする。
また仕様ではないですが、
- 出来るだけサクッと作成できる方法を選択する
として、上記を作ってみようと思います。
まず祝日の判定ですが、RubyやJSなど使えば祝日判定ライブラリがあるのですが、それらをインストールしたり、日付から祝日かどうかを毎回判定するもの処理が大変なので、今年の祝日リストをサクッとExcelなどで作ります。祝日は内閣府のページ などを参照して引っ張ってきます。
今回は 日付,曜日,祝日かどうか
をCSVに書きました。
作成したCSVは、以下のような内容になります。
上記のファイルをダウンロードして, .calendar.csv
としてホームディレクトリに保存します。
これで下準備は完了で、いよいよシェルによる調理を行います。
色々やり方はあり、 sed
や awk
でももちろんできるのですが、これらに馴染みのない人には謎文字列の羅列に見えがちという欠点がありますので、今回は皆さまに馴染みのある grep
を使うパターンでやりたいと思います。
で、早速ですが実際のシェルを以下に示します。
締め切りは 上半期の終わり (2019-06-30) を入れています。自分は .zshrc
に記載していますが、 bashの人は .bashrc
に記載すれば大丈夫です。
today=`date +%Y-%m-%d` eigyo_bi=`grep ${today} -A 1000 .calendar.csv | grep -B 1000 "2019-06-30" | grep -v "[土日祝]" | wc -l | tr -d " "` echo "##########################" echo "今期の営業日はあと ${eigyo_bi}日です" echo "##########################"
これだけで終わりです。設定した後にシェルを立ち上げると以下のような表示がされると思います。
以上です! が、ちょっとだけ処理の内容を説明しておきます。
today=date +%Y-%m-%d
これは シェル変数today
に本日の日付を所定のフォーマットにして格納しています。 例えば本日の投稿日
で在れば "2019-02-25" という文字列が入ります。
grep ${today} -A 1000 .calendar.csv
これは .calendar.csv の 本日の日付以降1000行を表示しています。 grep -A -1
でも似たような挙動になるようなのですが、ドキュメントに載っていなかったので、適当に1000行としています。
grep -B 1000 "2019-06-30"
さらに、締切の期日 "2019-06-30" 以前の1000行を表示します。これで本日 ~ 締切までの日付一覧が抜粋されている状態となります。
grep -v "[土日祝]"
さらに、土日祝 の文字列のいずれかにマッチした行を省いています -v
は grep でマッチしていない行のみ出力するオプションで、頻出です。
wc -l | tr -d " "
最後に残った行数をカウントしてやれば、営業日の数となります。wc -l
は先頭にスペースが含まれた状態で出力されるので、 tr -d " "
で不要なスペースを除いて、完成です。
とまあざっくりですがこんな感じです。毎回ログイン時に表示されるのは良くないとか、カレンダーを毎年作らないといけないとか、締め切りがハードコードなのは 如何なものか.. というのはもちろんありますが、とりあえずやりたいと思った処理を 10分ぐらいで作れた(ほとんど CSV作成) のでまあこんなもんかなと思います。
もっとこうしたらカッコよくできる、みたいなのがありましたらブログ、Twitterなどでご紹介ください。
RPAが嫌いなエンジニアが多い理由
システム開発者のみならず、PCなどで事務作業を行なっている人はRPAという言葉を 最近よく聞くかと思います。
RPAとは Robotic Process Automation(ロボティック・プロセス・オートメーション)
の略で、 主にPCを使用したデスクワークを自動化して効率化することを指します。
数年前からこのワード自体は耳にしていたのですが、近年ニュースでRPAによる効率化の事例やら、 RPA用のツール導入の話をよく耳にするようになりました。
RPAの導入事例として、例えば以下のようなニュースがあります。
他にも検索したらいくらでも出てきますが、大手の金融機関が事例としてよく挙がっています。
また、RPAのツールとして、以下のようなツールが有名です。
RPAを推進する理由としては、定型的な作業はロボットにやらせて効率化し、 より知的な作業をヒトがやるということが大抵挙げられています。
RPAが嫌いな理由
上記のRPAを推進して効率化すること自体はなんら悪いことではないのですが、 RPAツールがあまり好きではない、関わりたくないと言うエンジニアが一定数います(皆がそう言うわけではないですが)。
私もRPAの話を聞くたびに何かモヤっとするものがあり(RPAによる効率化推進自体は悪いと思っているわけでは無い)、 このRPAツールおよびその推進活動が好きになれない理由を周りのエンジニアの言っていることを含めて少し考えてみました。 私が思うに、以下のような理由があるのではないかと思います。
- 実態としてはExcelマクロやUWSC のような 昔からあるようなPCからの操作を覚えさせて自動実行する仕組みなのに、自動化プログラムをロボットと称してあたかも最新の技術のように謳っていること。
- APIがあればもっと簡単に自動化することが可能なのに、APIが無いが故に古いシステムの古いUIの挙動に合わせた実装を強制されること。
- コード記述不要のGUIのみで開発可能、と謳っているところがほとんどだが、エラーハンドリングなど考慮するとGUIのみでの開発がとても煩雑になること。
私が思うに、本当にプログラムで自動化したいなあら、API を用意すべきだと思います。GUIは人がアクセスするのに適したインターフェースであり、プログラムがアクセスするために適したインターフェースはAPIです (Application Programming Interfaceと言いますし) 。人の操作するインターフェースに対して、無理やり自動化を当てはめているのがRPAで、エンジニア目線で見ると無理をして自動化しているように見えます。なのでRPAの適応対象は APIが存在しなくて、追加開発するには規模が大きすぎるシステムであり、古いシステムが対象となり、エンジニアは古いシステムにあまり関わりたくありません。 Money Forward とか freee のシステムをRPAで効率化と言う話は聞きません。なぜならAPIを使う方がよっぽど楽だからです。
また、コード記述が大抵不要で、GUIで開発できるものが多いですが、どうしてもGUIの開発では表現できる範囲が限定されてしまいます。 PoCなどの検証レベルでは十分に使えるのですが、実運用となると、エラーハンドリングで大抵悩まされます。入力レコードが誤っていたものが入ってきたりとか、システムの応答が遅い/返ってこなかった場合など、これのエラー処理を共通化して処理する仕組みがなかったりなど、泥臭い実装を強いられるようになります。
一言で言うと
上記いくつか考えてみましたが、エンジニアが敬遠する理由を一言で言うと、 古いシステムのツケを無理やり清算するような泥臭い仕組みなので触りたくない上に、最新の技術っぽく喧伝しているところが気に食わない と言った感じなんでしょうか。
プロキシ統合と使用してLambdaのレスポンスをそのままAPI Gatewayにパススルーする
普段、ちょっとしたデータの取得、加工などにAPI Gatewayを使用してLambdaを使用しているのですが、かなりやっつけで開発していたコードで、正常系は問題なく動いていたのですが、エラー時に Lambdaが返したHTTPレスポンスコードが API Gatewayに連携されず、Lambdaが5XX系エラーを返しているのに、API Gatewayが200 OK を 返していたと言う不具合がありました。
Lambdaの統合レスポンスをうまくマッピングしてやれば解消するのですが、
Lambdaプロキシ統合
を使用すればもっと簡単にできそうだったので、その内容をメモしておきます。
今回の対応はAWSの以下の内容を参照して試しました。
Lambdaプロキシ統合の設定
Lambdaプロキシ統合を使用すると、 API GatewayからLambdaへの値の受け渡しの方法が自動化され、個別にパススルーなどの設定することなくAPI Gateway - Lambdaがイイ感じに使用できます。
Lambdaプロキシ統合を使用すると、リクエストパラメータの渡し方やレスポンスの受け取り方が通常と異なるので注意が必要です。
今回は GET メソッドをプロキシ統合で設定した例を示します。 どの言語でも内容は変わりませんが、Lambdaはnodeで書きました。
API Gateway -> Lambdaへのリクエストデータの渡し方
API Gateway -> Lambdaへのデータの受け渡しは以下のフォーマットに従う必要があります。
例えば、API Gatewayから GETリクエストをパラメータ付きで行った場合、Lambda側は以下のようなフォーマットとなります。
API Gatewayからのリクエスト
GET https://xxxxxxxxx.execute-api.ap-northeast-1.amazonaws.com/production/getapi?param1=111¶m2=222
Lambda側での受け取り
{ "resource": "Resource path", "path": "Path parameter", "httpMethod": "GET", "headers": {}, "multiValueHeaders": {}, "queryStringParameters": { "param1": "111", "param2": "222" }, "multiValueQueryStringParameters": {}, "pathParameters": {}, "stageVariables": {}, "requestContext": {}, "body": "{}", "isBase64Encoded": false }
上記のように、 queryStringParameters
に含まれてLambdaに渡されます。
Lambda -> API Gateway へのレスポンスデータの渡し方
Lambda -> API Gatewayへのデータの受け渡しは以下のフォーマットに従う必要があります。
const lambda_response_format = { "isBase64Encoded": false, "statusCode": 200, "headers": {}, "body": "json string" }
body の中には、jsonをstringにしたものを入れてやれば、そのまま API Gatewayにbodyやステータスが連携されます。ステータスコード statusCode
もそのままAPI Gatewayのレスポンスとして返ります(これがやりたかった)。
使用してみた所感
レスポンスがパススルーの設定することなくAPI Gatewayと連動するのは楽と感じる一方で、 若干リクエストやレスポンスのフォーマットを合わせるのに少し戸惑いました。
例えば通常の API Gatewayから /api?param1=111
のようなパラメータ渡しの場合 通常のパススルー(nodeの例)だと event.param1
のようにアクセスできますが、 プロキシ統合だと event.queryStringParameters.param1
のように受け渡し方法が変化するので、その辺りを留意すれば問題ないと感じました。
また、そもそもの話として、軽いAPIをサクッと作りたい場合、 API Gateway - Lambda間のやり取りは意識しないようにできれば良いのかなと思います。この辺りがやりたいだけならGCP(Firebase)のCloud Functionの方が手っ取り早いかもしれません。もしくは以前軽く触れたNetlifyのFunctionsなどを使えば静的コンテンツも含めてサクッとできたりしないかなと期待しています。