はじめに
Errorというクラスを継承してみよう。OpenProcessingにコードが置いてあります。
p5にも出番があるのでここで遊びます。現行バージョンは2.3.2です。
コード全文
/*
messageはErrorの第一引数がそのまま入る。
nameはErrorで固定なので上書きする。
p5の仕様ではこっちが用意したものであっても何かしらエラーが出ると止まるようになっている。
*/
function setup() {
console.clear();
createCanvas(400,400);
catchError(()=>{
NaNErrorCatcher.throw(1/2);
},{success:'NaNでもない',failure:'NaNですって'});
catchError(()=>{
NaNErrorCatcher.throw(0/0);
},{success:'NaNでもない',failure:'NaNですって'});
catchError(() => {
ClassErrorCatcher.throw(Float32Array.from([1,2,3]), Uint8Array);
},{success:'Uint8Arrayです', failure:'Uint8Arrayではないです'});
catchError(() => {
ClassErrorCatcher.throw(Uint8Array.from([0,255,128]), Uint8Array);
},{success:'Uint8Arrayです', failure:'Uint8Arrayではないです'});
let errorCount=0;
// p5はセーフモードなのでエラーがなんか出るとそこで止まる仕組みのようです
// とにかくどんなエラーが出ても止まる。FriendlyErrorは止まらないのに...クソ仕様。
draw = () => {
background(0);
fill(255);
noStroke();
square(200+80*sin(frameCount*TAU/120),200,40);
NaNErrorCatcher.throw((frameCount-120)/0);
}
}
function catchError(process, messages = {}){
const {success = null, failure = null} = messages;
try{
const value = process();
if(success !== null){
switch(typeof(success)){
case 'string': console.log(success); break;
case 'function': success(value); break;
}
}
return true;
}catch(e){
// CustomErrorの場合だけ独自処理
if(e.name === 'CustomError'){
console.error(e.message);
}else{
console.error(`予期せぬエラー:${e}`);
}
}
if(failure !== null){
switch(typeof(failure)){
case 'string': console.log(failure); break;
case 'function': failure(); break;
}
}
return false;
}
class ErrorCatcher extends Error{
constructor(message, value){
// messageで初期化するとmessageプロパティに自動的に入る
super(message);
this.value = value;
}
static validate(value){ return true; }
static throw(process, message = ""){
const value = (typeof process === 'function' ? process() : process);
const check = this.validate(value);
if(check){ return; }
throw new this(message, value);
}
}
class UndefinedErrorCatcher extends ErrorCatcher{
constructor(message, value){
super(message, value);
this.name = 'CustomError';
this.message = `Undefined Error!\n${this.message}`;
}
static validate(value){
// 配列の場合は全ての成分をチェックする
if(Array.isArray(value)){ return value.every((x) => x !== undefined); }
return value !== undefined;
}
}
class NaNErrorCatcher extends ErrorCatcher{
constructor(message, value){
super(message, value);
this.name = 'CustomError';
this.message = `NaN Error!\n${this.message}`;
}
static validate(value){
// 配列の場合は全ての成分をチェックする
if(Array.isArray(value)){ return value.every((x) => !isNaN(x)); }
return !isNaN(value);
}
}
class CompareErrorCatcher extends Error{
constructor(message, value, target){
super(message);
this.value = value;
this.target = target;
}
static validate(value, target){ return true; }
static throw(process, target, message = ""){
const value = (typeof process === 'function' ? process() : process);
const check = this.validate(value, target);
if(check){ return; }
throw new this(message, value, target);
}
}
class TypeErrorCatcher extends CompareErrorCatcher{
constructor(message, value, target){
super(message, value, target);
this.name = 'CustomError';
this.message = `${JSON.stringify(value)}は${target}型ではないです\n${this.message}`;
}
static validate(value, target){ return typeof(value) === target; }
}
class ClassErrorCatcher extends CompareErrorCatcher{
constructor(message, value, target){
super(message, value, target);
this.name = 'CustomError';
this.message = `${JSON.stringify(value)}は${target.name}クラスではないです\n${this.message}`;
}
static validate(value, target){ return (value instanceof target); }
}
エラークラス
Errorはなんか問題が発生すると送出されます。例えば未定義の値に+=とかで値を代入したりなどです。=ならエラーにならないんですが...そういう感じのエラーが出ると、それをキャッチして処理をすることになります。それに使うのがtry~catch構文です。上記のサイトにあるように、
try {
throw new Error("Whoops!");
} catch (e) {
console.error(`${e.name}: ${e.message}`);
}
とかするとtry内でエラーが送出された場合にそれ以降の処理は実行されず、catchに移り、なんかこの、Whoooaとかいうコメントがコンソールに出ます。なおErrorの第一引数の文字列は自動的にmessageプロパティにセットされる仕組みです。nameとありますがこれはこの場合'Error'です。
今回は、このサイトでも紹介されているカスタムエラーによる実装をしようと思います。要するに継承です。派生クラスを作ります。その際、継承先のnameをすべて'CustomError'で統一し、カスタムエラーかどうか区別できるようにします。さらにmessageも若干加工します。
ErrorCatcher
単数をとり、それについて評価するだけのクラスです。NaNかどうか、undefinedかどうかを調べる感じです。
class ErrorCatcher extends Error{
constructor(message, value){
// messageで初期化するとmessageプロパティに自動的に入る
super(message);
this.value = value;
}
static validate(value){ return true; }
static throw(process, message = ""){
const value = (typeof process === 'function' ? process() : process);
const check = this.validate(value);
if(check){ return; }
throw new this(message, value);
}
}
validateは継承先で上書きして判定に使います。静的メソッドも上書きは可能です。これをする意味は、元のクラスでこの関数を使って実装して、派生先で違う内容にすることで同じ内容を何度も書くのを防ぐことです。processは関数もしくは何らかの値です。それをvalidateしてfalseの場合にエラーを作ります。
undefinedとNaNについて作りました。
CompareErrorCatcher
型判定とクラス判定のためにこっちを作りました。何らかの方法で比較して、一致しない場合にエラーを送出します。
class CompareErrorCatcher extends Error{
constructor(message, value, target){
super(message);
this.value = value;
this.target = target;
}
static validate(value, target){ return true; }
static throw(process, target, message = ""){
const value = (typeof process === 'function' ? process() : process);
const check = this.validate(value, target);
if(check){ return; }
throw new this(message, value, target);
}
}
catchError
これだけだといちいちtry~catchで囲むのが面倒なので、そういう関数も作りました。でないとキャッチできないので。
function catchError(process, messages = {}){
const {success = null, failure = null} = messages;
try{
const value = process();
if(success !== null){
switch(typeof(success)){
case 'string': console.log(success); break;
case 'function': success(value); break;
}
}
return true;
}catch(e){
// CustomErrorの場合だけ独自処理
if(e.name === 'CustomError'){
console.error(e.message);
}else{
console.error(`予期せぬエラー:${e}`);
}
}
if(failure !== null){
switch(typeof(failure)){
case 'string': console.log(failure); break;
case 'function': failure(); break;
}
}
return false;
}
processをtry~catchで囲んで、内部でエラーが出た場合、それをキャッチして然るべく処理します。nameが'CustomError'であればmessageを用意します。そうでない場合は普通に出力します。
使用例
catchError(()=>{
NaNErrorCatcher.throw(1/2);
},{success:'NaNでもない',failure:'NaNですって'});
catchError(()=>{
NaNErrorCatcher.throw(0/0);
},{success:'NaNでもない',failure:'NaNですって'});
catchError(() => {
ClassErrorCatcher.throw(Float32Array.from([1,2,3]), Uint8Array);
},{success:'Uint8Arrayです', failure:'Uint8Arrayではないです'});
catchError(() => {
ClassErrorCatcher.throw(Uint8Array.from([0,255,128]), Uint8Array);
},{success:'Uint8Arrayです', failure:'Uint8Arrayではないです'});
NaNでもない sketch_preview.js:449:16
NaN Error!
sketch_preview.js:490:18
NaNですって sketch_preview.js:449:16
{"0":1,"1":2,"2":3}はUint8Arrayクラスではないです
sketch_preview.js:490:18
Uint8Arrayではないです sketch_preview.js:449:16
Uint8Arrayです
なんですって!?
p5.jsの恒常ループ
p5の恒常ループはPromise構文で書かれているので、エラーが出ると処理が止まります。どんなエラーをthrowしても止まる仕組みです。たとえばこうするとframeCountが120になったときにエラーが出て処理が止まります。
draw = () => {
background(0);
fill(255);
noStroke();
square(200+80*sin(frameCount*TAU/120),200,40);
NaNErrorCatcher.throw((frameCount-120)/0, 'ループが止まるぅぅ');
}
Uncaught (in promise) CustomError: NaN Error!
ループが止まるぅぅ
ErrorCatcher blob:https://preview.openprocessing.org/28029757-1802-4fd0-b00d-1f4d13c5faf8:71
NaNErrorCatcher blob:https://preview.openprocessing.org/28029757-1802-4fd0-b00d-1f4d13c5faf8:99
throw blob:https://preview.openprocessing.org/28029757-1802-4fd0-b00d-1f4d13c5faf8:79
draw blob:https://preview.openprocessing.org/28029757-1802-4fd0-b00d-1f4d13c5faf8:36
redraw https://cdn.jsdelivr.net/npm/p5@2.3.2/lib/p5.js:3571
validateParams https://cdn.jsdelivr.net/npm/p5@2.3.2/lib/p5.js:92654
_draw https://cdn.jsdelivr.net/npm/p5@2.3.2/lib/p5.js:74881
_draw https://cdn.jsdelivr.net/npm/p5@2.3.2/lib/p5.js:74905
_draw https://cdn.jsdelivr.net/npm/p5@2.3.2/lib/p5.js:74905
_draw https://cdn.jsdelivr.net/npm/p5@2.3.2/lib/p5.js:74905
_draw https://cdn.jsdelivr.net/npm/p5@2.3.2/lib/p5.js:74905
_draw https://cdn.jsdelivr.net/npm/p5@2.3.2/lib/p5.js:74905
_draw https://cdn.jsdelivr.net/npm/p5@2.3.2/lib/p5.js:74905
_draw https://cdn.jsdelivr.net/npm/p5@2.3.2/lib/p5.js:74905
_draw https://cdn.jsdelivr.net/npm/p5@2.3.2/lib/p5.js:74905
_draw https://cdn.jsdelivr.net/npm/p5@2.3.2/lib/p5.js:74905
_draw https://cdn.jsdelivr.net/npm/p5@2.3.2/lib/p5.js:74905
_draw https://cdn.jsdelivr.net/npm/p5@2.3.2/lib/p5.js:74905
_draw https://cdn.jsdelivr.net/npm/p5@2.3.2/lib/p5.js:74905
_draw https://cdn.jsdelivr.net/npm/p5@2.3.2/lib/p5.js:74905
_draw https://cdn.jsdelivr.net/npm/p5@2.3.2/lib/p5.js:74905
_draw https://cdn.jsdelivr.net/npm/p5@2.3.2/lib/p5.js:74905
_draw https://cdn.jsdelivr.net/npm/p5@2.3.2/lib/p5.js:74905
_draw https://cdn.jsdelivr.net/npm/p5@2.3.2/lib/p5.js:74905
_draw https://cdn.jsdelivr.net/npm/p5@2.3.2/lib/p5.js:74905
_draw https://cdn.jsdelivr.net/npm/p5@2.3.2/lib/p5.js:74905
_draw https://cdn.jsdelivr.net/npm/p5@2.3.2/lib/p5.js:74905
_draw https://cdn.jsdelivr.net/npm/p5@2.3.2/lib/p5.js:74905
_draw https://cdn.jsdelivr.net/npm/p5@2.3.2/lib/p5.js:74905
_draw https://cdn.jsdelivr.net/npm/p5@2.3.2/lib/p5.js:74905
_draw https://cdn.jsdelivr.net/npm/p5@2.3.2/lib/p5.js:74905
_draw https://cdn.jsdelivr.net/npm/p5@2.3.2/lib/p5.js:74905
_draw https://cdn.jsdelivr.net/npm/p5@2.3.2/lib/p5.js:74905
_draw https://cdn.jsdelivr.net/npm/p5@2.3.2/lib/p5.js:74905
_draw https://cdn.jsdelivr.net/npm/p5@2.3.2/lib/p5.js:74905
というわけで、単にスローするだけでよく、わざわざ囲む必要は無いんですが、draw外ではそうではないので、ループ外でエラーをキャッチしたい場合は自前で囲む必要があります。
そうでないエラー
p5はこれとは別に、処理を止めないエラーというものをもっていて、いわゆるフレンドリエラ―と言います。具体的には現行のシステムだとこんな感じのコードを書いた場合に送出されます。
function setup() {
createCanvas(windowWidth, windowHeight);
background(100);
draw = () => {
background(0);
const a=min(1,2,3,4,5);
const b=createVector();
const c=createVector(1,2,3).add(createVector(4,5));
}
}
🌸 p5.js says: Expected at most 2 arguments, but received more in min(). For more information, see https://p5js.org/reference/p5/min. sketch_preview.js:449:16
🌸 p5.jsが言っています: In 1.x, createVector() was a shortcut for createVector(0, 0, 0). In 2.x, p5.js has vectors of any dimension, so you must provide your desired number of zeros. Use createVector(0, 0) for a 2D vector and createVector(0, 0, 0) for a 3D vector. (https://p5js.org/reference/p5.createVector/) sketch_preview.js:449:16
When working with two vectors of different sizes, the smaller dimension is used. In this operation, both vector will be treated as 2D vectors, and any additional values of the linger vector will be ignored. sketch_preview.js:471:17
🌸 p5.js says: Expected at most 2 arguments, but received more in min(). For more information, see https://p5js.org/reference/p5/min. sketch_preview.js:449:16
🌸 p5.jsが言っています: In 1.x, createVector() was a shortcut for createVector(0, 0, 0). In 2.x, p5.js has vectors of any dimension, so you must provide your desired number of zeros. Use createVector(0, 0) for a 2D vector and createVector(0, 0, 0) for a 3D vector. (https://p5js.org/reference/p5.createVector/) sketch_preview.js:449:16
When working with two vectors of different sizes, the smaller dimension is used. In this operation, both vector will be treated as 2D vectors, and any additional values of the linger vector will be ignored. sketch_preview.js:471:17
🌸 p5.js says: Expected at most 2 arguments, but received more in min(). For more information, see https://p5js.org/reference/p5/min. sketch_preview.js:449:16
🌸 p5.jsが言っています: In 1.x, createVector() was a shortcut for createVector(0, 0, 0). In 2.x, p5.js has vectors of any dimension, so you must provide your desired number of zeros. Use createVector(0, 0) for a 2D vector and createVector(0, 0, 0) for a 3D vector. (https://p5js.org/reference/p5.createVector/) sketch_preview.js:449:16
When working with two vectors of different sizes, the smaller dimension is used. In this operation, both vector will be treated as 2D vectors, and any additional values of the linger vector will be ignored. sketch_preview.js:471:17
🌸 p5.js says: Expected at most 2 arguments, but received more in min(). For more information, see https://p5js.org/reference/p5/min. sketch_preview.js:449:16
🌸 p5.jsが言っています: In 1.x, createVector() was a shortcut for createVector(0, 0, 0). In 2.x, p5.js has vectors of any dimension, so you must provide your desired number of zeros. Use createVector(0, 0) for a 2D vector and createVector(0, 0, 0) for a 3D vector. (https://p5js.org/reference/p5.createVector/) sketch_preview.js:449:16
When working with two vectors of different sizes, the smaller dimension is used. In this operation, both vector will be treated as 2D vectors, and any additional values of the linger vector will be ignored. sketch_preview.js:471:17
🌸 p5.js says: Expected at most 2 arguments, but received more in min(). For more information, see https://p5js.org/reference/p5/min. sketch_preview.js:449:16
🌸 p5.jsが言っています: In 1.x, createVector() was a shortcut for createVector(0, 0, 0). In 2.x, p5.js has vectors of any dimension, so you must provide your desired number of zeros. Use createVector(0, 0) for a 2D vector and createVector(0, 0, 0) for a 3D vector. (https://p5js.org/reference/p5.createVector/) ske
2.3.2現在タブーとされているのは、minやmaxの3つ以上の引数による実行(単純なバグなので直してほしいんですが、現在p5は高度な実装にかかりっきりで構ってられないのか完全無視です)、createVectorの引数無しによる実行、引数の個数が異なるベクトルの間の演算、などです。
min/maxは諦めるしかなさそうですね。まあp5.jsってそういうライブラリなんです。
この間、ジオメトリインスタンシングをvertexPropertyで実行した場合も、こんなエラーが出ました。
🌸 p5.jsが言っています: One of the geometries has a custom vertex property 'aFloat' with fewer values than vertices. This is probably caused by directly using the Geometry.vertexProperty() method
しかもこれはフレンドリエラーを防ぐ方法では消せません。コードハックしたらフレンドリー扱いだったんですがなぜか防げませんでした。バグかな?まあいいや。
...って思ったんですが、書く場所を変えたら消えてくれました。
// 書く場所が重要なようで、ここに書くと消えてくれるようです。おそらくmodifyした後なら問題ないのだろう。知らんけど。
p5.disableFriendlyErrors = true;
shader(myShader);
model(geom);
clear();
const pg = gl.getParameter(gl.CURRENT_PROGRAM); // programゲットだぜ
const loc = gl.getAttribLocation(pg, "aFloat"); // ロケーションゲットだぜ
gl.vertexAttribDivisor(loc, 1); // divisorをいじるぜ
draw=()=>{
orbitControl();
background(255);
shader(myShader);
myShader.setUniform('time', millis()*TAU/1000);
lights();
noStroke();
fill(200, 99, 45);
model(geom, 7); // 増えるぜ
}
やはりフレンドリエラ―のようです。書く場所が重要なんですね。初めて知りました。
とはいえ恒常ループ内で実行されるのはうっとうしいので、何とかしたいところです。なお、先ほど挙げた次元の違う(ベクトルに次元があるのほんとに不便なんだが...)ベクトル同士の演算はconsole.warnというやや強い警告での送出のため、フレンドリーではないので防げません。
応急処置
雑な解決策ですが、参考までに。
function setup() {
console.clear();
createCanvas(windowWidth, windowHeight);
background(100);
let friendlyErrorCount = 0;
console.log = (function(){
const txt = arguments[0].toString();
if(txt.match(/🌸 p5.jsが言っています:/) !== null || txt.match(/🌸 p5.js says:/) !== null){
friendlyErrorCount++;
if(friendlyErrorCount > 4){ return; }
}
console.debug(...arguments);
});
let vectorErrorCount = 0;
console.warn = (function(){
const txt = arguments[0].toString();
if(txt.match(/When working with/) !== null){
vectorErrorCount++;
if(vectorErrorCount > 4){ return; }
}
console.debug(...arguments);
});
console.log("はろ~~~~");
console.warn("caution!!!");
draw = () => {
background(0);
const a=min(1,2,3,4,5);
const b=createVector();
const c=createVector(1,2,3).add(createVector(4,5));
if(frameCount%30===0){console.log(frameCount)}
if(frameCount > 600){ noLoop(); }
}
}
ざっくりいうと、console.logとconsole.warnを上書きします。しかし元の関数で上書きするやり方はおそらくできないので、console.debugに回避しています。コンソール処理を殺したいわけではないので。そこでこのように、内部的にフレンドリエラ―の枕詞をキャッチしてカウントし、一定数になったら処理を抜けることにしました。同じように、console.warnの方も同様にしています。もっとも現行の処理では次元の違うベクトルの間の演算は意図した結果にならないので、まああんまりしない方がいいんですが。面倒でも0を追加しましょう。面倒...
確認のために恒常ループ内でconsole.logを実行しています。きちんと表示されます。そしてフレンドリー系は何回か表示したのち、消えます。
はろ~~~~ 8cc6e1f5-306e-4296-89e5-d32fb9ce4626:13:13
caution!!! 8cc6e1f5-306e-4296-89e5-d32fb9ce4626:23:13
🌸 p5.js says: Expected at most 2 arguments, but received more in min(). For more information, see https://p5js.org/reference/p5/min. 8cc6e1f5-306e-4296-89e5-d32fb9ce4626:13:13
🌸 p5.jsが言っています: In 1.x, createVector() was a shortcut for createVector(0, 0, 0). In 2.x, p5.js has vectors of any dimension, so you must provide your desired number of zeros. Use createVector(0, 0) for a 2D vector and createVector(0, 0, 0) for a 3D vector. (https://p5js.org/reference/p5.createVector/) 8cc6e1f5-306e-4296-89e5-d32fb9ce4626:13:13
When working with two vectors of different sizes, the smaller dimension is used. In this operation, both vector will be treated as 2D vectors, and any additional values of the linger vector will be ignored. 8cc6e1f5-306e-4296-89e5-d32fb9ce4626:23:13
🌸 p5.js says: Expected at most 2 arguments, but received more in min(). For more information, see https://p5js.org/reference/p5/min. 8cc6e1f5-306e-4296-89e5-d32fb9ce4626:13:13
🌸 p5.jsが言っています: In 1.x, createVector() was a shortcut for createVector(0, 0, 0). In 2.x, p5.js has vectors of any dimension, so you must provide your desired number of zeros. Use createVector(0, 0) for a 2D vector and createVector(0, 0, 0) for a 3D vector. (https://p5js.org/reference/p5.createVector/) 8cc6e1f5-306e-4296-89e5-d32fb9ce4626:13:13
When working with two vectors of different sizes, the smaller dimension is used. In this operation, both vector will be treated as 2D vectors, and any additional values of the linger vector will be ignored. 3 8cc6e1f5-306e-4296-89e5-d32fb9ce4626:23:13
30 8cc6e1f5-306e-4296-89e5-d32fb9ce4626:13:13
60 8cc6e1f5-306e-4296-89e5-d32fb9ce4626:13:13
90 8cc6e1f5-306e-4296-89e5-d32fb9ce4626:13:13
120 8cc6e1f5-306e-4296-89e5-d32fb9ce4626:13:13
150 8cc6e1f5-306e-4296-89e5-d32fb9ce4626:13:13
180 8cc6e1f5-306e-4296-89e5-d32fb9ce4626:13:13
210 8cc6e1f5-306e-4296-89e5-d32fb9ce4626:13:13
240 8cc6e1f5-306e-4296-89e5-d32fb9ce4626:13:13
270 8cc6e1f5-306e-4296-89e5-d32fb9ce4626:13:13
300 8cc6e1f5-306e-4296-89e5-d32fb9ce4626:13:13
330 8cc6e1f5-306e-4296-89e5-d32fb9ce4626:13:13
360 8cc6e1f5-306e-4296-89e5-d32fb9ce4626:13:13
390 8cc6e1f5-306e-4296-89e5-d32fb9ce4626:13:13
420 8cc6e1f5-306e-4296-89e5-d32fb9ce4626:13:13
450 8cc6e1f5-306e-4296-89e5-d32fb9ce4626:13:13
480 8cc6e1f5-306e-4296-89e5-d32fb9ce4626:13:13
510 8cc6e1f5-306e-4296-89e5-d32fb9ce4626:13:13
540 8cc6e1f5-306e-4296-89e5-d32fb9ce4626:13:13
570 8cc6e1f5-306e-4296-89e5-d32fb9ce4626:13:13
600
もちろん1回だけ表示して消すとか、そもそも表示しないようにするとか、いろいろできます。debugはlogとは厳密には違うらしいんですが、よくわかんないです。ともかくこういう方法もあるというお話でした。
おわりに
ここまでお読みいただいてありがとうございました。