【解決】関数の定義の理解が足りませんでした

munokura

ユーザー
一応、現状では問題は解決できていると思うのですが、理解が追いつかないのでご助言ください。


choiceCols には指定した変数の値が入ります。

コード:
if (choiceCols >= 2) {

というように、変数の値が2以上である場合、という条件にしたところ解決したのですが、それまでは

コード:
if (choiceCols > 1) {

としていました。

この場合、変数の値が0の時に true にならないにも関わらず、想定通りに動いてくれず、カラム数が0になってしまいました。
これはJavaScriptの仕様の理解不足なのか、記述方法が間違っているのか分からなくなってしまいました。

どうぞ、ご指導ください。
 
最後に編集:
一応試して見ましたが、
if (choiceCols > 1) {
にした状態で変数の値を0で実行したときに、カラムが0になるというのは再現しませんでした。
> 1 と >= 2 は同じ意味であり、どちらで実行しても正常にカラム数は変更されました。
恐らく、別の部分でなにか勘違いされているのではないでしょうか。

ただ、それとは別の問題として
カラム数2 → 選択肢表示 → カラム数1 → 選択肢表示
としたときに選択肢の中が表示されなくなるというバグがありました。

これはupdatePlacementの定義の置き換え部分を消して、
JavaScript:
Window_Selectable.prototype.maxCols = function () {
    return Math.max(choiceCols, 1);
}
を追加することで、正常に表示されるようになるのを確認できました。
 

munokura

ユーザー
一応試して見ましたが、

にした状態で変数の値を0で実行したときに、カラムが0になるというのは再現しませんでした。
> 1 と >= 2 は同じ意味であり、どちらで実行しても正常にカラム数は変更されました。
恐らく、別の部分でなにか勘違いされているのではないでしょうか。

ただ、それとは別の問題として
カラム数2 → 選択肢表示 → カラム数1 → 選択肢表示
としたときに選択肢の中が表示されなくなるというバグがありました。

これはupdatePlacementの定義の置き換え部分を消して、
JavaScript:
Window_Selectable.prototype.maxCols = function () {
    return Math.max(choiceCols, 1);
}
を追加することで、正常に表示されるようになるのを確認できました。
ご助言、ありがとうございます。

ますます、謎が深まってしまいましたが…
どこが根本的なバグを生んでいるのでしょうか???

ご助言を元に、もう少し考えてみます。
 

fspace

ユーザー
おそらくこの部分じゃないでしょうか。

JavaScript:
Window_ChoiceList.prototype.maxCols = function () {
  return choiceCols;
}

この部分の意味は「最大列数をchoiceColsの値に置き換える」ではなく「最大列数を取得する方法を『choiceColsの値を参照する』に置き換える」です。つまり、choiceCols >= 2を判定した直後のchoiceColsの値が参照されるのではなく、maxColsが呼び出された時点でのchoiceColsの値が参照されます。

基本的にはゲームを開始してからprototypeのプロパティを置き換えるようなことはしません。ゲームを開始する前にmaxColsをどのように置き換えたら、実行時の挙動を変化させられるか考えてみるといいと思います。

あとchoiceColsは各関数の中でそれぞれ(constで)宣言した方がいいと思います。

追記:よく見たらあすてぃ~さんが似たようなこと書いてましたが、これだとまだ問題があるので改良してみてください。
 
最後に編集:

munokura

ユーザー
おそらくこの部分じゃないでしょうか。

JavaScript:
Window_ChoiceList.prototype.maxCols = function () {
  return choiceCols;
}

この部分の意味は「最大列数をchoiceColsの値に置き換える」ではなく「最大列数を取得する方法を『choiceColsの値を参照する』に置き換える」です。つまり、choiceCols >= 2を判定した直後のchoiceColsの値が参照されるのではなく、maxColsが呼び出された時点でのchoiceColsの値が参照されます。

基本的にはゲームを開始してからprototypeのプロパティを置き換えるようなことはしません。ゲームを開始する前にmaxColsをどのように置き換えたら、実行時の挙動を変化させられるか考えてみるといいと思います。
コアの理解が足りていないので、ご助言を元に予想すると、updatePlacementが呼び出される前に maxColsが一度別の箇所(継承元?)で呼び出されていて、その時(直前)に取得されている choiceColsが採用されているので、1を指定してから呼び出しても、直前の3が指定されている状態で表示に至ってしまう…という感じでしょうか?
あとchoiceColsは各関数の中でそれぞれ(constで)宣言した方がいいと思います。

追記:よく見たらあすてぃ~さんが似たようなこと書いてましたが、これだとまだ問題があるので改良してみてください。
あすてぃ~氏の助言を元に書き換えたのですが、選択肢については問題が見つかりませんでした。
どんな状況で問題が起こりそうか、教えていただけると何かが理解できるかも知れません。

書き換え後のコードで動いている理屈は、なんとなく分かった気がするのですが、その理解を元に updatePlacement を上書きする形式で書いてみると動きませんでした。
「何も理解できていない」状態で、学習が進んでいないのが悔やまれるところです…
(自分の目的はプラグインを完成させる事ではなく、その過程で学習することです)

頂いた助言と、「なんで、これだと動かない?」となったコードをコメントに残した(思考の過程が伝わると良いのですが)ものをアップしました。

特にprototypeと継承の挙動の理解が足りないのだと予想したのですが、少し調べた程度ではチンプンカンプンなので、そろそろ JavaScript の教本などで全貌を学ばないと無理があるのでしょうか。
(それが当然というか理想的なのは理解していますが)
 
細かいことではありますが「1 < choiceCols」のほうが、数直線に似ているので直感的に読みやすいかもしれません。
ツクールでプラグインを作る範囲であれば、オブジェクト指向の継承をかいつまんでおくのがいいと思います。
例えば「ActorとEnemyは同じ戦闘を行う存在なので、Battlerオブジェクトから継承されて定義されている」みたいな感じです。

プロトタイプベースを本格的に勉強すると、技術者の領域に足を突っ込んでしまいますので。
プラグイン作者として有名な、トリアコンタンさんやしぐれんさんはエンジニアだったはずです。
 
最後に編集:
あすてぃ~氏の助言を元に書き換えたのですが、選択肢については問題が見つかりませんでした。
どんな状況で問題が起こりそうか、教えていただけると何かが理解できるかも知れません。
不覚にもprototypeの書き換え先がWindow_Selectableになってしまっていました。正しくはWindow_ChoiceListです。
そうなっていないとchoiseColsが2以上の状態でメニューなどを開くと表示が崩れてしまいます。
(ムノクラさんのコードではChoiseListに置き換わっていたので大丈夫とは思いますが)

書き換え後のコードで動いている理屈は、なんとなく分かった気がするのですが、その理解を元に updatePlacementを上書きする形式で書いてみると動きませんでした。
「何も理解できていない」状態で、学習が進んでいないのが悔やまれるところです…
(自分の目的はプラグインを完成させる事ではなく、その過程で学習することです)
fspaceさんも仰っていますが、元のコードで問題だったのは
JavaScript:
if (choiceCols >= 2) {
 Window_ChoiceList.prototype.maxCols = function () {
   return choiceCols;
  }
}
この部分で、状態によってmaxColsの定義を動的に変更するという書き方は正直お勧めしません。

coiceColsの値を2 → 1と変更したときにmaxColsが返す値がどうなっているかというのを確認してみると
何が問題なのかについて理解が進む可能性はあると思います。
 

munokura

ユーザー
やはり、理解が届きません…

JavaScript:
    if (choiceCols > 1) {
      Window_ChoiceList.prototype.maxCols = function () {
        console.log('2');
        console.log(choiceCols);
        return choiceCols;
      }
    }

で、ログを見るとchoiceColsの値が0でも、Window_ChoiceList.prototype.maxColsが呼び出されているのは把握できました。
しかし、なぜ呼び出されているのかが理解できません。

if文を無視しているというよりも、上位の流れで一度登録された処理が再処理されている?
という思考に陥ったところで、これまでの助言を読み返したところ…

この部分の意味は「最大列数をchoiceColsの値に置き換える」ではなく「最大列数を取得する方法を『choiceColsの値を参照する』に置き換える」です。つまり、choiceCols >= 2を判定した直後のchoiceColsの値が参照されるのではなく、maxColsが呼び出された時点でのchoiceColsの値が参照されます。

あー、うーん…

つまり、一度(ifの条件をクリアして)Window_ChoiceList.prototype.maxColsを上書きしてしまうと、その後はif文とか関係なくWindow_ChoiceList.prototype.maxColsの処理自体が変化したままになってしまう、ということでしょうか?

関数を呼び出さずに上書きすると、こういう事が起こると…そういう理解で概ね合っているのでしょうか?
 

DarkPlasma

ユーザー
下記プラグインのコードがいつ実行されるか、を考えてみてください。

JavaScript:
(function () {
  'use strict';


  Window_ChoiceList.prototype.hoge = function() {
    console.log('Run every time by calling hoge.');
  };


  console.log('Run once at Boot.');
})();

ここで、 Window_ChoiceList.prototype.hoge に関数を代入する処理はいつ行われるでしょうか。
ゲーム起動時に一度だけです。 Run once at Boot. と表示させる処理と同じです。
(これが何故か、まで遡るには、 PluginManager.setup でDOM操作していることを理解する必要がありますが、ここでは省きます)
 

munokura

ユーザー
通常の関数はゲーム起動時に全て定義され(先にコアが読み込まれ、プラグインでの上書きが読み込まれる)、ゲームが開始する。
元のコードの問題点は、ゲーム中の変化で新しい関数を定義しているため、以降で既に定義された関数を、再定義することができない。
これにより、想像していなかった挙動をしてバグが発生している。

という理解で合っているでしょうか?
(うまく説明できているかも自信がありませんが…)

つまり、今回は、「条件分岐」ではなく「関数の定義」への理解が足りなかったということですね?
 
自分の上の返信に誤りがありました。
カラム数2 → 選択肢表示 → カラム数1 → 選択肢表示
この部分は誤りで、正しくは
カラム数2 → 選択肢表示 → カラム数0 → 選択肢表示
とすると選択肢の中身が表示されなくなる でした。
つまり、一度(ifの条件をクリアして)Window_ChoiceList.prototype.maxColsを上書きしてしまうと、その後はif文とか関係なくWindow_ChoiceList.prototype.maxColsの処理自体が変化したままになってしまう、ということでしょうか?
そういうことになります。
Window_ChoiceList.prototype.maxCols = function () {
この式は色々な要素があって複雑そうに見えるかもしれませんが、意味的には
JavaScript:
let a = 10;
と同じで、左の変数(プロパティ)に右の値を代入している式に過ぎません。
aの値が10になり、次に書き換えが行われるまでaの値が変わらないのと同じように、
maxColsプロパティの値は関数の内容で書き換えられ、次に書き換えが行われるまでその値は変化しません。

これを踏まえた上で、
カラム数2 → 選択肢表示 → カラム数0 → 選択肢表示
をトレースしてみると、今回の件について納得して頂けるのではないかと思います。

補足:
ムノクラさんの書き込み内容から察するにfunctionの中のコードを通過したときにその部分のコードが実行されると
解釈されているように見受けられますが、実際実行されるタイミングは、その関数を呼び出したときです。

また、JS学習用のお勧めの1冊を上げておこうと思います。

以上、参考になれば幸いです。
 
最後に編集:

munokura

ユーザー
補足:
ムノクラさんの書き込み内容から察するにfunctionの中のコードを通過したときにその部分のコードが実行されると
解釈されているように見受けられますが、実際実行されるタイミングは、その関数を呼び出したときです。
そこは誤解していなかったようです。
安心しました。
また、JS学習用のお勧めの1冊を上げておこうと思います。

以上、参考になれば幸いです。
お勉強きらーい(笑)
でも、少しでも目を通せるように努力します。

基本、何でも実践しながら学習するタイプなので…
(受験勉強は過去問をやってから、参考書で調べるタイプ)
 

fspace

ユーザー
元のコードの問題点は、ゲーム中の変化で新しい関数を定義しているため、以降で既に定義された関数を、再定義することができない。
これにより、想像していなかった挙動をしてバグが発生している。
「再定義することができない」というよりは「再定義していない」ですね。さらに言うと「再定義すべきでない」とも言えます。

あとは関数の外側で宣言した変数を関数の内側で参照した時の挙動に誤解がありそうな気がします。

次の関数fgの挙動は明確に違います。
JavaScript:
let value = 42;
function f() { return value; }
function g() { return 42; }

console.log(f()); // 42
console.log(g()); // 42

value = 24;

console.log(f()); // 24;
console.log(g()); // 42;

特にprototypeと継承の挙動の理解が足りないのだと予想したのですが、少し調べた程度ではチンプンカンプンなので、そろそろ JavaScript の教本などで全貌を学ばないと無理があるのでしょうか。
(それが当然というか理想的なのは理解していますが)
基本、何でも実践しながら学習するタイプなので…
(受験勉強は過去問をやってから、参考書で調べるタイプ)
さすがに入門書くらいは一冊読んでおいた方がいいと思います。参考書くらいなら後回しでもいいですが、独学で教科書も読まずに問題集を解き始めるのはかなり無謀です。

JS Primerが良書であることは自分も同意しますが、「はじめに」にも書かれている通り、JS Primerは他の言語を知っている人向けのJavaScriptの入門書であって、プログラミングの入門書ではないので、内容としては若干難しいかもしれません。といっても、代わりとなるいいプログラミングの入門書を知らないので、他におすすめできる書籍があるわけではないですが……。『三年以内に出版または改訂された』『プログラミング初心者向けの』JavaScriptの入門書で評価の高いものを探して通読してみるといいと思います。
 

munokura

ユーザー
なるほど、関数の再定義は今回の流れだとなかったので、そこは勘違いしていました。
再定義は可能であるが、するべきではない…と。
なるほど、なるほど。

fとgの例は理解できているつもりですが…なにか変なこと書きましたかね?

皆さんのご助言で、多くの学びを得ました。
ありがとうございました。
 

fspace

ユーザー
fとgの例は理解できているつもりですが…なにか変なこと書きましたかね?
元のコードでゲーム中にprototypeを変更していたことも問題ではあるんですが、なぜ値が変わってしまったかというとゲーム中に関数を書き換えたからではなく、gのような常に一定の値を返す挙動を期待してfのような関数を書いていたからだと思います。

ゲーム中にprototypeを書き換えないというのは、あくまでも間違えないための工夫やマナーであって、バグの直接の原因ではありません。工夫やマナーに違反したことでよくわからない挙動になってバグが発生した、というところで理解が止まっているように見えたので、どのように処理が行われて値が変わってしまったのか、この辺りに誤解があって理解できていないんじゃないかと思った次第です。
 
トップ