web-dev-qa-db-ja.com

連鎖関数呼び出しのコーディングスタイル

あなたがしなければならない一般的なことは、値をとり、それを関数に渡すことでそれを使って何かを行い、次に戻り値をチェーンでさらに行うことです。このタイプのシナリオに遭遇したときはいつでも、コードの最適な書き方がわかりません。

例として、私が数字numを持っていて、その床の平方根を取得して文字列に変換する必要があるとします。ここにいくつかの可能性があります(JavaScriptで)。

私は単にnumを一度に1つの関数に渡し、numを戻り値に置き換えるだけです。

function floorSqrt(num) {
    num = Math.sqrt(num);
    num = Math.floor(num);
    num = String(num);
    return num;
}

元のnumはもう必要ないので、これはいいですが、実際には、1つのもの(数値)から始めて、まったく異なるもの(文字列)で終わるのは混乱するかもしれません。

各変数を新しい変数に保存できます。

function floorSqrt(num) {
    var sqrtNum = Math.sqrt(num);
    var floorSqrt = Math.floor(sqrtNum);
    var stringNum = String(floorSqrt);
    return stringNum;
}

これらの変数をすべて宣言し、それらに適切な名前を付けるのは無駄です。

または、すべてを one-liner として実行することもできます。

function floorSqrt(num) {
    return String(Math.floor(Math.sqrt(num)));
}

しかし、2つを超える関数の場合、そのアプローチは コードゴルフ のように信じられないほど読みにくくなります。

これを行う最も美しい方法は、スタックベースの言語を使用することですが、これは次のようになります(疑似コード)。

[sqrt floor toString]

JavaScriptで同様のことを行う方法はありますか?いくつかの関数をリストし、一度に1つずつ実行し、各関数の戻り値を次の引数として使用しますか?最も近いのは、 jQuery または nderscore.js のようないくつかのライブラリーが、次のようなメソッドをチェーンできるようにする方法です。

function floorSqrt(num) {
    return _.chain(num).sqrt().floor().toString().value();
}

多くの賢明な人々がこれについて賢明なことを考えたと思います。異なるスタイルの長所と短所についてどう思いますか?

5
last-child

TL; DR:縮小/構成関数プログラミングイディオム

# define/import: compose(fn1, f2)(arg) => fn2(fn1(arg)) 
# define/import: reduce(fn, [l1, l2, l3, ...]) => fn(fn(l1, l2), l3)...

commands = [Math.sqrt, Math.floor, String]
floorSqrt = reduce(compose, commands)

# floorSqrt(2) == String(Math.floor(Math.sqrt(2)))

ノート:

reduceおよびcomposeの使用は、データが通過する変換のパイプラインをセットアップするときに使用される非常に一般的なイディオムです。読みやすくエレガントです。

<rant>読みやすさと親しみやすさを両立させるプログラマーが多すぎます。彼らは、なじみのないイディオムは読めないと考えています。私にとって、この振る舞いはジレタントとより一致しており、自分の技術に真剣に取り組んでいる人ではありません。 </ rant>

15
dietbuddha

ソフトウェアエンジニアとしての私たちの目標は、他の人間にとって明確で理解しやすいコードを記述することです。ネストされた関数呼び出しを多く記述しすぎると、コードが読みにくくなることにすでに気付いていることをうれしく思います。

したがって、最後に述べたjQueryのような構文が言語でサポートされていないと仮定した場合、残された唯一の優れた代替策は、関数呼び出しの複雑なチェーンをより簡単なステートメントに分割し、それぞれが中間変数に一時的な値を割り当てることです。したがって、2つの選択肢のみが残ります。1)各ステップで同じ変数名を再利用する、2)各ステップで新しいローカル変数を定義する。

そして、あなたが私と同じ結論に達したと仮定して、私はあなたが最後の選択をするのを手伝います:各ステップに新しい変数名を使用する

「5つの変数を使用できるときに5つの変数を宣言するのは無駄です」という議論は、少し時期尚早な最適化です。私はチームに組込みの人を参加させてきましたが、これは彼らが常に破らなければならない習慣の1つでした。各変数には、a)意味のある名前、b)常に同じ意味が必要です。次に、コードを読むときに、特に「num」が何を意味するかを推測する必要はありません。コードは、例のように単純なままではない傾向があるためです。他の誰かが長くなり、いくつかのループとifステートメントを追加し、 "num"変数を再利用し続けると想像してください。何が入っているかを理解するために、たどられたステートメントと「num」が何回再割り当てされたかだけを判断するために、トレースして精神体操を実行する必要があります。

8 KBのRAMを備えた一部のボードで組み込みプログラミングを実行していない場合、同じ目的で同じ変数を複数回再利用する理由はありません。明確でわかりやすい名前を付けて使用します。スタックがアンロールするとき、6整数を削除するのと同じように、スタックから1整数を削除するのとまったく同じコストがかかります。

9
DXM

