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

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

新規登録して質問してみよう
ただいま回答率
85.50%
Node.js

Node.jsとはGoogleのV8 JavaScriptエンジンを使用しているサーバーサイドのイベント駆動型プログラムです。

JavaScript

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

Q&A

解決済

3回答

408閲覧

再帰関数を引数に取りそれを実行する高階関数のコンテキストを変数巻き上げを使わずに変える方法

murabito

総合スコア108

Node.js

Node.jsとはGoogleのV8 JavaScriptエンジンを使用しているサーバーサイドのイベント駆動型プログラムです。

JavaScript

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

0グッド

2クリップ

投稿2018/04/22 00:07

編集2018/04/22 03:08

再帰関数を引数に取る高階関数を書いていたのですが、ちょっとつまづきました。
試行錯誤の上、動くもの自体は出来たのですが、自分の考えた対策は変数巻き上げを使わないといけないため、あまり読み手(将来の自分)にとって分かりづらいなと感じています。

それを変数巻き上げを使わずに別のより読み手に分かりやすい方法で行えないかと思っております。

言葉での説明が上手く出来そうにないので、質問用に用意した簡易的なコードで説明をいたします。

※極力、ES6を使わずに書きました。

高階関数に渡す関数

function func(n) { if (!n) return; console.log('#'.repeat(n)); newFunc(--n); } func(5); ##### #### ### ## #

単純に#を関数の引数に渡された数だけ出力するという処理を引数が0になるまで繰り返すというものです。

高階関数

