web-dev-qa-db-ja.com

サービスパターンを多用することで、メリットを失うことなく純粋関数を置き換えることができますか?

関数型プログラミングの純粋関数には大きな利点がありますが、サービスパターンを多用する命令型プログラミングでも同じ利点を得ることができますか?

スタック空間または不滅のヒープ空間のみを使用することに焦点を当てた、純粋な関数型プログラミングから命令型プログラミングにアイデアをマッピングする方法を見つけたいので、私は尋ねます。 (つまり、ハードリアルタイムに焦点を当てる)

私が話すサービスパターンはここからと同じです: https://www.schoolofhaskell.com/user/meiersi/the-service-pattern

そして、頻繁に使用するということは、関数に、引数で渡された提供されたエフェクトフル関数によってのみエフェクトを実行させることを意味します。

例(C++):

_template <typename SetPixel>
void mandelbrot(float x1, float y1, float stepX, float stepY, int resX, int resY, SetPixel setPixel) {
  float y = y1;
  for (int i = 0; i < resY; ++i, y += stepY) {
    float x = x1;
    for (int j = 0; j < resX; ++j, x += stepX) {
      float za = 0;
      float zb = 0;
      float zm = 0;
      float ca = x;
      float cb = y;
      int k = 0;
      while (zm < 4.0f) {
        float za2 = za * za - zb * zb + ca;
        float zb2 = 2 * za * zb + cb;
        float zm2 = za2 * za2 + zb2 * zb2;
        ++k;
        if (k > 255) {
          k = 0;
          break;
        }
        za = za2;
        zb = zb2;
        zm = zm2;
      }
      setPixel(j, i, k);
    }
  }
}
_

上記のメソッドmandelbrotは、純粋な方法と不純な方法の両方で使用できます。結果を収集して不変の配列を生成するSetPixelを提供できます(効果を完全にローカルにし、純粋な計算にします)。または、エフェクトフルSetPixelを渡して、エフェクトを直接実行し、不純な計算にすることもできます。

これにより、関数を純粋な方法でデバッグしたり、実行時に次のように直接エフェクトを実行したりできます。

_  mandelbrot(
    -2.0f,
    -2.0f * 480.0 / 640.0,
    4.0f / 640.0,
    4.0f / 640.0,
    640,
    480,
    [renderer](int x, int y, int c) {
      SDL_SetRenderDrawColor(renderer, (c*5) & 0xFF, 0, 0, 255);
      SDL_RenderDrawPoint(renderer, x, y);
    }
  );
_

このアプローチをとることによって、純粋な関数の利点を失ったことがありますか?

2回目の試行:

以下のコメントとDerekElkinsの回答が示唆しているように、回答は「いいえ」です。純粋関数の利点を失いました。

だからここに2回目の試みがあります。 C++移動セマンティクス+ unique_ptrを使用して線形型システムを実現し、それを使用して純粋な状態操作スタイルでエフェクトシステムを実装します。これは、MercuryがIOを行う方法に似ています。

_struct DoNotDelete {
  void operator()(void *ptr) {}
};

struct Void {};

typedef unique_ptr<Void,DoNotDelete> RealWorld;

template <typename SetPixel, typename S>
S mandelbrot(S state0, SetPixel setPixel, float x1, float y1, float stepX, float stepY, int resX, int resY) {
  S state = move(state0);
  float y = y1;
  for (int i = 0; i < resY; ++i, y += stepY) {
    float x = x1;
    for (int j = 0; j < resX; ++j, x += stepX) {
      float za = 0;
      float zb = 0;
      float zm = 0;
      float ca = x;
      float cb = y;
      int k = 0;
      while (zm < 4.0f) {
        float za2 = za * za - zb * zb + ca;
        float zb2 = 2 * za * zb + cb;
        float zm2 = za2 * za2 + zb2 * zb2;
        ++k;
        if (k > 255) {
          k = 0;
          break;
        }
        za = za2;
        zb = zb2;
        zm = zm2;
      }
      state = move(setPixel(move(state), j, i, k));
    }
  }
  return state;
}

