CSVパーサーの例を使用してScalaでモナドを記述します

最近、モナドについて多くのことを学びました。 私たちはすでにそれが何であるかを理解し、 それらをどのように描くかさえ知っています 、彼らの目的を説明するレポートを見ました。 それで、私は発信モナドトレインに立ち寄り、最終的に主流になるまでこのトピックについて書くことにしました。 しかし、私はやや異なる側面から来ます:カテゴリー理論からの計算はありません、 最高の言語での挿入はありません、そしてscalaz / shapelessとparser-combinatorsライブラリさえありません。 ご存知のように、何かがどのように機能するかを理解する最良の方法は、自分でそれを行うことです。 今日はモナドを書きます。


画像


挑戦する


たとえば、ありふれたタスク:CSVファイルの解析を見てみましょう。 caseクラスのファイルの行を解析し、それらをデータベースに送信し、json / protobufなどでシリアル化する必要があるとします。 エスケープと引用符は忘れてください。さらに簡単にするために、フィールドに区切り文字が見つからないと考えています。 誰かがこのソリューションをプロジェクトにドラッグすることに決めた場合、この機能をねじることは難しくないと思います。



次のCSVファイルがあるとします。


1997;Ford;E350;ac, abs, moon;3000.00 1996; Jeep; Grand Cherokee; MUST SELL! air, moon roof, loaded; 4799.00 1999;Chevy;Venture "Extended Edition"; ; 4900.00 

これを次のタイプのオブジェクトのセットにデシリアライズする必要があります。


 case class Car(year: Int, mark: String, model: String, comment: String, price: BigDecimal) 

明らかなアプローチ


比較するものを得るために、モナドの使用をより見やすく、より楽しく、より信頼できるようにする人生の例を挙げなければなりません。


ファイルが既に変数contentロードされているとしましょう:


 val lines = content.split('\n') val entities = lines.map { line => line.split(';').map(_.trim) match { case Array(year, mark, model, comment, price) => Car(year.toInt, mark, model, comment, BigDecimal(price)) } }.toSeq 

アプローチの短所:



長所:



モナドパーサー


問題を別の視点から見ることをお勧めします。



画像


コードに戻ると、各ステージのハンドラーには次のような宣言があります。


 def parse[T, Src]: Src => (T, Src) 

モナド自体について少し説明します。


一言で言えば、モナドは、値と何らかのコンテキストを含むコンテナとして説明できます。
構文的には、Scalaの場合、これはモナドにflatMapメソッドが必要であり、一般に次のように宣言されることを意味します。


 def flatMap[T](f: T => M[T]): M[T] 

fがコンテナに保存されている値である場合、コンテキストは何ですか? そして、ここにあります:fには1つの引数しかありませんが、1つのflatMapの内側から別のflatMapを呼び出すことができるので、内側のflatMapから、外側の内側で宣言されたすべての値、つまり前のすべての単語にアクセスできます。


画像


モナドからmapメソッドを実装する必要はありませんが、それを定義することに注意してください。既に定義されているパーサーから変更されたパーサーを作成するのに役立ちます。


また、モナドで正味額をラッピングする操作を定義する必要があります。 これはクラスメソッドではありませんが、コンストラクター呼び出し、またはコンパニオンオブジェクトの適用メソッドである可能性があります。厳密な要件はありません。便宜上、適用メソッドを定義することをお勧めします。


上記で定義した種類の解析関数を含むモナドを実装し、それを使用して異なるパーサーを結合する方法を確認します。


そのため、特定のタイプのフィールドの解析をカプセル化するクラスを作成する必要があります。


  1. flatMapメソッドを実装します
  2. mapメソッドを実装します
  3. コンパニオンオブジェクトの適用操作も定義する必要があります。
  4. 最終的なクライアントコードによって呼び出され、宣言に不要な詳細が含まれないインターフェイスメソッドを定義する必要があります。

 class Parser[T, Src](private val p: Src => (T, Src)) { def flatMap[M](f: T => Parser[M, Src]): Parser[M, Src] = Parser { src => val (word, rest) = p(src) f(word).p(rest) } def map[M](f: T => M): Parser[M, Src] = Parser { src => val (word, rest) = p(src) (f(word), rest) } def parse(src: Src): T = p(src)._1 } 

