つまり、読者は少なくともモナドまではHaskellを知っています。
Monadは、 MyMonad SomeTypeのようなものを返す関数から(非公式に言えば)モナドと対話する特定の関数を実行できるようにする型クラスです。 モナドを使用する関数は、特別な関数(>>) 、 (>> =) 、 (= <<)によってリンクする必要があります。
したがって、 Comonadは、 関数がcomonadの効果と対話できるようにする型クラスでもあり、これらの関数についてのみ、comonadは戻り値の型ではなく引数で示されます。 各引数で同じまたは異なるコマンドを示し、戻り値でモナドを示すことを妨げるものは何もありません。
単項式を結合するために、特別な関数も使用されます- (= >>) 、 (<< =) 、 (=> =) 、 (= <=) 、独自のニュアンス。
一般に、モナドとコモナの間には類似性があり、それらのアプリケーションを組み合わせることができます。
わかりやすくするために、実用的な例を解き、ヒストグラムを計算して表示するためのミニライブラリを作成します。
タスクはそれほど大規模ではありませんが、複数の部分に分割します。
- メイン部分(ヒストグラムエンジン)、つまり 計算;
- ヒストグラムデータの保存と蓄積の実装。
- ヒストグラム表示(端末出力)。
バーグラフエンジン
そして、計算。 モナドもコモナドもありませんが、重要です。 予備計算の結果、タイプの関数
type CalcPos x = x -> Maybe Int
指定された初期値に対して、ユニットを追加するヒストグラムポケットの数を決定します(ヒストグラムの計算方法、および実際にナビゲートするのはとても簡単です)。 元の数値が指定範囲外の場合、関数はNothingを返します。
指定されたソースデータの予備計算の結果全体が構造に保存されます
data HistoEngine x = HistoEngine { low :: x
レコードの残りのフィールドはcalcPos関数を必要としません。クロージャーを介して作成するときに必要なすべてのデータを受け取りますが、他の場合は残りのフィールドが必要になる場合があります。
mkHistoEngine関数は、ヒストグラムの開始点です。 初期データについては、分析範囲の下限と上限、およびヒストグラムポケットの数を使用します。 mkHistoEngine関数は、ラムダで指定されたcalcPosを含むHistoEngineを埋めて返します。
mkHistoEngine :: (Show x, Ord x, RealFrac x) => x -> x -> Int -> HistoEngine x
(関数の定義は簡単であり、以下に完全なコードで示します)。
カプセル化の原則に基づきます(たとえば、 Gabriel Gonzalezは、 コモナはオブジェクトであると考えているため、OOPの原則の1つを侵害しません)。 上記のすべてのコードを個別のモジュールに取り込み 、内部コンテンツなしでHistoEngineへの外部アクセスを提供することができます。 ただし、その後、いくつかのHistoEngineフィールドの値を取得する関数を提供する必要があります。
histoEngPosFn:: HistoEngine x -> CalcPos x histoEngPosFn = calcPos histoEngSize:: HistoEngine x -> Int histoEngSize = size
将来的には、ヒストグラムスケールを表示する必要があります。 いくつかのフィールドにアクセス関数を作成する代わりに、スケール値をすぐに生成することをお勧めします。
gistoXList:: (RealFrac x, Enum x) => HistoEngine x -> [x] gistoXList HistoEngine{..} = take size [low+ step*0.5,(low + step*1.5)..]
(この{..}にはRecordWildCards拡張機能を使用しました)。
しかし、コモナはどこにありますか? 彼らに移る時が来ました。
コマンドとしての棒グラフ
この例のコマンドは、計算、保存、表示間のリンクとして機能します。 私は彼女のデータのタイプをこのようにしました
data Histogram xa = Histogram (HistoEngine x -> a) (HistoEngine x) deriving Functor
すべてのタイプがコモネードの作成に適しているわけではありません。 必ずしも上記と同じではありません:任意の型へのこの値の値と機能。 ただし、この型では、再帰を実行できる必要があります。 この場合、関数HistoEngine x-> aを再帰的に呼び出すことができます。 他の場合、再帰的なデータ型(リスト、ツリー)を使用します。
そして、 Histogram typeをcomonadにします
instance Comonad (Histogram x) where extract (Histogram fx) = fx duplicate (Histogram fx) = Histogram (Histogram f) x
以上です。
関数extract :: wa-> a (ここでwはコマンド、文字w 、コマンドの受け入れられる表記は逆さまにmです )は戻りモナドに対応しますが、逆を行い、コマンドから値を返します。 いくつかの記事では、 コアターンと呼ばれていました (まだ誰もそれをナーターと呼ぶつもりはないようです)。
この関数は後で直接使用されます。 重複:: :: wa-> w(wa)関数とは異なり、これはjoin ::(Monad m)=> m(ma)-> maであり 、モナドの場合にのみ、反対の意味があります。 結合により、モナドの1つのレイヤーが削除され、 複製によりモナドのレイヤーが複製されます。 直接呼び出すことはそれ以上発生しませんが、 重複はcomonadパッケージの他の関数内、特にextend : :(wa- > b)-> wa- > wbで呼び出されます。 実際には、コマンドは、 extract 、 duplicate (およびFunctor )、およびextract 、 extendのいずれかより便利な方で定義できます。
そして、コマンド自体は準備ができています。 次のステップに進みます。
保管するもの
累積値(ヒストグラムのセルまたはポケット)を不安定なベクトルに保存します。 つなぐ
import qualified Data.Vector.Unboxed as VU import qualified Data.Vector.Unboxed.Mutable as VUM
そして書く
type HistoCounter = VU.Vector Int type Histo x = Histogram x HistoCounter
空のセルで元のヒストグラムコマンドを作成する関数を作成します。
mkHisto:: (Show x, Ord x, RealFrac x) => x -> x -> Int -> Histo x mkHisto lo hi sz = Histogram (\e -> VU.replicate (histoEngSize e) 0) $ mkHistoEngine lo hi sz
すべてがシンプルです。 ゼロで満たされた特定のサイズのベクトルを選択し 、以前に作成されたmkHistoEngineを使用してHistoEngineを作成するラムダ関数について説明します。 MkHistoパラメーターは、単にmkHistoEngineに渡されます 。
次に、ヒストグラムにサンプルを蓄積する関数を書きます。
histoAdd:: x -> Histo x -> HistoCounter histoAdd xh@(Histogram _ e) = case histoEngPosFn ex of Just i -> VU.modify (\v -> VUM.modify v (+1) i) (extract h) Nothing -> extract h
モナド関数の場合、モナドは関数の結果の型で示され、モナド関数の場合は引数に含まれることを思い出してください。 histoEngPosFnを使用して、 mkHistoEngineで作成されたCalcPos型の関数を取得します。 その結果に応じて、ベクトルの対応するセルを1つ増やすか、ベクトルを変更せずに返します。 ベクトル自体は、式extract hによって取得されます。
ヒストグラム表示アルゴリズムをそのストレージと蓄積にバインドしないために、表示のためにリストで表されるべきであると仮定します。 これは、リストの表示回数が少なく、常に連続して処理されるため、許容されます。 (蓄積中のヒストグラム保存の方法は、たとえば、より効率的な可変ベクトルに変更できますが、これはヒストグラム表示機能の変更につながるべきではありません)。
そして、ヒストグラムデータの表示をベクトルからリストに変換する共通の機能。 彼女はまた些細です。
histoToList:: Histo x -> [Int] histoToList = VU.toList . extract
端末に結果を表示する
バーが横にある場合、ヒストグラム出力を水平に使用しました。
関数の定義は、完全なコードで以下に示されています。 関数は(もちろん引数の)コモナと(出力値の)モナドの両方を使用することに注意してください。
histoHorizPrint:: (RealFloat x, Enum x) => Int -> Int -> Histogram x [Int] -> IO ()
引数は、出力フィールドの幅、元の値のスケール値を表示するフィールドの幅、そしてもちろんコマンドです。 VU.Vector Intの代わりに[Int]の形式のデータを使用するようになりました。
すべてをまとめる
テストデータとして、単純に一連の正弦波サンプルを形成します
map sin [0,0.01 .. 314.15]
通常のリストfoldlにヒストグラムを蓄積します。
foldl f (mkHisto (-1::Double) 1 20) (map sin [0,0.01 .. 314.16])
(ヒストグラムの初期パラメーターは、-1〜+1、20ポケットの範囲です)。
しかし、関数fは、 foldlの場合、 (b-> a- > b)という形式になります。この場合、 bはコマンドで、次の累積カウントです。 複雑なことは何もありません。 結局、私たちはすでにhistoAdd関数を作成しました。 モナドと同様に、矢印に似たヘルパー関数が必要です。
そして、 メイン関数でのテスト全体。
main :: IO () main = histoHorizPrint 65 5 $ histoToList <<= foldl (\ba -> b =>> histoAdd a) (mkHisto (-1::Double) 1 20) (map sin [0,0.01 .. 314.16])
完全なコード(comonadおよびベクターパッケージが必要になります) {-# LANGUAGE DeriveFunctor, RecordWildCards #-} import Control.Comonad import qualified Data.Vector.Unboxed as VU import qualified Data.Vector.Unboxed.Mutable as VUM import Numeric type CalcPos x = x -> Maybe Int data HistoEngine x = HistoEngine { low :: x
チームトランスフォーマー
モナドと同様に、コモナ用のトランスフォーマーがあります(ただし、それらのトランスフォーマーは非常に少数ですが、だれが独自のものを書くことを妨げますか)。 つまり、コモナはネストできます。
空のヒストグラムを作成したばかりの最初に数字を表示するためのフィールド幅を設定するとします。
別のモジュールを接続する
import Control.Comonad.Env
対応するモナドReaderの意味内でモナドを実装します。
main関数の例は次のようになります
(\h-> histoHorizPrint 65 (ask h) (lower h)) $ (histoToList . lower) <<= foldl (\ba -> b =>> (histoAdd a . lower)) (EnvT 5 (mkHisto (-1::Double) 1 20)) (map sin [0,0.01 .. 314.16])
(EnvT 5(...))を使用して、別のcomonadレイヤーを作成し、その中に値5(数値フィールドに必要な幅)を格納します。 現在、comonad式は2層であるため、 Histogram comonadを使用して関数を使用するには、モナドトランスフォーマーが前の層に移動するためにliftを使用するように、補助関数lowerが必要です。
また、値の抽出にも注意してください - (h) なぜリーダーではありませんか!
最後の例は、 runEnvT関数を使用してペアの2つのコマンドレイヤー(値、内部コマンド)の最後で解析し、その名前がReaderTで使用されるrunReaderTを大胆に示唆する、異なる方法で記述することができます。
(\(nw,h)-> histoHorizPrint 65 nw h) $ runEnvT $ (histoToList . lower) <<= foldl (\ba -> b =>> (histoAdd a . lower)) (EnvT 5 (mkHisto (-1::Double) 1 20)) (map sin [0,0.01 .. 314.16])
チームビルディングで頑張ってください!