template <typename SetPixel>
RealWorld interpreter(RealWorld realWorld, SetPixel setPixel) {
  return move(mandelbrot(
    move(realWorld),
    setPixel,
    -2.0f,
    -2.0f * 480.0 / 640.0,
    4.0f / 640.0,
    4.0f / 640.0,
    640,
    480
  ));
}
_

次に、それを開始するために、RealWorldmain()のどこかに構築されます。

_  Void realWorldState;
  RealWorld realWorld = unique_ptr<Void,DoNotDelete>(&realWorldState);

  RealWorld realWorld2 = interpreter(
    move(realWorld),
    [renderer](RealWorld realWorld, int x, int y, int c) {
      SDL_SetRenderDrawColor(renderer, (c*5) & 0xFF, 0, 0, 255);
      SDL_RenderDrawPoint(renderer, x, y);
      return realWorld;
    }
  );
_

これにより、純粋な不変の値(ピクセル位置から色への不変のツリーマップなど)または実世界自体を使用できます。

4
clinux

参照記事で言及されているように、これはHaskellの基本的なOOPの模倣に非常に近いですが、言及されているように、ファーストクラスの構成を提供します。 OOPで実践されています。いずれにせよ、何が起こっているのかを考えてモデル化する1つの方法は、 object-capabilities の概念を使用することです。

オブジェクト機能言語では、何かを行う権限は機能の概念によって取得されます。通常、これらは特定のI/O関連のものですが、より抽象的な場合もあります。重要なアイデアは、あなたが能力を得ることができる唯一の方法は誰かがあなたにそれを与えるかどうかであるということです。そのプロパティを強制する言語を作成するには、ほとんどのOO言語で慣れているようなカプセル化と、すべての環境権限の削除大まかに言えば、これはJavaのような言語で、権限を持つ「トップレベル」のクラス/関数がなく、グローバルな可変変数、たとえばクラスの可変静的(Java/C#の意味で)プロパティがないことを意味します。 。ほとんどのOO言語は(C++ではありませんが)必要なカプセル化を提供しますが、周囲の権限を排除するものはほとんどありません。それを削除すると、依存関係の注入のようなスタイルが強制されます。これは言語で非常に明確に示されています- ニュースピーク

あなたが言及する利点は、オブジェクト機能言語と依存性注入の一般的な利点です。実際、オブジェクト機能言語では、権限を持つオブジェクトをラップまたはモックする機能は、サンドボックス化および取り消し可能な機能の基礎です。とは言っても、これはあなたの質問に関しては少し不平等でした。

あなたの質問に対する答えは基本的に「いいえ」です。あなたが言及する利点は、純粋に関数型コードの利点ではなく、依存関係を明示的にすることの利点です。 命令型推論は難しい であるため、命令型プログラミングに関する純粋な関数型プログラミングの利点が得られます。結局のところ、コードが「純粋」(一種)であるにもかかわらず、mandlebrotを使用してコードの正確性について推論するには、依然として命令型の推論が必要になります。 (当然のことながら、mandelbrotの実装は必須であるため、命令の推論を使用してその正当性を検証する必要があります。)

たとえば、HaskellのIOモナドのようなアプローチを採用した場合、「何も返さない」というコメントに対処し、何をすべきかについての(一般的な)表現を返しますが、それでも大部分または完全にプログラムが正しいことを確認するために命令型の推論手法を採用する必要があります。これが、HaskellのIOモナドでのプログラミングが、典型的な言語で命令型プログラミングを行うよりもはるかに簡単ではない理由です。一方、Haskellで一般的に使用されている手法である、実行すべきことのより単純でより専門的な表現を返す場合は、純粋に関数型コードにのみ適用される推論手法をはるかに強力に活用できます。たとえば、更新するピクセルの配列を返すことができます。この手法は、権限を必要とするすべての作業を「インタープリター」機能にプッシュすることにより、オブジェクト機能/依存性注入アプローチと同じ利点のいくつかを生み出すことになります。ただし、そのインタープリター機能を実装することで、オブジェクト機能の手法から大きなメリットを得ることができます。

一般に、さまざまな機関には機能を介してのみアクセスできるように見せかけることを強くお勧めします(実際にはそうではない場合)。オブジェクト機能システムで作業しているという前提が成り立つ限り、柔軟なコードを使用しながら大幅な簡素化を実現できます。