2017-12-04 7 views
2

ここでは以下のコードのためのscasteです。まず、items.cgiは、次の形式で項目オブジェクトのリストを返しますスカラ座、どちらか[_、配列[いずれかの[_、T]]のいずれかに[_、配列[T]]

$ curl http://example.com/items.cgi 
[ 
    ... 
    { sn: "KXB1333", ownerId: 3, borrowerId: 0 }, 
    { sn: "KCB1200", ownerId: 1, borrowerId: 2 }, 
    ... 
] 

borrowerId == 0は、アイテムが何の借り手を持っていないことを意味します。

第二に、users.cgiは、idクエリパラメータ

$ curl http://example.com/user.cgi?id=1 
{ id: 1, name: "frank" } 

で指定されたユーザーを返すAPIが悪いかもしれないが、私はそれに対処する必要があります。今、Scalaで私はまた、HTTPを行うために、以下のいるこの素敵なデータモデル

case class User(id: Int, name: String) 
case class Item(sn: String, owner: User, borrower: Option[User]) 

で作業したい

case class ApiFail(reason: String) 
def get[T](url: String): Either[ApiFail, T] = ??? /* omitted for brevity */ 

get()機能はURLからJSONを取得するためにいくつかの魔法を使用していますを要求そこからTを作ります(いくつかのライブラリを使います)。 IOエラーまたはHTTPステータスが不良の場合は、Leftを返します。私は、各項目がどの HTTPをリンクし、ユーザーをフェッチし、Item Sの新しいリストを返すか、に失敗するのは、項目のリストを取得する必要があり、以下の機能

def getItems: Either[ApiFail, Seq[Item]] 

を書きたいと思い

要求の失敗。 (そこに同じIDを持つユーザーのための冗長要求かもしれないが、私はまだおよそメモ化/キャッシュを気にしないでください。)

は、これまでのところ私はこの関数を記述するために管理

def getItems: Either[ApiFail, Seq[Either[ApiFail, Item]]] 

場所一部のユーザーの検索に失敗した場合は、結果全体ではなく、対応する項目のみが致命的です。ここでは実装が

def getItems: Either[ApiFail, Seq[Either[ApiFail, Item]]] = { 
    case class ItemRaw(sn: String, ownerId: Int, borrowerId: Int) 

    get[List[ItemRaw]]("items.cgi").flatMap(itemRawList => Right(
     itemRawList.map(itemRaw => { 
      for { 
       owner <- get[User](s"users.cgi?id=${itemRaw.ownerId}") 
       borrower <- 
        if (itemRaw.borrowerId > 0) 
         get[User](s"users.cgi?id=${itemRaw.borrowerId}").map(Some(_)) 
        else 
         Right(None) 
      } yield 
       Item(itemRaw.sn, owner, borrower) 
     }) 
    )) 
} 

だこれは宿題のための要求のように思えるが、私が別のラッパー事(M-モナド?)から切り替えたいと私は少し困惑だと頻繁に私に起こりますラッパー関数(c-combinators?)のみで行う方法についてです。私はもちろん、命令的な実装に切り替えることができます。私はちょうど興味がある。

答えて

3

FP世界でこれを正確に実行する言葉があります。「トラバース」(link to cats implementation)です。 F[A]と機能A => G[B]があり、G[F[B]]が必要な場合に使用されます。ここで、FList,AItemRaw,GEither[ApiFail, _]BItemである。もちろん、FGにはいくつかの制約があります。

猫を使用して、あなたは非常にわずかにあなたの方法を変更することができます、私は確かにそのルートの下に完全に行くことを躊躇されて理解することができたものと

import cats._, cats.implicits._ 

def getItems: Either[ApiFail, Seq[Item]] = { 
    case class ItemRaw(sn: String, ownerId: Int, borrowerId: Int) 

    get[List[ItemRaw]]("items.cgi").flatMap(itemRawList => 
    itemRawList.traverse[({type T[A]=Either[ApiFail, A]})#T, Item](itemRaw => { 
     for { 
     owner <- get[User](s"users.cgi?id=${itemRaw.ownerId}") 
     borrower <- 
      if (itemRaw.borrowerId > 0) 
      get[User](s"users.cgi?id=${itemRaw.borrowerId}").map(Some(_)) 
      else 
      Right(None) 
     } yield 
     Item(itemRaw.sn, owner, borrower) 
    }) 
) 
} 

を。猫(そしてscalaz)はたくさん取り込まれます - しかし、私はいつかあなたがすることをお勧めします!

それらがなければ、あなたは常にあなたの一般的に使用されるコンテナを操作するための独自のユーティリティメソッドを書くことができます。

def seqEither2EitherSeq[A, B](s: Seq[Either[A, B]]): Either[A, Seq[B]] = { 
    val xs: Seq[Either[A, Seq[B]]] = s.map(_.map(b => Seq(b))) 
    xs.reduce{ (e1, e2) => for (x1 <- e1; x2 <- e2) yield x1 ++ x2 } 
} 

def flattenEither[A, B](e: Either[A, Either[A, B]]): Either[A, B] = e.flatMap(identity) 

次に、あなたが望む結果は次のようになります。

val result: Either[ApiFail, Seq[Item]] = flattenEither(getItems.map(seqEither2EitherSeq)) 
+0

は、詳細な説明をありがとうございました。学ぶべきことがたくさんあります。 'traverse'の型パラメータはやや困惑しています。私は3つ必要だとソースから見てきましたが、2つしか指定しません。これは '-Ypartial-unification' scalacフラグ付きの型パラメータなしで動作します。 https://scastie.scala-lang.org/8c1pdg7eTeqzQGRUDLPQOw – woky

+0

catsは、simulacrum(https://github.com/mpilquist/simulacrum)を使用して、タイプクレーンの構文を追加します。トラバースメソッドは実際に 'Traverse.Ops'クラスなどで生成されたもので、' List'からの暗黙の変換を行います。 'F'と' A'型のパラメータは、それを呼び出すオブジェクト( 'itemRawList')から得られ、' G'型と 'B'型のパラメータだけが必要です。 –

関連する問題