カテゴリーClaysley

ロギング構成



型と純粋な関数のカテゴリを作成する方法を見ました。 また、カテゴリー理論の枠組みの中で、副作用または汚れた関数をモデル化する方法があることにも触れました。 例を見てみましょう:実行の進行状況を記録または記録する関数。



命令型言語の何かは、いくつかのグローバルな状態の突然変異によって実現される可能性があります。例えば:



string logger; bool negate(bool b) { logger += "Not so! "; return !b; }
      
      





結果をキャッシュするバージョンではログに書き込むことができないため、これは純粋な関数ではないことを知っています。 この機能には副作用があります。



現代のプログラミングでは、可能な限りグローバルな可変状態から離れようとします-少なくとも並行性の問題のため。 また、ライブラリに同様のコードを記述することはありません。



幸いなことに、この機能をクリーンにすることができます。 関数との間で明示的な形式でログを渡すだけです。 これを行うには、文字列引数を追加し、メイン結果と更新されたログを含む文字列からペアを返します。



 pair<bool, string> negate(bool b, string logger) { return make_pair(!b, logger + "Not so! "); }
      
      





この関数は純粋で、副作用はなく、同じ引数で呼び出されるたびに同じペアを返し、必要に応じて結果をキャッシュできます。 ただし、ログの累積的な性質を考えると、この課題につながる可能性のあるすべてのストーリーをキャッシュする必要があります。 以下については、個別のキャッシュエントリがあります。



 negate(true, "It was the best of times. ");
      
      





そして

 negate(true, "It was the worst of times. ");
      
      





などなど。



これは、ライブラリ関数の適切なインターフェイスでもありません。 ユーザーは戻り値の文字列を自由に無視できるため、追加の複雑さはほとんどありませんが、入力として文字列を渡すことを余儀なくされ、不便になる可能性があります。



同じことをより邪魔にしない方法はありますか? 問題を共有する方法はありますか? この単純な例では、否定関数の主な目的は、あるブール値を別のブール値に変えることです。 ロギングはセカンダリです。 もちろん、ログに記録されるメッセージは機能固有ですが、メッセージを1つの連続したログに集約することは別のタスクです。 関数が文字列を返すようにしますが、ログを作成するタスクから解放したいと思います。 妥協案は次のとおりです。



 pair<bool, string> negate(bool b) { return make_pair(!b, "Not so! "); }
      
      





これは、関数呼び出し間でログが集約されるという考え方です。



