2013-07-02 20 views
7

Uso l'Etilit di Scalaz 7 per costruire le incomprensioni che fondono State e \ /. Fin qui tutto bene; Ottengo qualcosa che è fondamentalmente:Come restituire una tupla in un EitherT

State[MyStateType, MyLeftType \/ MyRightType] 

e che mi permette di costruire per-comprensioni che hanno belle variabili sul lato sinistro della < -.

Ma non riesco a capire come restituire le tuple da un'azione di stato. I singoli risultati vanno bene - nel codice qui sotto, "val comprehension" è esattamente ciò che voglio accadere.

Ma le cose vanno a pezzi quando voglio restituire una tupla; "Val otherComprehension" non mi lascia fare

(a, b) <- comprehension 

Sembra che si aspetta che il lato sinistro del \/per essere un Monoide e non capisco perché. Cosa mi manca?

(Scalaz 7 2.0.0-SNAPSHOT, Scala 2.10.2)

object StateProblem { 
    case class MyStateType 
    case class MyRightType 
    case class MyLeftType 

    type StateWithFixedStateType[+A] = State[MyStateType, A] 
    type EitherTWithFailureType[F[+_], A] = EitherT[F, MyLeftType, A] 
    type CombinedStateAndFailure[A] = EitherTWithFailureType[StateWithFixedStateType, A] 

    def doSomething: CombinedStateAndFailure[MyRightType] = { 
    val x = State[MyStateType, MyLeftType \/ MyRightType] { 
     case s => (s, MyRightType().right) 
    } 
    EitherT[StateWithFixedStateType, MyLeftType, MyRightType](x) 
    } 

    val comprehension = for { 
    a <- doSomething 
    b <- doSomething 
    } yield (a, b) 

    val otherComprehension = for { 
    // this gets a compile error: 
    // could not find implicit value for parameter M: scalaz.Monoid[com.seattleglassware.StateProblem.MyLeftType] 
    (x, y) <- comprehension 

    z <- doSomething 
    } yield (x, y, z) 
} 

Edit: ho la prova che MyLeftType è una monade, anche se non è aggiunto. Nel mio codice reale, MyLeftType è una classe caso (chiamato EarlyReturn), così posso fornire uno zero, ma accodare funziona solo se uno degli argomenti è uno zero:

implicit val partialMonoidForEarlyReturn = new Monoid[EarlyReturn] { 
    case object NoOp extends EarlyReturn 
    def zero = NoOp 
    def append(a: EarlyReturn, b: => EarlyReturn) = 
     (a, b) match { 
     case (NoOp, b) => b 
     case (a, NoOp) => a 
     case _   => throw new RuntimeException("""this isnt really a Monoid, I just want to use it on the left side of a \/""") 
     } 
    } 

Io non sono convinto che questo sia un buona idea, ma sta risolvendo il problema.

+1

C'è qualcosa di strano nel modo in cui 2.10.1+ desugars la 'for'-comprensione qui-vedi [questa domanda] (http://stackoverflow.com/q/17424763/334519) per una versione semplificata del stesso problema. –

+2

E per essere chiari, questo sta accadendo perché il filtro 'EitherT' (o' \/') richiede un'istanza monoid per il lato sinistro, e per qualche ragione 2.10.2 sta attaccando un'operazione di filtro in questo' for'-comprehension. –

risposta

4

Come faccio notare in un commento precedente, il problema è che la versione Dezuccherato del vostro secondo for -comprehension comporta un operazione di filtraggio in 2.10.2 (e 2.10.1, ma non 2.10.0), e non è possibile filtrare EitherT (o semplice vecchio \/) senza un'istanza monoid per il tipo sul lato sinistro.

E 'abbastanza facile capire il motivo per cui il monoide è necessaria nel seguente esempio:

val x: String \/ Int = 1.right 
val y: String \/ Int = x.filter(_ < 0) 

Qual è y? È chiaro che deve essere una sorta di "vuoto" String \/ Int, e dal momento che \/ è corretto, sappiamo che non può essere un valore da quella parte. Quindi abbiamo bisogno di uno zero per il lato sinistro, e l'istanza per monoide String fornisce questo-è solo la stringa vuota:

assert(y == "".left) 

Secondo this answer-my related question sui modelli di tuple in for -comprehensions, il comportamento si è vedere in 2.10.2 è corretto e inteso: la chiamata apparentemente completamente inutile a withFilter è qui per rimanere.

È possibile utilizzare la soluzione in risposta di Petr Pudlak, ma è anche la pena notare che la seguente versione senza zucchero è anche abbastanza chiaro e conciso:

val notAnotherComprehension = comprehension.flatMap { 
    case (x, y) => doSomething.map((x, y, _)) 
} 

Questo è più o meno quello che avrebbe ingenuamente si aspettano il for -comprensione a desugar a, comunque (e io sono not the only one).

2

senza conoscere la causa, ho trovato una possibile soluzione:

for { 
    //(x, y) <- comprehension 
    p <- comprehension 

    z <- doSomething 
} yield (p._1, p._2, z) 

o forse leggermente migliore

for { 
    //(x, y) <- comprehension 
    p <- comprehension 
    (x, y) = p 

    z <- doSomething 
} yield (x, y, z) 

Non è molto bello, ma non il lavoro.

(ho veramente apprezzare che hai fatto un self-contained, esempio di lavoro del problema.)

+1

Invece di x = p._1; y = p._2; puoi semplicemente fare (x, y) = p. Sembra strano che tu debba farlo su un'altra linea (link e commenti da @TravisBrown ha informazioni utili). –

+0

@JamesMoore Vero, corretto. –