質問をすることでしか得られない、回答やアドバイスがある。

15分調べてもわからないことは、質問しよう!

新規登録して質問してみよう
ただいま回答率
85.48%
ECMAScript

ECMAScriptとは、JavaScript類の標準を定めるために作られたスクリプト言語です。

デザインパターン

デザインパターンは、ソフトウェアのデザインでよく起きる問題に対して、解決策をノウハウとして蓄積し再利用出来るようにした設計パターンを指します。

プログラミング言語

プログラミング言語はパソコン上で実行することができるソースコードを記述する為に扱う言語の総称です。

JavaScript

JavaScriptは、プログラミング言語のひとつです。ネットスケープコミュニケーションズで開発されました。 開発当初はLiveScriptと呼ばれていましたが、業務提携していたサン・マイクロシステムズが開発したJavaが脚光を浴びていたことから、JavaScriptと改名されました。 動きのあるWebページを作ることを目的に開発されたもので、主要なWebブラウザのほとんどに搭載されています。

関数

関数(ファンクション・メソッド・サブルーチンとも呼ばれる)は、はプログラムのコードの一部であり、ある特定のタスクを処理するように設計されたものです。

Q&A

4回答

8290閲覧

エラーメッセージをまとめて出力する為の最適解は?

think49

総合スコア18162

ECMAScript

ECMAScriptとは、JavaScript類の標準を定めるために作られたスクリプト言語です。

デザインパターン

デザインパターンは、ソフトウェアのデザインでよく起きる問題に対して、解決策をノウハウとして蓄積し再利用出来るようにした設計パターンを指します。

プログラミング言語

プログラミング言語はパソコン上で実行することができるソースコードを記述する為に扱う言語の総称です。

JavaScript

JavaScriptは、プログラミング言語のひとつです。ネットスケープコミュニケーションズで開発されました。 開発当初はLiveScriptと呼ばれていましたが、業務提携していたサン・マイクロシステムズが開発したJavaが脚光を浴びていたことから、JavaScriptと改名されました。 動きのあるWebページを作ることを目的に開発されたもので、主要なWebブラウザのほとんどに搭載されています。

関数

関数(ファンクション・メソッド・サブルーチンとも呼ばれる)は、はプログラムのコードの一部であり、ある特定のタスクを処理するように設計されたものです。

4グッド

10クリップ

投稿2016/11/11 17:18

編集2016/11/11 17:45

計算式を評価する

四則演算の計算式となる文字列を渡すと計算結果を返す関数を作りました(コードは文末の「参考リンク」から読めます)。

JavaScript

1evalCalculation('-1*3*.1+14/7*0.65'); // 1

eval()JSON.parse() に倣い、文法違反な文字列を渡すと例外を返します。

JavaScript

1evalCalculation('*1+3'); // SyntaxError: An expression starts with an unexpected token: * 2evalCalculation('1+3/'); // SyntaxError: An expression ends with an unexpected token: / 3evalCalculation('1.1.1+3'); // SyntaxError: Illegal number: 1.1.1

エラーメッセージをDOM上に出力する

この関数は電卓機能の為に作ったものなので早速組み込みました。
しかし、ユーザが電卓で不正な文字を入力したとしても通常はコンソールまで確認しませんのでエラーメッセージに気が付いてもらえません。
そこで、次のように修正する事を検討しました(下記は仮想コードであり、calculator.js には同コードはありません)。

JavaScript

