-
-
Save yujikiriki/3d1b98342de4ddc78c44 to your computer and use it in GitHub Desktop.
case class OrderIncomeReportEntry( month: String, value: Double ) | |
private def sumValues( entries: List[ OrderIncomeReportEntry ] ): List[OrderIncomeReportEntry] = { | |
val byMonth: Map[ String, List[ OrderIncomeReportEntry ] ] = entries.groupBy( e => e.month ) | |
byMonth.foldLeft( List[OrderIncomeReportEntry]() ) { | |
( list, value ) => | |
val sum = value._2.foldLeft( 0.0 )( ( acc, oire ) => acc + oire.value ) | |
list.+:( OrderIncomeReportEntry( value._1, sum ) ) | |
} | |
} | |
Rpta: [{"order":{"month":"08"},"value":200.0},{"order":{"month":"01"},"value":100.0}] |
Señores, me tomó más de 6 meses ponerme al día para poder dar una respuesta al post laaaaaaargo de Vilá. Sin embargo, algunas observaciones de lo que me respondió a la solución propuesta en ese momento:
Lo primero es que pensando la implementación del Monoid me detuve en la >definición del elemento identidad y se me hizo feo eso de poner en el mes un string cualquiera ("" en el caso de Juan Pablo). Claro, uno sabe según la forma en la que se define el append puede no importar ese valor en tanto uno lo ignore (por eso es que Juan Pablo puso b.month y no a.month). Además ¿que significa en el dominio el OrderIncomeReportEntry identidad? ¿No debería ser una entidad que tenga sentido instanciar en cualquier contexto y no solamente para hacer calculos?
Dado el escenario propuesto, pienso que el monoide al tener efecto sólo sobre el campo "value" omite, sin sacrificar la semántica del problema, el campo "month".
Siendo así, creo que en vez de concentrarse en modelar en un ADT los meses como en su ejemplo final
sealed abstract class Month(name: String)
case object January extends Month("January")
case object February extends Month("February")
// etc, ...
case class OrderIncomeReportEntry[M<:Month]( month: M, value: Double )
yo me habría concentrado en modelar el campo "value" como un ADT. Algo así:
case class Money(value: Double)
case class OrderIncomeReportEntry( month: String, value: Money)
y haber creado el monoide sobre Money
:
trait Monoid[T] {
def zero: T
def op(t1: T, t2: T): T
}
implicit def MoneyAdditionMonoid = new Monoid[Money] {
def zero = zeroMoney
def append(m1: Money, m2: Money) = Money(m1.value + m2.value)
}
La cosa que me queda en duda y que no se si se podría hacer es que, dado que partimos de un OrderIncomeReportEntry
poder meter dentro de un lense
sobre OrderIncomeReportEntry
esa suma de Money
s.
Otra opción podría ser crear un tipo genérico "sumable" y crearle su monoide, que la final de cuentas termina siendo nada más que un Monoid[Double] o algo así...
Por otro lado y viendo su propuesta de semigrupos, dado que monoide es "hijo" de semigrupo y que monoide sólo agrega identidad a semigrupo, es tentador dejarlo en términos de semigrupo; solución que quizás sea la que más me convence.
Estos días estuve pensando muuuucho sobre esto y hubo varias cosas que me rayaron de la solución del monoide. Lo que sigue son algunos pensamientos incompletos:
Lo primero es que pensando la implementación del Monoid me detuve en la definición del elemento identidad y se me hizo feo eso de poner en el mes un string cualquiera (
""
en el caso de Juan Pablo). Claro, uno sabe según la forma en la que se define elappend
puede no importar ese valor en tanto uno lo ignore (por eso es que Juan Pablo pusob.month
y noa.month
). Además ¿que significa en el dominio elOrderIncomeReportEntry
identidad? ¿No debería ser una entidad que tenga sentido instanciar en cualquier contexto y no solamente para hacer calculos?Entonces por eso es que se me ocurrió que en vez de definir un Monoide uno podría definir un Semigrupo (que exige únicamente implementar la función
append
). En este caso en particular como el groupBy asegura que está agrupando con listas de por lo menos un elemento (no tiene sentido tener unMap[ String, List[ OrderIncomeReportEntry ] ]
con valores de listas vacías) entonces uno no tiene la necesidad de definir un elemento identidad con el que empezar la computación.Siendo así la cosa podría ser:
foldMap1Opt
, que es un nombre como feo, es la función en scalaz que utiliza un semigrupo sobre unFoldable
para sumarlo. Retorna unSome
de la suma por que puede que elFoldable
sea vacío. En este caso, por la forma en que funciona elgroupBy
, sabemos que esto nunca va a pasar entonces podemos llamarget
libremente. Están esos detalles feos: el nombre de la función y el llamado alget
, entonces este acercamiento, si bien es "minimalista" termina con esas torpezas (tal vez haya una función mejor en scalaz, hasta ahora no la he encontrado).Alejándome del tema un poquito hubiera sido chévere que la librería estandar de scala hubieran definido la función
groupBy
de forma que los valores no fueran unList
sino algo como unNonEmptyList
, de forma que en los tipos se hiciese explícito que no va a estar vacia. De ser así en scalaz podría existir unfold
que funcionase sobreNonEmptyList
y que utilizase un semigrupo. Si así fuera esa función no tendría por que retornar unOption
sino de una el resultado.La otra cosa que me rayó es que en ambas definiciones de
append
, tanto la del Monoide como la del Semigrupo de arriba, no se está evitando la posibilidad de que se llame con instancias deOrderIncomeReportEntry
de meses distintos: ¿Cual debería ser el resultado entonces? ¿Se debería elegir arbitrariamente el mes de alguno de los dos para el resultado?Digamos que se quiere evitar sumar
OrderIncomeReportEntry
's de meses distintos. Tal vez este no sea el caso pero la idea es ver que tan lejos se puede llegar. Uno podría hacer que el tipoOrderIncomeReportEntry
codificase adicionalmente el mes:Entonces ahora los semigrupos están restringidos a operar sobre
OrderIncomeReportEntry
del mismo tipo y por lo tanto del mismo mes:Entonces el intento de sumar Entries de meses distintos se convierte en un error en tiempo de compilación:
Dado esto ¿Como podría definir uno la función
sumValues
? . Mi sospecha es que el sistema de tipos le ayuda a uno hasta donde empieza el "no determinismo". Sería chévere ver si el compilador se da cuenta que elgroupBy
en el fondo está segmentando losOrderIncomeReportEntry
de acuerdo a su mes, pero lo dudo. No me he puesto a experimentar tanto con eso.