これがどのように行われるかを見るために、もう少し現実的な例に移りましょう。 文字列から文字列への1つの関数があり、小文字を大文字に変換します。



 string toUpper(string s) { string result; int (*toupperp)(int) = &toupper; // toupper is overloaded transform(begin(s), end(s), back_inserter(result), toupperp); return result; }
      
      





もう1つは、文字列を文字列ベクトルに分割し、スペースで分割します。



 vector<string> toWords(string s) { return words(s); }
      
      





主な作業は、補助機能ワードで行われます。



 vector<string> words(string s) { vector<string> result{""}; for (auto i = begin(s); i != end(s); ++i) { if (isspace(*i)) result.push_back(""); else result.back() += *i; } return result; }
      
      





toUpperおよびtoWords関数を変更して、通常の戻り値の上にメッセージ行をキャッチするようにします。



画像



これらの関数の戻り値を充実させます。 一般的な方法で、ペアをカプセル化するWriterテンプレートを定義して、これを行いましょう。最初のコンポーネントは任意のタイプAの値で、2番目のコンポーネントは文字列です。



 template<class A> using Writer = pair<A, string>;
      
      





豊富な機能は次のとおりです。



 Writer<string> toUpper(string s) { string result; int (*toupperp)(int) = &toupper; transform(begin(s), end(s), back_inserter(result), toupperp); return make_pair(result, "toUpper "); } Writer<vector<string>> toWords(string s) { return make_pair(words(s), "toWords "); }
      
      





これらの関数を別の強化された関数に結合します。これらの関数は、これらのアクションをログに記録しながら、文字列を大文字にして単語に分割します。 方法は次のとおりです。



 Writer<vector<string>> process(string s) { auto p1 = toUpper(s); auto p2 = toWords(p1.first); return make_pair(p2.first, p1.second + p2.second); }
      
      





私たちは目標を達成しました:雑誌の統合はもはや個々の機能の関心事ではありません。 それらは独自のメッセージを生成し、外部からログに収集されます。



次に、このスタイルで書かれたプログラム全体を想像してください。 これは、反復的でエラーが発生しやすいコードの悪夢です。 しかし、私たちはプログラマです。 重複したコードを処理する方法を知っています:それを抽象化します! ただし、これらは通常の抽象化ではありません。関数の構成そのものを抽象化する必要があります。 しかし、構成はカテゴリー理論の本質なので、コードをさらに記述する前に、カテゴリーの観点からこの問題を分析しましょう。



作家カテゴリー



いくつかの追加機能をフックするために関数の戻り値の型を充実させるというアイデアは非常に便利です。 このような例がさらに多く表示されます。 開始点は、型と関数の通常のカテゴリです。 型をオブジェクトとして残しますが、モーフィズムを充実した関数として再定義します。



たとえば、intをboolに変換するisEven関数を充実させたいとします。 これを射影変換に変換します。これは、強化された関数で表されます。 重要なことは、装飾された関数はペアを返しますが、この射はintオブジェクトとboolオブジェクトの間の矢印と見なされることです。



 pair<bool, string> isEven(int n) { return make_pair(n % 2 == 0, "isEven "); }
      
      





このカテゴリの法則によれば、このモルフィズムを、boolから何にでもなる別のモルフィズムと組み合わせることができなければなりません。 特に、それを以前の否定関数と組み合わせることができるはずです:



 pair<bool, string> negate(bool b) { return make_pair(!b, "Not so! "); }
      
      





明らかに、入力/出力の不一致のために、通常の関数を構成するのと同じ方法でこれらの2つの射を構成することはできません。 構成は次のようになります。



 pair<bool, string> isOdd(int n) { pair<bool, string> p1 = isEven(n); pair<bool, string> p2 = negate(p1.first); return make_pair(p2.first, p1.second + p2.second); }
      
      





それで、ここに、私たちが構築しているこの新しいカテゴリに2つの射を作成するためのレシピがあります:

  1. 最初の射に対応する強化された関数を実行する
  2. 結果のペアの最初のコンポーネントを抽出し、2番目の射に対応する強化された関数に渡します
  3. 最初の結果の2番目のコンポーネント(行)と2番目の結果の2番目のコンポーネント(行)を接続します
  4. 最終結果の最初のコンポーネントと連結された文字列を組み合わせた新しいペアを返します。


この構成をC ++の高階関数として抽象化する場合は、カテゴリ内の3つのオブジェクトに対応する3つの型でパラメーター化されたテンプレートを使用する必要があります。 ルールに従って構成可能な2つの強化された関数を使用し、3番目の強化された関数を返す必要があります。



 template<class A, class B, class C> function<Writer<C>(A)> compose(function<Writer<B>(A)> m1, function<Writer<C>(B)> m2) { return [m1, m2](A x) { auto p1 = m1(x); auto p2 = m2(p1.first); return make_pair(p2.first, p1.second + p2.second); }; }
      
      





例に戻り、このテンプレートを使用して構成toUpperおよびtoWordsを実装できます。



 Writer<vector<string>> process(string s) { return compose<string, string, vector<string>>(toUpper, toWords)(s); }
      
      





作成テンプレートに型を渡すことには、まだ多くのノイズがあります。 現時点では、戻り値の型の推測による一般化されたラムダ関数をサポートするC ++ 14互換コンパイラを使用している場合は、これを回避できます(コードについてはEric Niblerに感謝します)。



 auto const compose = [](auto m1, auto m2) { return [m1, m2](auto x) { auto p1 = m1(x); auto p2 = m2(p1.first); return make_pair(p2.first, p1.second + p2.second); }; };
      
      





この新しい定義では、プロセスの実装が簡略化されています。



 Writer<vector<string>> process(string s){ return compose(toUpper, toWords)(s); }
      
      





しかし、まだ完了していません。 新しいカテゴリで構成を定義しましたが、単一の射とは何ですか? これらは通常のアイデンティティ関数ではありません! これらは、タイプAからタイプAへの射である必要があります。これは、次の形式の強化された関数であることを意味します。



 Writer<A> identity(A);
      
      





それらは、構成に関してユニットとして振る舞うべきです。 合成の定義を見ると、同一のモルフィズムは引数を変更せずに渡すが、ログに空の行を追加するだけであることがわかります。



 template<class A> Writer<A> identity(A x) { return make_pair(x, ""); }
      
      





確認したカテゴリが本当に正当なカテゴリであることを簡単に確認できます。 特に、私たちの構成は自明な連想です。 各ペアの最初のコンポーネントに何が起こるかをトレースすると、これは連想的な機能の単なる通常の構成であることがわかります。 2番目のコンポーネントは結合され、連結も結合的です。



鋭い読者は、この構造を文字列だけでなく、モノイドに簡単に一般化できることに気付くかもしれません。 compose内でmappendを使用し、(+および ""の代わりに)内部でmemptyを使用したいと思います。 実際、文字列のみを記録するように制限する理由はありません。 優れたライブラリライタは、ライブラリが機能するために必要な制限の最小値を決定できるはずです。ロギングライブラリの唯一の要件は、ログにモノイダルプロパティがあることです。



ハスケルライター



Haskellでの同じ構築は、はるかに簡潔になり、コンパイラーは私たちにもっと役立つでしょう。 Writer型を定義することから始めましょう。



 type Writer a = (a, String)
      
      





ここでは、C ++でtypedef(またはusing)と同等の型エイリアスを定義するだけです。 Writer型は、型変数によってパラメーター化され、aとaのペアと同等です。 ペアの構文は最小限です。カンマで区切られた括弧内の2つの名前のみです。



私たちの射は、任意の型から特定の型のWriterへの関数です:



 a -> Writer b
      
      





構成は、「魚」とも呼ばれる楽しい中置演算子として宣言します。

 (>=>) :: (a -> Writer b) -> (b -> Writer c) -> (a -> Writer c)
      
      





これは2つの引数の関数であり、それぞれがそれ自体が関数であり、関数を返します。 最初の引数は、タイプ(a->ライターb)、2番目(b->ライターc)、および結果(a->ライターc)です。



この中置演算子の定義は次のとおりです。2つの引数m1およびm2は、魚のシンボルの両側に表示されます。



 m1 >=> m2 = \x -> let (y, s1) = m1 x (z, s2) = m2 y in (z, s1 ++ s2)
      
      





結果は、単一の引数xのラムダ関数です。 ラムダはバックスラッシュとして書かれています-足を切断したギリシャ文字λと考えることができます。



letという単語を使用すると、ヘルパー変数を宣言できます。 ここで、m1を呼び出した結果は1組の変数(y、s1)と一致し、最初のパターンから引数yを付けてm2を呼び出した結果は(z、s2)と一致します。



Haskellでは、C ++で行ったように、ペアマッチングはゲッターを使用する一般的な代替手段です。 それ以外に、2つの実装の間にはかなり単純な対応があります。



式のlet値はin:の後に含まれます。ここは、最初のコンポーネントがzで、2番目のコンポーネントが2行の和集合s1 ++ s2であるペアです。



また、カテゴリに対して単一のモルフィズムを定義しますが、後で明らかになる理由から、それをreturnと呼びます。



 return :: a -> Writer a return x = (x, "")
      
      





完全を期すために、upCase関数(翻訳者のメモ:私はC ++の例からtoUpperを意味しましたが、その名前の関数はすでに標準のPreludeモジュールにあります)とtoWordsで充実したHaskellバージョンを書きましょう:



 upCase :: String -> Writer String upCase s = (map toUpper s, "upCase ") toWords :: String -> Writer [String] toWords s = (words s, "toWords ")
      
      





マップ関数は、C ++の変換関数に対応しています。 toUpper文字の関数をstringに適用します。 補助機能語は、Prelude標準ライブラリで定義されています。



最後に、これらの2つの関数の構成は、魚演算子を使用して構築されます。



 process :: String -> Writer [String] process = upCase >=> toWords
      
      





カテゴリーClaysley



おそらく、私はこのカテゴリをその場で思いついたのではないと推測したでしょう。 これは、いわゆるKleisleyカテゴリー(モナドベースのカテゴリー)の例です。 モナドについて議論する準備はまだできていませんが、モナドが何ができるかを示したかったのです。 限られた目的のために、Claysleyカテゴリーにはオブジェクトのようなタイプがあります。 タイプAからタイプBへの形態素は、特別な強化によりBから取得されたAからタイプへの関数です。 各Kleisleyカテゴリは、このような射の独自の合成方法と、この合成に関する同一の射を定義します。 (後で、「濃縮」という不正確な用語は、カテゴリーの内機能の概念に対応することがわかります。)



この投稿でカテゴリの基礎として使用した特定のモナドはライターと呼ばれ、関数の実行を記録または追跡するために使用されます。 また、純粋なコンピューティングに効果を埋め込むためのより一般的なメカニズムの例でもあります。 先ほど、セットのカテゴリ(通常、ボトムなし)でプログラミング言語と関数のタイプをモデル化できることを見ました。 ここで、このモデルをわずかに異なるカテゴリに拡張しました。このカテゴリでは、モーフィズムは豊富な関数で表され、その合成は、ある関数の結果を別の関数の入力に渡すだけではありません。 さらに1つの自由度があります。構成自体を変更できます。 副作用を使用して命令型言語で伝統的に実装されているプログラムに単純な表示意味論を与えることができるのは、まさにこの自由度であることがわかります。



プログラマーのカテゴリー理論:序文

カテゴリ:構成の本質

種類と機能

大小のカテゴリ

カテゴリーClaysley



All Articles