JavaScriptに制限されていない場合は、多くの関数型プログラミング言語で同様のアプローチを使用できます dietbuddhaにより推奨 (関数合成演算子)。 Haskellを使用してこれを説明します。

中間変数を使用したソリューションは次のようになります。

floorSqrt :: Float -> String
floorSqrt num = let
                  sqrtNum   = sqrt num
                  floorSqrt = floor sqrtNum
                  stringNum = show floorSqrt
                in
                  stringNum

ワンライナーは

floorSqrt num = show (floor (sqrt num))

またはまた

floorSqrt num = (show . floor . sqrt) num

または

floorSqrt = show . floor . sqrt

つまり、.演算子を使用して3つの関数を作成し、結果の関数を引数numに適用できます。上記の最後の例に示すように、複合関数を使用することで、引数numを定義から除外できます。

一般に、一連の関数がある場合

f1, ..., fn

ここで、i∈{1、...、n}の場合:f :a -> b、そしてi∈{1、...、n-1}の場合:b = ai + 1、複合関数を定義できます

compositeFunction = fn .   . f1

作成するすべての関数が同じタイプa -> aである場合、foldr関数を作成する関数のリストに適用できます。たとえば、次の関数があるとします。

floorSqrt :: Float -> Float
floorSqrt = (fromIntegral . floor) . sqrt

fromIntegralfloorを返さないため、Floatを挿入する必要があることに注意してください)。これは次のように書くことができます

floorSqrt = foldr (.) id [fromIntegral . floor, sqrt]

ここで、foldrはdietbuddhaの回答のreduceに対応し、.composeに対応します。 id関数(ID)は、折りたたみ/縮小の開始点として必要です(これは、JavaScriptの例では暗黙的であると思います)。

上記のパターンは、関数のシーケンスf1, ..., fnに使用できます。ここで、i∈{1、...、n}、f :a-> a:

compositeFunction = foldr (.) id [fn, ..., f1]

Haskellでは、この最後のパターンを元の例に使用することはできません。これは、foldrがリスト内のすべての関数の型を同じにする必要があるためです。この制限がないため、JavaScriptで使用できます(dietbuddhaの回答を参照)。

5
Giorgio

たとえばJavaScriptで、標準の数学および文字列メソッドを使用する場合は、あまり多くのメソッドをネストしません。しかし、いわゆる fluent interface を持つライブラリーがあります。これらの種類のAPIは、メソッドチェーンによって読み取り可能なコードを作成するために特に作成されています。あなたがそれらのキーワードのためにグーグルするとき、あなたはたくさんの記事を見つけるでしょう。

4
Doc Brown

あなたが説明するような言語が存在し、最も人気がある(人気のある用語は相対的な用語です) [〜#〜] forth [〜#〜]

あなたの例は、FORTHでは次のようになります。

: floorSQRT  ( NUM -- NUM )
    Sqrt
    Floor
    ToString ;

(FORTHは非常にレベルが低いため、現在のところ、文字列型は考えられていません)。

JavaScriptでこのようなことを本当に実行したい場合は、次のように、配列を最初のパラメーターとして取るこれらのメソッドの独自のバージョンを作成することでできます。

function Sqrt(stack) { stack.Push(Math.sqrt(stack.pop())) };
function Floor(stack) { stack.Push(Math.floor(stack.pop())) };
function MakeString(stack) { stack.Push(String(stack.pop())) };

function floorSqrt(stack) {
    Sqrt(stack);
    Math.floor(stack);
    MakeString(stack);
}

stack.Push(num);
floorSqrt(stack);
num = stack.pop();

これは機能しますが、ほとんどのJavaScriptプログラマーを混乱させるため、絶対に実行しないでください。

昔はFORTHで作業していたので、スタックベースの言語のアイデアは非常に魅力的ですが、実際には、妥当なサイズのコードの場合、すぐに混乱します。すべてが暗黙的であるため、各メソッドが実際に何を操作するのかを思い出すことは困難です。実際には、元のコードよりもはるかに混乱します。正直なところ、私がこのコードを書いていて、それを明確にしたいのであれば、次のようになります。

function floorSqrt(num) {
    num = Math.sqrt(num);
    num = Math.floor(num);

    return String(num);
}

私はそれが本当に混乱しているとは全く思いません。それは非常に明確です。

連鎖を本当にしたい場合は、次のように明確にすることができます。

function floorSqrt(num) {
    return String(
               Math.floor(
                   Math.sqrt(num)
           ));
}

しかし、繰り返しますが、それは本当に必要だとは思いません。オリジナルは完全に素晴らしく、簡単だと思います。

3
Gort the Robot

Doc Brownが言うように、これはfluentインターフェースであり、あなたの例を使用するためにJavaScriptで簡単に達成できます:

Number.prototype.sqrt=function(){return Math.sqrt(this);};
Number.prototype.floor=function(){return Math.floor(this);};
String.prototype.alert=function(){alert(this);return this;}

(5).sqrt().floor().toString().alert();  //alerts "2"
2
Tom