では、 flatMapメソッドでは何が起こるのでしょうか?
現在のパーサーのハンドラーを入力値に適用し、メソッドの関数引数を使用して、チェーン内の後続のすべてのパーサーから見えるコンテキストに追加します。


mapメソッドを使用すると、すべてがより明確になります。引数-関数fを現在の単語に適用し、残りは変更しないままにします。


そして、ポイント操作を含むコンパニオンオブジェクトは、applyメソッドでもあり、かっこ付きのオブジェクトの呼び出しでもあります。


 object Parser { def apply[T, Src](f: Src => (T, Src)) = new Parser[T, Src](f) } 

申込み


それで何? モナドになじみのない同僚間のあなたの権限の疑いのない増加に加えて、このアプローチにはどのような利点がありますか? 今から見ます。


上で提案した抽象化を使用して、最終的に革新的で機能的でタイプセーフなCSVパーサーを作成します。


フィールド型パーサーの作成


最初に、String型の1つのフィールドのパーサーを実装します。


 def StringField = Parser[String, String] { str => val idx = str.indexOf(separator) if (idx > -1) (str.substring(0, idx), str.substring(idx + 1)) else (str, "") } 

複雑なことはありませんか?


ここで、StringFieldに基づいてIntパーサーを定義する方法を見てみましょう。
さらに簡単に!


 def IntField = StringField.map(_.toInt) 

同様に、他のすべてについて:


 def BigDecimalField = StringField.map(BigDecimal(_)) def IntField = StringField.map(_.toInt) def BooleanField = StringField.map(_.toBoolean) //      

すべてをまとめる


これまで、個々のフィールドのパーサーのみを検討してきましたが、これらのフィールドを単一のエンティティにどのようにアセンブルしますか? これがまさにまさに文脈が救いに来るところです。 そのおかげで、下にあるパーサーで、上にあるパーサーで取得した値を使用できます。


したがって、最終的なエンティティパーサーの構築は次のようになります。


 val parser = for { year <- IntField mark <- StringField model <- StringField comment <- StringField price <- BigDecimalField } yield Car(year, mark, model, comment, price) 

私の意見では、非常にクールに見えます。
理解のための構文糖衣に突然完全に自信を感じない場合、このようなものはflatMapsのチェーンのように見えます:


 IntField.flatMap { year => StringField.flatMap { mark => StringField.flatMap { model => StringField.flatMap { comment => BigDecimalField.map { price => Car(year, mark, model, comment, price) } } } } } 

もちろん、少し悪いように見えますが、どのコンテキストが関係しているかが明らかになります。これらは中括弧で制限された可視性の領域です。


パーサーパーサーを取得しました。ソースファイルを1行ずつ解析メソッドにフィードし、結果を取得するだけです。 たとえば、次のように:


 val result = str.split('\n').map(parser.parse) 

結果:


 Array(Car(1997,Ford,E350,ac, abs, moon,3000.00), Car(1996,Jeep,Grand Cherokee,MUST SELL! air, moon roof, loaded,4799.00), Car(1999,Chevy,Venture "Extended Edition",,4900.00)) 

長所



短所



まとめ


ロックのモナドや他のカテゴリーは、あなたがなしでは生きていけないものではありません。 さらに、それらは実際には言語自体によって課されるものではありません。 本質的に、ロックのモナドネスは小さなアドホックな契約であり、それを実行することにより、理解のためにクラスを使用する機会が得られます。 そしてそれだけです。


それにも関わらず、言語の柔軟性と非常にトリッキーな構造を簡単に実装する能力は、言語の無条件のプラスであり、実験のために手を解放します。


実稼働コードでこの種の構成を使用するかどうかについてはわかりませんが、これは各チームの選択です。 おそらく、私は最初にそれらを別々のライブラリに分離し、テストでカバーし、あらゆる方法で実行しようとします(実際の機能では、テストなしですべてが機能することは確かです)。 ここで必要なロジックについては、より単純な実装を使用したいと思います。



Source: https://habr.com/ru/post/J326002/


All Articles