function hof(fn) { const hr = '-----' return function inner() { console.log(hr); const args = Array.prototype.slice.apply(arguments); fn.apply(this, args); } } function func(n) { if (!n) return; console.log('#'.repeat(n)); newFunc(--n); //★関数スコープ外のnewFuncを参照 } const newFunc = hof(func); newFunc(5); ----- ##### ----- #### ----- ### ----- ## ----- # -----

質問用に用意した簡易的なコードのため、機能としては意味がないですが、
単純に区切り線を追加する高階関数です。

このコード自体は期待した動作をします。

でも、本当はこうしたかった

function func(n) { if (!n) return; console.log('#'.repeat(n)); func(--n); //★これだと元の`func`を再帰実行してしまう! } const newFunc = hof(func); newFunc(5); ----- ##### #### ### ## #

こちらの方が読み手にとって直感的で分かりやすいと思うのですが、、、

出力される結果は期待通りのものにはなりません。

原因はコード内のコメントに記した通りです。

そのため、最初に載せた高階関数のコードにたどり着いたのですが、やはり、外のスコープの変数を参照しているということと、変数の巻き上げを使っているというところで、僕的には読みづらいと感じてしまいます。
特に実際のコードはこれよりも長い為。

こういう場合は、何か出来る対策はあるのでしょうか?

補足

質問に掲載しているサンプルコードは、表題のことを示す以外の意味はありません。

そのため、「サンプルコードの場合であれば、再帰をつかなくてもこう出来る、高階関数使わなくてもこう出来る」のような話ではなく、以下の条件を前提とした質問となります。

  • 再帰処理が行われる
  • その再帰関数(fn1)を別の関数(fn2)の引数に渡して実行する
  • 再帰関数(fn1)を渡す別の関数(fn2)のスコープにある変数を再帰関数(fn1)は参照する(キャッシュ等)

※別の例を挙げると、フィボナッチの再帰的に行う関数をメモ化したようなものをイメージするとわかりやすいかもしれません。

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

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

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

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

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

miyabi-sun

2018/04/22 13:09

なるほど、それは無理なんでこれ以上の回答は控えます。
guest

回答3

0

再帰関数を引数に取りそれを実行する高階関数のコンテキストを変数巻き上げを使わずに変える方法

タイトルどおりに回答していきます。

質問文のコードはあくまで参考資料であり、
例えば記述してある関数は関数としての要件を満たしていませんので、
一旦ノイズということでわきに追いやって、タイトルのみに集中して考えていきます。

再帰関数を引数に取り

再帰関数はそれ自体が関数なので、引数に取った高階関数は用意すること自体は可能です。
再帰関数はそれ自体がループのような性質であり、
間に処理を挟みたいという利用用途やケースがちょっと分かりづらいです。

むしろ再帰関数自体が公開関数であるべきで、
値を加工する関数を引数にとり受け付けるのが一般的なプログラミングです。

変数巻き上げを使わずに

変数巻き上げって同じスコープの下の行で宣言した変数が、
上の行でもundefined扱いで取れてしまう意図どおりにならない変な挙動ですよね?

別に得になるケースも殆ど無いですし、
無くてもJavaScriptはチューリング完全ですので使わなくて構いません。

高階関数のコンテキスト

変えたい対象は文脈から推測するに、
再帰関数のコンテキストではなく、高階関数のコンテキストですよね?
高階関数は今から作るものなので、お好きなように変えて下さい。


まとめ

再帰関数を引数に取りそれを実行する高階関数のコンテキストを変数巻き上げを使わずに変える方法

「再帰関数を引数に取り」 → 誰でも作れるので可能
「それを実行する高階関数のコンテキストを変える」 → JavaScriptはチューリング完全なので可能
「変数巻き上げを使わず」 → むしろJavaScriptのダメな挙動なので使わない方が良い、もちろん可能

全ての条件を論理積で考えた場合、全て可能なので可能です。

投稿2018/04/22 02:45

編集2018/04/22 13:37
miyabi-sun

総合スコア21158

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

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

KSwordOfHaste

2018/04/22 03:07

自分はこの髙階関数の応用を「関数定義をラップするトリック」と捉えて回答しましたが、関数の機能自体を「より可用性を持つように設計すればよい」というのがmiyabi-sunさんの視点だと思います。確かにそちらの方が有用であると思いました。 自分は覚えたての言語の場合特に言語機能をパズルかトンチ問題のように捉えてしまい本質的にどう設計すべきかという点から目が離れがちです。アマチュアゆえの悲しさかも知れません。コメントする際にはそういうことに注意せねばと思いました。
miyabi-sun

2018/04/22 11:16

こういう場面は絶対な正解ってのは無くて 自信満々に言い切ったもの勝ちです。 私の回答も最善とは言いづらく、最善を探せばキリがないと思ってます。 今回の質問は情報をしっかりだしてて良い質問ではあったものの答えづらかった印象がありますね、私もかなりの長文になりました。 たまたまKSwordOfHasteさんが自分の回答があまり良くないなぁと感じるままに回答して、本来回答したかった内容に近いものがたまたま私の所から転がり出てきたからくそーってなってるだけなんじゃないかと思います。 これは普段私もよく他の回答を見て感じることでもありますので、 あまり自分がプロだとかアマチュアだとか思い悩まないでください。
guest

0

ベストアンサー

コード (クロージャ版)

ようするに、次の要素を「当該関数だけが参照可能な場所に閉じ込めたい」という事でしょうか。

  • loop変数
  • コールバック関数
  • コールバック関数に渡される引数

コード(文字数制限に引っかかったので、jsfiddleに移しました)。

「this値固定」が汎用性に欠けますが、挙動を見る限りでは要件を満たしている気がします。

コード (非クロージャ版)

全体的にクロージャを使用しない方がすっきりしますね。

JavaScript

1'use strict'; 2/** 3 * do-while版 4 */ 5function sample1 (fn, continuefn, fnargs, thisArgs) { 6 let condition; 7 8 thisArgs = Array.isArray(thisArgs) ? thisArgs : []; 9 10 do { 11 for (let i = 0, length = fn.length; i < length; ++i) { 12 fn[i].apply(thisArgs[i], fnargs); 13 } 14 15 condition = continuefn(...fnargs); 16 fnargs = condition[1]; 17 } while (condition[0]) 18} 19 20/** 21 * 再帰版 22 */ 23function sample2 (fn, continuefn, fnargs, thisArgs) { 24 thisArgs = Array.isArray(thisArgs) ? thisArgs : []; 25 26 for (let i = 0, length = fn.length; i < length; ++i) { 27 fn[i].apply(thisArgs[i], fnargs); 28 } 29 30 const condition = continuefn(...fnargs); 31 32 if (condition[0]) { 33 sample2(fn, continuefn, condition[1], thisArgs); 34 } 35} 36 37function fn1 () { 38 console.log('-----'); 39} 40 41function fn2 (number, string) { 42 console.log(string.repeat(number)); 43} 44 45function conditionfn (number, ...args) { 46 return [number > 0, [--number, ...args]]; 47} 48 49sample1([fn1, fn2], conditionfn, [5, '#']); 50sample2([fn1, fn2], conditionfn, [5, '#']);

引数束縛

非クロージャ版で Function.prototype.bind を併用する事で、クロージャ版に近い動作にする事が出来ます。

JavaScript

1sample1.bind(null, [fn1, fn2])(conditionfn, [5, '#']); 2sample1.bind(null, [fn1, fn2]).bind(null, conditionfn)([5, '#']);

しかしながら、Function.prototype.bind は第二引数の実を束縛する事が不可能なので、独自に引数束縛するコードを書いてみました。

JavaScript

1bindFromFunction(sample1, [,conditionfn])([fn1, fn2], null, [5, '#']);

sample1 の関数設計を変える方が美しい設計かも…。

JavaScript

1sample1({fn: [fn1, fn2], conditionfn: conditionfn, args: [5, '#'], thisArgs: thisArgs});

これなら、Function.prototype.bind で束縛可能です。
(※引数束縛対象が単一なのに対し、this 束縛が関数ごとに独立しているのは、設計上美しくないかもしれません。お好みで変更して下さい。)

更新履歴

  • 2018/04/22 13:03 「コード (クロージャ版)」の再帰版を追記
  • 2018/04/22 14:13 「コード (非クロージャ版)」を追記
  • 2018/04/23 00:12 「引数束縛」を追記

Re: murabito さん

投稿2018/04/22 03:49

編集2018/04/22 15:12
think49

総合スコア18156

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

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

think49

2018/04/22 03:55 編集

書いてから気が付きましたが、これ再帰じゃないですね。 再帰で書き直すには closure3 を作れば可能ですが、これは再帰にする必要ありますかね? 再帰にしたら、無駄に複雑化するだけのような。 (2018/04/22 12:55追記) やってみたら嘘になる気もしたので、再帰で書き直してみます。
think49

2018/04/22 04:04

「再帰で書き直すには closure3 を作れば可能」は嘘でした。ごめんなさい。
guest

0

質問者さんの意図を捻じ曲げてしまっているかも知れません。

発想を変えてオリジナルの関数の定義を上書きしてしまい、必要に応じて元に戻せばよいというのではダメでしょうか?

再帰関数の場合、どうしても関数本体内のスコープで自分自身の名前を直接参照したくなります。それを「高階関数の存在を意識して別の関数を呼び出す」とするぐらいなら関数名に束縛する値(関数)を置き換えてしまった方が分かり易い気がしました。

javascript

1const log = console.log 2 3let nest = '' 4 5function traced(f) { 6 const func_name = f.prototype.constructor.name 7 return [ 8 function () { 9 const args = [...arguments].join(", ") 10 log(`${nest}invoking ${func_name}(${args})`) 11 nest += ' ' 12 let result = f.apply(this, arguments) 13 nest = nest.slice(1) 14 log(`${nest}result is ${result}`) 15 return result 16 }, 17 f 18 ] 19} 20 21function fact(n) { 22 if (n == 0) 23 return 1 24 else 25 return n * fact(n - 1) 26} 27 28log('--- original function ---') 29log("fact(3) = " + fact(3)) 30 31log('--- trace function ---') 32let original_fact 33[fact, original_fact] = traced(fact) 34log("fact(3) = " + fact(3)) 35 36 37log('--- untrace function ---') 38fact = original_fact 39log("fact(3) = " + fact(3))

結果:

--- original function --- fact(3) = 6 --- trace function --- invoking fact(3) invoking fact(2) invoking fact(1) invoking fact(0) result is 1 result is 1 result is 2 result is 6 fact(3) = 6 --- untrace function --- fact(3) = 6

少し本質的でないところに手を入れてしまってます。分かりづらかったらスミマセン。

ES5で書かないといけないというわけでもないと思ったのでES2015で書いているつもりですが「どの言語仕様に準拠しているか」あまり意識出来てません。おかしなところがあったらご指摘いただければ嬉しいです。


実際に動かした環境はNodejs 9.0.0のみです。

投稿2018/04/22 01:47

編集2018/04/22 11:32
KSwordOfHaste

総合スコア18392

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

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

murabito

2018/04/22 03:05

ご回答頂きましてありがとうございます。コードを実行出来る状況に今いないため、帰宅しましたらコードを試しつつ理解を深めて参考にさせて頂ければと思います。
KSwordOfHaste

2018/04/22 03:28

自分の回答はちょっと中途半端で再帰呼び出しをすべき関数名の束縛をfunc側で意識するかfuncをwrapする側で意識するかの違いでしかなく、本質的にはmiyabi-sunさんコメントにあるように適用すべき関数を引数にすれば一番自然に解決できると思いました。
guest

あなたの回答

tips

太字

斜体

打ち消し線

見出し

引用テキストの挿入

コードの挿入

リンクの挿入

リストの挿入

番号リストの挿入

表の挿入

水平線の挿入

プレビュー

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

ただいまの回答率
85.50%

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

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

質問する

関連した質問