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

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

ただいまの
回答率

90.76%

  • JavaScript

    15354questions

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

  • Node.js

    1740questions

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

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

解決済

回答 3

投稿 編集

  • 評価
  • クリップ 2
  • VIEW 229

murabito

score 21

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

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

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

※極力、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)は参照する(キャッシュ等)

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

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

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

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

    クリップを取り消します

  • 良い質問の評価を上げる

    以下のような質問は評価を上げましょう

    • 質問内容が明確
    • 自分も答えを知りたい
    • 質問者以外のユーザにも役立つ

    評価が高い質問は、TOPページの「注目」タブのフィードに表示されやすくなります。

    質問の評価を上げたことを取り消します

  • 評価を下げられる数の上限に達しました

    評価を下げることができません

    • 1日5回まで評価を下げられます
    • 1日に1ユーザに対して2回まで評価を下げられます

    質問の評価を下げる

    teratailでは下記のような質問を「具体的に困っていることがない質問」、「サイトポリシーに違反する質問」と定義し、推奨していません。

    • プログラミングに関係のない質問
    • やってほしいことだけを記載した丸投げの質問
    • 問題・課題が含まれていない質問
    • 意図的に内容が抹消された質問
    • 広告と受け取られるような投稿

    評価が下がると、TOPページの「アクティブ」「注目」タブのフィードに表示されにくくなります。

    質問の評価を下げたことを取り消します

    この機能は開放されていません

    評価を下げる条件を満たしてません

    評価を下げる理由を選択してください

    詳細な説明はこちら

    上記に当てはまらず、質問内容が明確になっていない質問には「情報の追加・修正依頼」機能からコメントをしてください。

    質問の評価を下げる機能の利用条件

    この機能を利用するためには、以下の事項を行う必要があります。

質問への追記・修正、ベストアンサー選択の依頼

  • miyabi-sun

    2018/04/22 22:09

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

    キャンセル

回答 3

+5

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

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

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

再帰関数を引数に取り

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

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

変数巻き上げを使わずに

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

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

高階関数のコンテキスト

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


まとめ

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

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

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

投稿

編集

  • 回答の評価を上げる

    以下のような回答は評価を上げましょう

    • 正しい回答
    • わかりやすい回答
    • ためになる回答

    評価が高い回答ほどページの上位に表示されます。

  • 回答の評価を下げる

    下記のような回答は推奨されていません。

    • 間違っている回答
    • 質問の回答になっていない投稿
    • スパムや攻撃的な表現を用いた投稿

    評価を下げる際はその理由を明確に伝え、適切な回答に修正してもらいましょう。

  • 2018/04/22 12:07

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

    キャンセル

  • 2018/04/22 20:16

    こういう場面は絶対な正解ってのは無くて
    自信満々に言い切ったもの勝ちです。
    私の回答も最善とは言いづらく、最善を探せばキリがないと思ってます。

    今回の質問は情報をしっかりだしてて良い質問ではあったものの答えづらかった印象がありますね、私もかなりの長文になりました。
    たまたまKSwordOfHasteさんが自分の回答があまり良くないなぁと感じるままに回答して、本来回答したかった内容に近いものがたまたま私の所から転がり出てきたからくそーってなってるだけなんじゃないかと思います。

    これは普段私もよく他の回答を見て感じることでもありますので、
    あまり自分がプロだとかアマチュアだとか思い悩まないでください。

    キャンセル

checkベストアンサー

+3

 コード (クロージャ版)

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

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

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

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

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

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

'use strict';
/**
 * do-while版
 */
function sample1 (fn, continuefn, fnargs, thisArgs) {
  let condition;

  thisArgs = Array.isArray(thisArgs) ? thisArgs : [];

  do {
    for (let i = 0, length = fn.length; i < length; ++i) {
      fn[i].apply(thisArgs[i], fnargs);
    }

    condition = continuefn(...fnargs);
    fnargs = condition[1];
  } while (condition[0])
}

/**
 * 再帰版
 */
function sample2 (fn, continuefn, fnargs, thisArgs) {
  thisArgs = Array.isArray(thisArgs) ? thisArgs : [];

  for (let i = 0, length = fn.length; i < length; ++i) {
    fn[i].apply(thisArgs[i], fnargs);
  }

  const condition = continuefn(...fnargs);

  if (condition[0]) {
    sample2(fn, continuefn, condition[1], thisArgs);
  }
}

function fn1 () {
  console.log('-----');
}

function fn2 (number, string) {
  console.log(string.repeat(number));
}

function conditionfn (number, ...args) {
  return [number > 0, [--number, ...args]];
}

sample1([fn1, fn2], conditionfn, [5, '#']);
sample2([fn1, fn2], conditionfn, [5, '#']);

 引数束縛

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

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

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

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

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

sample1({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 12:52 編集

    書いてから気が付きましたが、これ再帰じゃないですね。
    再帰で書き直すには closure3 を作れば可能ですが、これは再帰にする必要ありますかね?
    再帰にしたら、無駄に複雑化するだけのような。

    (2018/04/22 12:55追記)
    やってみたら嘘になる気もしたので、再帰で書き直してみます。

    キャンセル

  • 2018/04/22 13:04

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

    キャンセル

+3

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

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

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

const log = console.log

let nest = ''

function traced(f) {
  const func_name = f.prototype.constructor.name
  return [
    function () {
      const args = [...arguments].join(", ")
      log(`${nest}invoking ${func_name}(${args})`)
      nest += ' '
      let result = f.apply(this, arguments)
      nest = nest.slice(1)
      log(`${nest}result is ${result}`)
      return result
    },
    f
  ]
}

function fact(n) {
  if (n == 0)
    return 1
  else
    return n * fact(n - 1)
}

log('--- original function ---')
log("fact(3) = " + fact(3))

log('--- trace function ---')
let original_fact
[fact, original_fact] = traced(fact)
log("fact(3) = " + fact(3))


log('--- untrace function ---')
fact = original_fact
log("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 12:05

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

    キャンセル

  • 2018/04/22 12:28

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

    キャンセル

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

  • ただいまの回答率 90.76%
  • 質問をまとめることで、思考を整理して素早く解決
  • テンプレート機能で、簡単に質問をまとめられる

関連した質問

同じタグがついた質問を見る

  • JavaScript

    15354questions

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

  • Node.js

    1740questions

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