1function outputErrorMessage (errorMessage) { // 電卓上にエラーメッセージを出力する関数 2 var messages = [ 3 [/^SyntaxError: An expression starts with an unexpected token: ([\s\S]+)$/, '文法違反: "$1" から始まる式は不正です'], 4 [/^SyntaxError: An expression ends with an unexpected token: ([\s\S]+)$/, '文法違反: "$1" で終わる式は不正です'], 5 [/^SyntaxError: Illegal number: ([\s\S]+)$/, '文法違反: "$1" は不正な数値です'] 6 ]; 7 8 for (var i = 0, l = messages.length, message, reg; i < l; ++i) { 9 message = messages[i]; 10 reg = message[0]; 11 12 if (reg.test(errorMessage)) { 13 console.error(errorMessage.replace(reg, message[1])); 14 return; 15 } 16 } 17 18 console.error(errorMessage); 19} 20 21function calc (expression) { 22 try { 23 return evalCalculation(expression); 24 } catch (error) { 25 outputErrorMessage(error.name + ': ' + error.message); 26 return NaN; 27 } 28} 29 30calc('*1+3'); // 文法違反: "*" から始まる式は不正です 31calc('1+3/'); // 文法違反: "/" で終わる式は不正です 32calc('1.1.1+3'); // 文法違反: "1.1.1" は不正な数値です

しかし、これでも出力されるエラーが初めに検出したエラーに限定されるという問題があり、次の状況が考えられます。

  1. ユーザがクリップボードから '*1.1.1+2/' の式を貼り付ける -> エラー「文法違反: "*" から始まる式は不正です」
    1. のエラーを修正し、1.1.1+2/ とする -> エラー「文法違反: "/" で終わる式は不正です」
    1. のエラーを修正し、1.1.1+2 とする -> エラー「文法違反: "1.1.1" は不正な数値です」
    1. のエラーを修正し、1.1+2 とする -> 3.1 が出力される

ユーザの事を考えるなら初めから全てのエラーメッセージを出力する設計が親切といえます。

私が考えた打開案

要点

[1] 関数を一つにするか複数に分けるか
[2] 出力値を固定するか、変動するか
[3] 入力値を固定するか、変動するか

[1] は一つの関数の中で「エラーが発生するか、しないか」のような判断要素を関数に持たせて完結させるか、「XMLHttpRequest と Fetch API」のように根幹となる挙動が同じでも期待する結果が違う考えから関数を分けるか。

[2] は例えば、「エラーが発生した場合にエラーメッセージのリストを返し、エラーが発生なかった場合は Number 型を返す」というように条件に応じて出力値を変えるか、「エラーが発生したら {value: NaN, error: ['エラーメッセージ1', 'エラーメッセージ2']} を返し、エラーが発生しなかったら {value: 12, error: []} を返す」のように Number 型への拘りを止めて出力を固定するか。

[3] は例えば、「evalCalculation('*1+2', true); で関数呼び出ししたなら SyntaxError 例外を発生させずにエラーメッセージリストとなるオブジェクトを返し、evalCalculation('*1+2', false); または evalCalculation('*1+2'); で関数呼び出ししたなら SyntaxError の例外を発生させる、というように入力条件に応じて挙動を変更するか、何らかの形で統一的なインターフェースを確立(一例として 2. の出力値を固定する事例が該当)して入力値を evalCalculation('*1+2'); に固定するか。

打開案コード事例

(1) 関数を一つ、入力値を変動、出力値を変動(Number型、Object型)

第二引数に「例外を発生させないフラグ変数」を指定します(規定動作は例外を発生させます)。

JavaScript

1evalCalculation('1+2', true); // 3 2evalCalculation('*1+2/', true); // {value: NaN, errors: ['SyntaxError: An expression starts with an unexpected token: *', 'SyntaxError: An expression ends with an unexpected token: /']} 3 4evalCalculation('1+2', false); // 3 5evalCalculation('*1+2/', false); // SyntaxError: An expression starts with an unexpected token: * 6 7evalCalculation('1+2'); // 3 8evalCalculation('*1+2/'); // SyntaxError: An expression starts with an unexpected token: *

(2) 関数を一つ、入力値を固定、出力値を固定(Object型)

常に Object 型の値を返しますが、valueOf 関数、toString 関数を書き換える事でプリミティブ値は Number 型と同様の扱いとなります。
下記コードでは分かりやすいようにオブジェクト初期化子で書いていますが、実際にコーディングするなら内部的に new CalculationFormula のようなコンストラクタを作ることになると思います。

JavaScript

1var number1 = evalCalculation('1+2'), // {valueOf: function () { return 3; }, toString: function () { return this.valueOf().toString(); }, errors: []} 2 number2 = evalCalculation('*1+2/'); // {valueOf: function () { return NaN; }, toString: function () { return this.valueOf().toString(); }, errors: [['SyntaxError: An expression starts with an unexpected token: *', 'SyntaxError: An expression ends with an unexpected token: /']} 3 4console.log(number1 * 2); // 6 (* 演算子は Number 型に変換して実行する為、number1 は 3 として扱われる) 5document.body.appendChild(document.createTextNode(number1)); // createTextNode で String 型に変換される為、"3" が出力される

(3) 関数を分ける、入力値を固定、出力値を固定(Number型、Promise)

evalCalculation() は Number 型を返し、evalCalculationPromise() は Promise を返します。

JavaScript

1/** 2 * evalCalculation() 3 * Number 型を返す 4 */ 5evalCalculation('1+2'); // {valueOf: function () { return 3; }, toString: function () { return this.valueOf().toString(); }, errors: []} 6evalCalculation('*1+2/'); // SyntaxError: An expression starts with an unexpected token: * 7 8/** 9 * evalCalculationPromise() 10 * Promise を返す 11 */ 12function calc (expression) { 13 evalCalculationPromise(expression).then(function (value) { // 成功時には value に Number 型の値が格納される 14 console.log(value); 15 }).catch(function (errors) { // 失敗時には errors にエラーメッセージの配列が格納される 16 for (var i = 0, l = errors.length; i < l; ++i) { 17 console.error(errors[i]); 18 } 19 }); 20} 21 22calc('1+2'); // 成功: 3 23calc('*1+2/'); // 失敗: ['SyntaxError: An expression starts with an unexpected token: *', 'SyntaxError: An expression ends with an unexpected token: /']

(4) 関数を一つ、入力値を固定、出力値を固定(Number型、Object型、Promise)

(2), (3) の複合型。

JavaScript

1/** 2 * Calculation.eval() 3 * Number 型を返す 4 */ 5Calculation.eval('1+2'); // 3 6Calculation.eval('*1+2/'); // SyntaxError: An expression starts with an unexpected token: * 7 8/** 9 * Calculation() 10 * Number 型を返す 11 */ 12Calculation('1+2'); // 3 13Calculation('*1+2/'); // SyntaxError: An expression starts with an unexpected token: * 14 15/** 16 * new Calculation 17 * Object 型を返す 18 */ 19new Calculation('1+2'); // {valueOf: function () { return 3; }, toString: function () { return this.valueOf().toString(); }, errors: []} 20new Calculation('*1+2/'); // {valueOf: function () { return NaN; }, toString: function () { return this.valueOf().toString(); }, errors: [['SyntaxError: An expression starts with an unexpected token: *', 'SyntaxError: An expression ends with an unexpected token: /']} 21 22/** 23 * Calculation.evalPromise() 24 * Promise を返す 25 */ 26function calc (expression) { 27 Calculation.evalPromise(expression).then(function (value) { // 成功時には value に Number 型の値が格納される 28 console.log(value); 29 }).catch(function (errors) { // 失敗時には errors にエラーメッセージの配列が格納される 30 for (var i = 0, l = errors.length; i < l; ++i) { 31 console.error(errors[i]); 32 } 33 }); 34} 35 36calc('1+2'); // 成功: 3 37calc('*1+2/'); // 失敗: ['SyntaxError: An expression starts with an unexpected token: *', 'SyntaxError: An expression ends with an unexpected token: /']

所感

直観的には (4) ですが、「名前」や「関数の置き場所」で悩みを抱えています。

  • 「Number 型を返す関数」を Calculation.eval, Calculation のどちらにもっていくべきか
  • new Calculation は必要?Number()new Number() の関係性に持っていくと統一感が出る?
  • evalPromise とは何ぞ?Promise を実行する?

私個人のポリシーとしては ECMAScript や DOM 等の標準 API のインターフェースを参考にして真似る事が多いのですが、同じような設計が見つかりませんでした。
jQuery() で与えられた引数に応じて挙動を変えるケース(関数を渡せば疑似 DOMContentLoaded のハンドラとなり、文字列を渡せば Selectors API となる)がありますが、ECMAScript を見ると関数の出力値は固定されており、入力値に応じて挙動や出力値が大きく変わるのは筋が良くないように感じています。

エラーメッセージをまとめて出力する為の最適解は?

長くなりましたが、要件をまとめると次のようになります。

  • エラーメッセージをまとめて出力したい
  • 成功時/失敗時の出力値で最もわかりやすい設計は何か

一人ひとりで設計思想が違うので答えも一つではないと思いますが、皆さんであればどのように設計するでしょうか。
ご意見お聞かせください。

参考リンク

Lhankor_Mhy, LLman, kei344, kong👍を押しています

気になる質問をクリップする

クリップした質問は、後からいつでもMYページで確認できます。

またクリップした質問に回答があった際、通知やメールを受け取ることができます。

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

guest

回答4

0

個人的には、この場合だとむしろ「1つのエラーメッセージしか出さなくていい」と考えます。

というのも、(コンパイルエラーを想像してもらうとわかりやすいのですが)カッコのネスト構造でミスをしたりすると、1つのミスから連鎖的に多数の構文エラーを生んでしまって、結局何が原因だったのか、わからなくなりかねません

そういう、ノイズの山に埋もれさせてしまうくらいだったら、きちんと「ここで構文解析に失敗しました」という、それだけを出す、と割り切ったほうがスッキリするのではないかと思います。

投稿2016/11/12 01:24

maisumakun

総合スコア145183

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

miyabi-sun

2016/11/12 04:23

これに1票 SQL文の解析やAltJSなどのコンパイラの実装を見ても、 ダメな書き方の先頭に対してシンタックスエラーを吐き出しています。
think49

2016/11/13 03:04

> 1つのミスから連鎖的に多数の構文エラーを生んでしまって、結局何が原因だったのか、わからなくなりかねません。 目から鱗が落ちました。 今回の事例でいえば、evalCalculation('h'); を呼び出せば - 「SyntaxError: An expression starts with an unexpected token: h」 - 「SyntaxError: An expression ends with an unexpected token: h」 が同時に発生しますね。 時にはユーザにトライアンドエラーをさせる事も必要なのが必要なのだと思いました。 他の方の回答からも気付きを得る事が出来たので、そちらが解決してからベストアンサーを選びたいと思います。
guest

0

エラーメッセージをまとめて出力したい

基本的には一度にひとつのエラーメッセージで良い、と私も思います。
芋づる式に連鎖してエラーが大量に起こると、原因が埋もれるからです。

ただ、今回の電卓なら一瞬で答えが出せるでしょうが、
たとえば機械学習のように計算時間がかかるもので、
何時間も経った時点で失敗が判明して最初からやり直しになる場合なら、
なるべく分かるエラーはまとめて先に表示して欲しいですよね。


成功時/失敗時の出力値で最もわかりやすい設計は何か
皆さんであればどのように設計するでしょうか

そこで(より複雑な処理の場合で)、もし私が設計するなら、
エラーチェックと本番の計算を分割します。

[1] 関数を一つにするか複数に分けるか

関数レベルより大きなレベルで複数化します。
エラーを型や例外で個別に扱うと複雑化するからです。
とくに、入力チェックは煩雑なので、例外処理より正常系で扱いたいです。

イメージとしては、静的言語のコンパイルやHTMLのバリデーションとか。
あるいはマイクロサービスやDSLなど、レイヤーを多層化する発想です。


具体的には、小数点のコンマは2回打たないだとかの「文法違反」、
すなわち単なる文字列として扱えるエラーは先に全部出してしまい、
その後で計算する際のエラー(ゼロ除算とか)と分離します。

処理を2回に分けると冗長になって、全体の行数や処理時間は増えるかもしれません。
しかし、四則演算ならともかく、複雑な処理は複雑な設計になってしまいがちです。

そこでチェックと処理を分割すれば、複雑な場合でも単純な設計になります。
「エラーメッセージをまとめて出力」でき、「わかりやすい設計」になります。

より正確には、ゼロ除算などの計算部分のエラーは「まとめて出力」になりませんが、
文字列チェックのときにエラーを多く出せれば、十分に効率的だという考え方です。

今回の場合は四則演算なので、わざわざ分けるのはやや大げさに思えるかもしれません。
ただ、四則演算はサンプルで、後でより複雑なものを作ると想定して回答しました。
とくに前述のように時間がかかる処理のとき、事前チェックのやり方は有効だと思います。

投稿2016/11/12 09:12

LLman

総合スコア5592

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

think49

2016/11/13 15:57

> エラーチェックと本番の計算を分割します。 今回の機能は maisumakun さんが仰るように一つのエラーを返す方向性で考えていますが、後学のために LLman さんが提案された方法で実装してみました。 https://gist.github.com/think49/3c484937a84c91cd5c51f5788c7c34c7 文法チェッカが必要な時には Calculation.checkSyntax() を呼び出した後に Calculation.eval() を呼び出し、文法チェッカが不要な時には Calculation.eval() だけを呼び出す、という形になるのでしょうか。 var errors = Calculation.checkSyntax(expression); if (errors.length) { outputError(errors); // 電卓にエラー出力 } else { output(Calculation.eval(expression)); // 電卓に演算結果を出力 }
think49

2016/11/15 00:06 編集

個人的に迷った部分に「Calculation.eval() 呼び出し時に処理の先頭で Calculation.checkSyntax() を実行するか」があります。 - checkSyntax() を実行しないのなら、ユーザは文法チェックをする為に Calculation.checkSyntax(), Calculation.eval() と2回呼び出さなくてはなりません。 - checkSyntax() を実行するのなら、文法チェックを事前に行いたい場合に Calculation.checkSyntax() を実行してから Calculation.eval() を実行するので内部的に Calculation.checkSyntax() が2回呼び出されます。 LLman さんのお考えでは「何時間も経った時点で失敗が判明して最初からやり直しになる場合」を想定しておられるようですので、「Calculation.eval() に Calculation.checkSyntax() の処理を含めない」方向で実装していますが、この考え方で合っているでしょうか。
LLman

2016/11/15 01:42

>文法チェッカが不要な時には Calculation.eval() だけを呼び出す、という形 >「Calculation.eval() に Calculation.checkSyntax() の処理を含めない」方向で実装 >ユーザは文法チェックをする為に(~)2回呼び出さなくてはなりません。 その考え方で基本的に合っています。 本文の方向性に従うと分離するので、チェックと処理の2回呼び出すことになります。 チェック後は別ファイルに吐きだすとか、WebAPIのように別アプリとして呼ぶとか、 完全に分離した構造をイメージすると、分ける感覚が分かりやすいと思います。 >個人的に迷った部分~2回呼び出さなくてはなりません 迷われているように、たしかに電卓のように処理時間が短くて、 実行回数が多いものは、まとめて実行した方が便利だと思います。 しかし、処理時間が長くて、実行回数が少ないものでは、 工程を何段階かに分割した方が便利です。 たとえば、大量にデータ収集するクローラーだったら、 データ取得と解析の二段階に分けると便利です。 解析に失敗するたび、またWebページの取得を一からやり直すのでは大変です。 それに比べたら、2回実行する手間は少ないです。そう短期間に何度も取得し直さないので。 さらに、取得して問題が起きなかった場合にのみ、解析に進む (別アプリなら自動起動)、といった工夫の余地もまだあります。 その辺は、トレードオフを考慮して選択する問題だと考えています。 まあ今回の場合に適用するのは微妙なところもあるかもしれませんが、 設計の選択肢としては持っておきたいので、本文で回答した次第です。
think49

2016/11/15 01:54

ありがとうございます。大変よくわかりました。 今後の設計の選択肢の一つとして覚えておきたいと思います。
guest

0

(最初の回答文は頓珍漢なので削りました、読む場合は履歴を追って下さい。)

今回のスタンスはSyntaxErrorは最初の1個だけでいいじゃんと考えていますが、
類似の複数エラーを出すべきケースもあると思うので、前提を満たすように考えていきます。

まずは結論からですが、(2)でシンプルで行数も少なくて一番好きです。
Usageもわずか数行で済みます。
例えば計算式をDBに登録したいという要望があっても、バリデーションチェックにわずか1〜2行しかかかりません。

(4)までやるのは反対です。
問題の解決の仕方はシンプルで分かりやすいものが1つあれば十分です。

[2][3]に関しては変動を許容すると、Usageやコアのソースコードがそれだけ追加されます。
十分なUsageやコードを書くコスト、読むコストを払うだけの機能であれば十分ありでしょう。
evalCalcはそれだけの規模なのかという話になりますが、質問文を読む限り利用者目線では無しです。

ただし、プロジェクトやチーム単位で設計や思想が異なるものなので、許容されるケースなら積極的に使った方が良いでしょう。
例えばjQueryとか…個人がオレオレな書き方すると一貫性がなくなり極めて汚いですからね。

[1] 関数を一つにするか複数に分けるか

必要な数だけ分かれて欲しいです。
(2)のアプローチは現実的に最善な落とし所のように感じますし、
もし私に仕事を任せると(2)にそっくりなこんな感じのコードが納品されるでしょう。

個人的にはSyntaxチェックをしてくれる関数は別途あって欲しいので、
Syntaxのチェック自体の処理は軽そうなので、多少冗長ですがこのようにしました。

JavaScript

1var Calculation = { 2 errors_of: function(expr) { 3 var errors; 4 // 各種チェック 5 return errors.map(function(it){ 6 return new SyntaxError(it); 7 }); 8 }, 9 eval: function(expr) { 10 var result; 11 var errors = this.errors_of(expr); 12 if (errors.length > 0) { 13 throw errors[0]; 14 } 15 // 計算実行 16 return result; 17 } 18} 19 20var errors, expr; 21 22// エラー1個でいい 23try { 24 console.log(Calculation.eval(expr)); 25} catch (e) { 26 console.error(e.message); 27} 28 29// エラー複数欲しい 30errors = Calculation.errors_of(expr); 31if (errors.length == 0) { 32 console.log(Calculation.eval(expr)); 33} else { 34 errors.forEach(function(it){ 35 console.error(it.message); 36 }); 37}

[2] 出力値を固定するか、変動するか

今回の例は失敗時に例外を返す為に戻り値は固定されていますが、基本的に戻り値の変動は避けて欲しいです。
PHPの標準関数には成功すると数値、失敗するとfalseが返る関数がありますが、
0はfalsyだけど成功扱いだから比較する時は$result === falseにしてね。アホか。

(1)の例は違和感を極限まで削る事に成功していますが、JavaScriptのErrorは1個想定なので
errorsプロパティに配列が入ったオレオレExceptionになるわけですよね。
出来ればオレオレExceptionはUsageに書きたくないし読みたくないところです…

(1)の妥協案としては、
エンジニアの一般的な感性からすればExceptionならばe.messageで文字列を取り出したいので、
messageをgetterメソッドを定義して、this.errors.map(function(it){return it.message}).join(',')みたいにカンマで区切って返すようにする感じですかね。

[3] 入力値を固定するか、変動するか

PHPにin_arrayという配列から一致する要素があればtrueを返す標準関数がありますが、
デフォルトは==で比較して、第三引数にtrueを入れると===になる糞仕様です。
このように設計者と開発者の感覚がミスマッチすると、毎回最終引数に無駄なtrueを書き込むハメになりますから他の逃げ方が良いと思います。


Promise の可否

MDNのPromiseでは下記のように定義されています。

Promiseオブジェクトは処理の延期(deferred)と非同期処理のために使われます。Promiseはまだ完了していないが、いずれ完了する処理を表します。

evalという同期的な処理の為にPromiseを使うのは流石に冗長過ぎるかなぁ…と感じます。


「別解」

戻り値が複数欲しいというのが根本の質問だと思いますので、
Node.jsでよくあるコールバック関数にしてしまうやり方も非推奨ながら手段の一つではあります。

JavaScript

1var evalCalc = function(it, cb) { 2 // チェック 3 if (syntaxError) { 4 cb errors, NaN; 5 return; 6 } 7 // 計算 8 cb null, result; 9} 10 11evalCalc('1+2', function(err, result) { 12 if (err) { 13 console.error(err); 14 } 15 console.log(result); 16});

投稿2016/11/12 04:47

編集2016/11/14 04:55
miyabi-sun

総合スコア21158

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

think49

2016/11/13 16:25

後だしになって大変申し訳ないのですが…、Promise を返す設計は既に思いついていました。 それを採用しなかった理由は下記の通りです。 - おそらく、多くの人は evalCalculation() が Number 型の値を返す事を期待しているだろう - Promise 等の複雑なオブジェクトが返って Number 型の値を得るのに面倒な手順を踏むことは望まないだろう - ならば、Number 型の値を返しつつ、エラーリストを得る方法を上手い事両立させられないものだろうか 質問文を改めて読み返してみて、上にあげた私の考えが初めに書かれていなかった為に miyabi-sun さんの手を煩わせる事になってしまっていたと思います。お手数おかけしました。
miyabi-sun

2016/11/14 05:07

レス内容と質問文、回答文を見て愕然としました。 いや申し訳ないです… レス内容を元に質問文に出来るだけ沿うように回答しなおしました。 結論LLmanさんと一緒ですね、めっちゃ重複してますが…
think49

2016/11/15 01:36

やはり、最初の一つのエラーで十分というお考えなのですね。 皆さん、そう仰っていますし、私も今はその考えに向いているのでこのままの実装でいこうと考えています。 > (1)の例は違和感を極限まで削る事に成功していますが、JavaScriptのErrorは1個想定なので > errorsプロパティに配列が入ったオレオレExceptionになるわけですよね。 説明が不足していましたが、最初の一個だけエラーを返すことを想定しています. 今ある evalCalculation() は一つの完成形だと思っていて、出来るだけその形は崩したくありませんでした。 (2) もひとつの形だと思いますが、「オレオレオブジェクトを返す実装で本当にいいのか」踏ん切りがつかなかったという思いがありました。 > 個人的にはSyntaxチェックをしてくれる関数は別途あって欲しいので、 こちらのコードでは Calculation.eval() が内部的に Calculation.errors_of() を呼び出していますが、「エラー複数欲しいケース」では Calculation.errors_of() が2回呼び出されます。 「Syntaxのチェック自体の処理は軽そう」から軽い処理なので2回呼び出すコストを許容しているのだと思いますが、Chironianさん、LLman さんは反対に「処理時間がかかるような関数だと無駄な時間がかかる」事を考慮されていたので考え方の違いが興味深いです。 miyabi-sunさんの考える「関数を分ける判断基準」はどのようなものになるのでしょうか。
miyabi-sun

2016/11/15 07:06

> (1)の例は云々 確かにこの辺りの一文おかしいですね。 これまた推敲が足りず申し訳ないです…忘れてください。 一応説明しておくと、第二引数にtrueを設定した状態で走らせる前提の話ですが、 第二引数にtrueを入れた時、例外が走ってオレオレExceptionが返る前提の話になっていました。 > パフォーマンスの低下を許容 育った(職場)の環境ですかね… LiveScript(以下LS)なんてニッチな言語を使っている職場なので、思想もマイノリティを覚悟しています。 LSはCoffeeScriptのようにほぼそのままJSファイルに変換されますが、 設計思想に則って関数型プログラミング的にリストをオラオラ処理し始めると、(遅延評価ができるHaskellとは違い)大幅にパフォーマンスが落ちます。 それでも素のJSに比べて1/3程度にあっさり書けるので、ソースの綺麗さや速く書く事を重視し、納得してLSを採用しています。 > 関数を分ける判断基準 今回の例は「エンドユーザーの入力値」が入ってくるので、堅めなチェックは欲しいと思いました。 またevalCaluculationでは文字列の妥当か否かのみをチェックだけして、ファイルやDB等に格納するケースを想定しています。 また、計算対象の文字列は10文字以下が殆どだろうと考えて許容できると判断しました。 実際に動かしてみて数百文字の評価が多いとなれば(2)のような逃げ方にしていたと思います。 綺麗なコードを書くことを一番、パフォーマンスは二番で考えています。 処理をだらだら記述しそうになると、すぐバラバラに出来ないか、 ネストを減らせないか、もっと読みやすく短く書けないかを判断基準にしています。 (綺麗なコードと言いつつ、JSのコードはゴミですがスルーでお願いします…) 少々極端ですが、関数の呼び出しコストはとりあえず0だと思って書いてますし、 短い文字列の操作も同じく0に近いものと想定して書いてます。 著しく遅くなりそうと感じた場合は、n*nの計算を2nで済ませる、キャッシュ変数等を用意するというアプローチで個別対応しています。 ただし、後で考え直してリファクタリングする事もしょっちゅうです。 今回の例に関しても「SyntaxCheck以外にも0割りとかで評価したらダメなケース」もSyntaxCheckでは拾えないので、とりあえず評価してしまうアプローチの方が良いのかもと考えています。
guest

0

こんにちは。

私もまずは最初の1つだけを返却する方向と思います。
でも、なんとかして全部返したいケースもありますので、その場合を想定して回答します。

まず、異なる関数やバラメータを変える方法は無しです。
処理時間がかかるような関数だと無駄な時間がかかりますし、副作用がある関数だとそもそもうまくいかないと思います。使う側も2回呼び出すコードを書かないと行けないので手間がかかります。

さて、何にせよ、エラーをリストにして返却するしかありませんから、エラー発生の有無とそのエラー・リストをどのようにして返却するのか?が問題です。

戻り値をペアとし、片方は本来の戻り値、もう片方はエラー・リストし、エラー・リスト長が0ならエラー無しと言うI/Fはそれなりに説得力があると思います。
ペアで返却するって見慣れないI/Fですが、本来の戻り値とエラー発生の有無をstd::pairで返却するI/FはC++のSTLで使われています。

昔からあるerrno的な仕組みを拡張することも考えられます。
TLSなグローバル変数にエラー・リストを返却し、関数自体の戻り値でエラー発生の有無を返却するイメージです。
この方法の問題は関数の戻り値でエラー発生の有無を返却し辛い時どうするか?です。

安易に例外を投げれればよいのですが、複数のエラー発生をサポートすると言うことは、最後のエラーのみ例外を投げることができます。しかし、自分が最後のエラーであることは中々判断できない場合が多いと思います。
関数から戻る直前でエラーが発生していたら例外を投げることは考えられますが、例外の最も美味しい「中断」機能を使わないで「例外」のデメリットである処理が重くなるを受け入れるのもどうかと感じます。

ならば、戻り値をペアにするか、戻り値はエラー発生の有無を返却しパラメータで本来欲しい結果を返却するしか無いということかもしれません。


【追記】
ごめんなさい。エラー・リストを返却したい先は関数の呼び出し元ではなく、DOMのコンソールなのですね。

しかし、ユーザが電卓で不正な文字を入力したとしても通常はコンソールまで確認しませんのでエラーメッセージに気が付いてもらえません。

を避けるために、コンソールへ出力している主旨が理解できませんでした。
てっきり呼び出し元で受け取って、何かダイアログ的なものを出力するのだと早合点してました。

投稿2016/11/12 05:13

編集2016/11/12 05:31
Chironian

総合スコア23272

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

think49

2016/11/13 15:26

詳細な解説ありがとうございます。 私がC++未収得な事に起因しますが、内容を理解できない部分がいくつかありました。 理解できる範囲でコード化してみましたが、正直自信がありません…。 https://gist.github.com/think49/47fb5dcd29c552550a73192df6f1d947 > http://blog.livedoor.jp/dormolin/archives/51778178.html こちらの解説から ES6 の Map に近い機能であろうと想定しています。しかしながら、理解しがたい説明もあり、例えば、 > //同じ値を重複して入れる事はできない > ret = map.insert(Pair("D", 4)); ES6 の Map では同じキーの重複を許しませんが、同じ値を入れる事は可能です。 std::map はキー検索もできるとの事ですのでキーの重複も許可しないようですが、「異なるキーとペアになっている値の重複を許さない」のか「同じキー、同じ値のペアで上書きする事を許さない」のか判断に悩みます。 前者なら new Map([['a', 1], ['b', 1]]) を許さず、後者なら new Map([['a', 1]]).set('a', 1) を許さない、という事になります。 ES6 の Map ではどちらも許容される動作であり、後者は関数実行後の構造は全く変わらないですね(Map は同キー/同値のsetでも「上書きする」が、std::map は「何もしない」)。 http://www.ecma-international.org/ecma-262/7.0/#sec-map.prototype.set STL, TLS, errno に関しては私の理解可能な範疇を超えており、「JavaScriptではどのようにコードを書くのか」まで落とし込むことが出来ませんでした。 > ごめんなさい。エラー・リストを返却したい先は関数の呼び出し元ではなく、DOMのコンソールなのですね。 分かりづらい質問ですみません。 eval-calculation.js は汎用性を重視していますので、DOMに出力するような電卓独自の機能は実装しない予定です。 返り値となった「本来の戻り値」「エラーリスト(配列?イテレータ?)」のペアを扱ってごにょごにょすれば、電卓機能としての目的は達成できますのでChironianさんの回答は私の望むものになっていると思います。
Chironian

2016/11/13 16:20

了解です。(私はJavaScriptはほとんど知らないので用語を間違っていたらごめんなさい。) まず、STL(std::map)の件はすいません。混乱させてしまいました。ペアを使ってエラーを返却する方式ってあまり見かけないけど、実はC++の標準規格でも使われているので、極端に特殊な使い方ではないですよって言いたかっただけです。申し訳ない。 「ペア」に関してはJavaScriptの場合は(たぶん)普通にオブジェクトで良いと思います。単に2つの値を保持するだけですから。 その2つの値の片方は、元々返却したかった戻り値(電卓の計算結果)で、もう一つをエラー・メッセージの配列で良い筈です。 Jsonなら知っているので これで表現すると、例えば下記のようなイメージです。 {   "value":計算結果,   "errors":["最初のエラー・メッセージ", "2番めのエラー・メッセージ", ... ] } エラーがなければ、errorsの要素数が0になる感じです。 errnoの件は単なるグローバル変数です。 上記のvalueは普通に電卓の戻り値とし、errorsをグローバル変数として定義して、返却する方法です。ただし、valueを使ってエラーが発生したかどうか表現できる必要があります。NaNで良さそうですね。 TLSはマルチ・スレッド対応のために用いられる概念です。 グローバル変数は通常、プロセスに1セットしか設けられないため、マルチ・スレッド環境で同じグローバル変数をアクセスし、何も手当しないと壊れます。そして、壊れないように排他制御しても、他のスレッドのエラーが自分のスレッドに混じる等の問題が発生するのでこのケースでは使えません。 スレッド毎に獲得するグローバル変数のようなものがTLSですが、これを使うと解決できます。 スレッド毎にerrorsを確保すれば排他制御も不要ですし、他のスレッドの結果が入り交じる心配もありません。 ただ、JavaScriptにTLSサポートがあるのかどうか確認できませんでした。 > //同じ値を重複して入れる事はできない これはリンク先の記述が舌足らずです。C++のstd::mapも同じキーを重複して入れることができません。
think49

2016/11/14 02:10

補足ありがとうございます。ペア, errno, TLS, std::map について概ね理解できたと思います。 > ただ、JavaScriptにTLSサポートがあるのかどうか確認できませんでした。 JavaScript は原則シングルスレッドと理解しています。 # Web Workers を使えばマルチスレッド処理が出来ると聞きますが、私はそこまで学習していない、というのが正直なところです。 # https://www.google.co.jp/search?num=30&q=web+worker+%E3%83%9E%E3%83%AB%E3%83%81%E3%82%B9%E3%83%AC%E3%83%83%E3%83%89 setTimeout, setInterval のような、いかにもマルチスレッドで動作しそうなAPIでもシングルスレッド上で動作するように設計されています。 1. setInterval(task, 500); で500ミリ秒毎に処理時間が1秒の関数taskを実行するとします 2. 500ミリ秒後に関数taskの1回目の処理が開始されます 3. 1000ミリ秒後に関数taskの2回目の処理を開始しようとしますが、前処理(1回目のtask)が完了していないので前処理が完了するまで処理待ちで待機させます 4. 1500ミリ秒後に関数task(1回目)の処理が完了し、待機中であった関数task(2回目)を即座に(500ミリ秒待機しません)実行します。同タイミングで関数taskの3回目の処理を実行しようとしますが、前処理(2回目のtask)が完了していないので処理待ちで待機させます。 5. 以下ループ 処理の連続性は保証されますが、他の処理が上のタスク処理の間に割り込む可能性はあります(その間、setIntervalの処理は割り込み処理が完了するまで待機されます)。 ですので、JavaScriptでは原則グローバル変数は使わず、関数スコープやブロックスコープでスコープを厳密に区切るという使い方をします。 --- (function () { // 他処理から干渉されないように関数スコープを作る  var hoge = 1; // 関数taskからのみ参照可能  setInterval(function task () {   // 1秒かかる処理   hoge++;  }, 500); }()); --- あるいは、引数で渡す処理にします(setIntervalの第三引数はIE9-で使用できませんが、Polyfillで対応する事が出来ます)。 --- setInterval(function task (data) {  // 1秒かかる処理  data.hoge++; }, 500, {hoge: 1}); ---
Chironian

2016/11/14 03:23

そのような環境でしたら、グローバル変数は使わない方が良さそうですね。 ペア返却とグローバル変数返却の両方とも一長一短あるので、言語やプロジェクトの方針に従って、より適した方を使えばよいように思います。 ただ、ペア返却はコンストラクタやデストラクタでのエラーを返却できない問題が残ります。 コンストラクタ内部でerrorsを生成し、コンストラクタの最後で例外を投げるしかないかも知れません。その場合は、全部その仕組みにするという発想もありと思います。 また、JavaScriptによる並行処理方法について、解説頂きありがとうございます。 スタックを1本しか使わないで並列処理するって、マジたいへんですね。 専用のスタックを持つスレッドを使えば、サブルーチン呼び出しの奥深くで「待ち」できますので論理的な構造に従ってサブルーチン分割できますが、スタックが1本しかないとできません。「待ち」毎に分割する必要が有ります。 私の場合は、それでは大きなプログラムをとても作れないので、仕様変更対応で地獄を見そうです。
think49

2016/11/15 00:26

> ただ、ペア返却はコンストラクタやデストラクタでのエラーを返却できない問題が残ります。 instanceof 演算子を利用すると、関数呼び出し時に new 演算子が使われたかが判定できるので内部的に分岐処理させる方法はありそうですね(JavaScriptにデストラクタはないと思います)。 また、関数呼び出ししか想定していない場合はコンストラクタ呼び出し出来ないように制限する場合もあります。 --- function Hoge () {  if (this instanceof Hoge) {   this.value = NaN;   this.errors = [1, 2, 3];  } else {   return {value: NaN, errors: [1, 2, 3]};  } } console.log(new Hoge); console.log(Hoge()); new parseInt; // TypeError: parseInt is not a constructor --- > スタックを1本しか使わないで並列処理するって、マジたいへんですね。 「待ち」に価値を見出すのであれば、setTimeout を使う方法がお勧めです。 --- setTimeout(function task (hoge) {  // 1秒かかる処理  setTimeout(task, 500, ++hoge); }, 500, 1); --- 1. setTimeout(task, 500); で500ミリ秒毎に処理時間が1秒の関数taskを実行するとします 2. 500ミリ秒後に関数task(1回目)の処理が開始されます 3. 1500ミリ秒後に関数task(1回目)の処理が完了し、関数task(2回目)を500ミリ秒後に実行するよう予約します 4. 2000ミリ秒後に関数task(2回目)の処理が開始されます 5. 3000ミリ秒後に関数task(2回目)の処理が完了し、関数task(3回目)を500ミリ秒後に実行するよう予約します 6. 以下ループ setIntervalはタスク開始時刻を元にインターバルを計算しますが、setTimeout(再帰処理)はタスク完了時刻を元にインターバルを計算します。 setIntervalはtask関数内で例外が発生しても止まらないので一定時間毎に例外を発生し続けますが、setTimeout(再帰処理)は例外が発生した時点で後続処理を全て停止します。 総合的にはsetTimeout(再帰処理)が優れていると思います。
Chironian

2016/11/15 03:29

ご質問の主旨は「複数のエラーを返却できる最適解」と理解しています。 できるだけ使えない場面が少ないことも「最適解」の1つと考えています。 コンストラクタは戻り値でペアを返却できません。この影響は結構大きいので、その意味で制限がかかる旨を書きました。 他の代替案は色々あるでしょうけど、「最適解」ということは統一的に複数エラーを返却できる方式が望ましいので、それが可能な方法として例外を使う案も有ることを述べました。 JavaScriptには詳しくないのですが、タグに「プログラミング言語」も入ってますので、一般的に使える可能性のある案で考えています。 「待ち」は他の並列に行いたい処理を行えるタイミングです。ですので、並列処理の要です。 そして、スタックを使わないで「待ち」を構築する場合、サブルーチン呼び出し連鎖の奥で「待ち」できません。 例えば、サーバへ何か一連の要求を発行する際、各要求の応答待ち中、他の処理を並列処理したい場合は少なくないと予想します。(JavaScriptの現場は知らないので予想です。) その応答待ちまで含めた一連の処理を1つのサブルーチンへ纏めることができるとプログラミングはびっくりする程 楽になります。そして、それはスタックフルな同期処理方式なら可能です。 しかし、ご提示されているようなスタックレスな非同期処理方式ではできません。setTimeout()を使ってもこの状況は改善できないです。 さて、スタックフルな処理を行わないのであれば、グローバル変数でエラー返却することはありです。サブルーチン呼び出し連鎖の途中で他の処理が割り込むことはありませんので、他の処理のエラー報告が入り交じる心配はありません。 更にしかし、(Web Workersを使って)マルチ・スレッドする場合にはTLSが必要になるので、そのようなケースでも適用したい場合はTLSサポートがあることまで確認しておかないと使うのは危険と感じます。 伝わり難いことを書いていると思います。申し訳ないです。
guest

あなたの回答

tips

太字

斜体

打ち消し線

見出し

引用テキストの挿入

コードの挿入

リンクの挿入

リストの挿入

番号リストの挿入

表の挿入

水平線の挿入

プレビュー

まだベストアンサーが選ばれていません

会員登録して回答してみよう

アカウントをお持ちの方は

15分調べてもわからないことは
teratailで質問しよう!

ただいまの回答率
85.48%

質問をまとめることで
思考を整理して素早く解決

テンプレート機能で
簡単に質問をまとめる

質問する

関連した質問